这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 AOP 用于把日志、监控、权限、事务、限流、审计这类横切逻辑从业务代码中抽离。Spring AOP 是运行时代理模型,理解代理边界比记住切点语法更重要。
5.1 核心概念
| 概念 | 说明 |
|---|---|
| Joinpoint | 程序执行点,Spring 主要是方法执行 |
| Pointcut | 匹配哪些 Joinpoint |
| Advice | 匹配后执行的逻辑 |
| Aspect | 切面,切点与通知的组合 |
| Target Object | 被代理目标 |
| AOP Proxy | 容器暴露的代理对象 |
| Weaver | 织入者,Spring 在运行时创建代理 |
常用通知:
| 注解 | 时机 |
|---|---|
@Before |
方法前 |
@AfterReturning |
正常返回后 |
@AfterThrowing |
抛异常后 |
@After |
finally 语义 |
@Around |
包裹方法,可控制是否执行 |
5.2 第一个切面
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("within(com.example.order.service..*)")
public void orderServiceMethods() {}
@Around("orderServiceMethods()")
public Object logCost(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long cost = (System.nanoTime() - start) / 1_000_000;
log.info("{} cost={}ms", pjp.getSignature().toShortString(), cost);
}
}
}
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
5.3 切点表达式
execution(* com.example.order.service.OrderService.create(..))
within(com.example.order.service..*)
@annotation(com.example.log.LogCost)
@within(com.example.log.LogModule)
bean(orderService)
target(com.example.order.service.OrderService)
常用表达式:
| 表达式 | 说明 |
|---|---|
execution |
按方法签名匹配 |
within |
按类型包范围匹配 |
@annotation |
按方法注解匹配 |
@within |
按类注解匹配 |
bean |
按 Bean 名称匹配 |
args |
按参数类型匹配 |
工程建议:避免复杂通用表达式,优先使用注解切点,可读性更好。
5.4 自定义注解切面
注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogCost {
String value() default "";
}
切面:
@Aspect
@Component
public class LogCostAspect {
@Around("@annotation(logCost)")
public Object around(ProceedingJoinPoint pjp, LogCost logCost) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
System.out.printf("%s cost %dms%n",
logCost.value(), System.currentTimeMillis() - start);
}
}
}
使用:
@LogCost("createOrder")
public Order create(CreateOrderCommand command) {
return repository.save(new Order(command.userId()));
}
5.5 JDK 动态代理与 CGLIB
| 代理 | 条件 | 特点 |
|---|---|---|
| JDK 动态代理 | 目标有接口 | 基于接口反射 |
| CGLIB | 无接口或强制使用 | 生成子类 |
Spring Boot 默认通常使用 CGLIB 代理。CGLIB 限制:
- final 类不能代理;
- private 方法不能拦截;
- final 方法不能覆盖;
- 构造器不能是私有或不可访问;
- 代理会引入类型差异;
- 自调用问题依然存在。
5.6 自调用失效
@Service
public class OrderService {
public void create(Order order) {
this.validate(order);
}
@LogCost("validate")
public void validate(Order order) {
}
}
外部调用:
Proxy.create()
-> target.create()
-> this.validate() // 直接调用目标对象
this.validate() 绕过代理,所以切面不执行。
解决方式:
- 拆分 Bean;
- 通过
AopContext.currentProxy(); - 自注入代理对象;
- 改成外部调用;
- 使用 AspectJ 编译期或类加载期织入。
最推荐第 1 种:把被拦截动作放到另一个职责清晰的 Bean。
5.7 参数与返回值
读取参数:
Object[] args = pjp.getArgs();
记录安全日志时必须脱敏:
Object[] args = pjp.getArgs();
for (Object arg : args) {
if (arg instanceof LoginCommand command) {
log.info("user={}", command.username());
}
}
修改返回值:
@Around("...")
public Object hidePhone(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
if (result instanceof UserView view) {
return new UserView(view.id(), mask(view.phone()));
}
return result;
}
不要在切面中随意吞异常:
try {
return pjp.proceed();
} catch (Exception e) {
return null; // 破坏调用方错误处理
}
5.8 切面顺序
@Aspect
@Order(1)
public class SecurityAspect {
}
@Aspect
@Order(2)
public class LoggingAspect {
}
简化规则:
order 小的 @Around 先进入
order 小的 @After 后执行
常见顺序:
审计 / 权限
-> 限流
-> 日志
-> 事务
-> 业务
事务通常在最内层,确保日志先记录请求,但具体顺序要按业务语义设计并写测试验证。
5.9 AOP 与事务的关系
@Transactional 本质是 AOP 拦截:
TransactionInterceptor
-> 开启事务
-> 执行业务方法
-> 正常提交
-> 异常回滚
因此以下情况会失效:
- 同类自调用;
- 方法不是 public;
- final 或 static 方法;
- 异常被内部吞掉;
- 默认回滚规则不匹配受检异常;
- 多线程内部逻辑不在同一事务上下文;
- 数据库引擎不支持事务;
- 传播行为设置错误。
5.10 性能与治理
切面不是免费能力:
- 反射获取注解有成本;
- 过宽切点会拦截大量方法;
- 同步日志可能拖慢请求;
- 代理对象创建增加启动成本;
- 复杂序列化可能泄露敏感数据。
规范:
- 切点必须精确;
- 高频路径加开关;
- 日志异步化;
- 参数脱敏;
- 异常只观察不吞;
- 给切面写单元测试;
- 使用 Micrometer 而不是手写指标协议;
- 不在切面中写业务规则。
本章小结
Spring AOP 通过运行时代理实现横切逻辑,核心是切点、通知和代理对象。CGLIB 与 JDK 动态代理各有条件,private、final 和自调用都会导致拦截失效。工程上应优先使用自定义注解切点,保持切面职责单一,并通过测试验证顺序和异常行为。
思考题
@Around和@After的执行顺序关系是什么?- 为什么自调用不经过代理?
- CGLIB 代理有哪些限制?
@Transactional为什么本质上也是 AOP?- 如何设计一个安全且高性能的日志切面?