本文从 Kiczales 1997 年提出 AOP 范式出发,阐明"横切关注点"问题的本质,逐层深入 Spring AOP 的代理机制、Advice 执行链路源码,并厘清 Spring AOP 与 AspectJ 的边界。
目录
| 章节 | 说明 |
|---|---|
| 理论基础 | AOP 编程范式起源、横切关注点、核心术语 |
| Spring AOP vs AspectJ | 两者定位、织入时机、能力边界 |
| 实现原理 | JDK 动态代理 vs CGLIB,选择策略 |
| 源码链路 | 代理创建 → Advice 责任链执行完整路径 |
| 通知类型与执行顺序 | 5 种通知、单切面/多切面执行顺序 |
| 切入点表达式 | execution / @annotation / within 等 |
| 常见坑 | 失效场景与根因 |
理论基础
横切关注点问题
1997 年,Gregor Kiczales 等人在施乐 PARC 研究中心提出 AOP(发表于 ECOOP '97),动机是解决 OOP 的一个固有局限:横切关注点(crosscutting concerns)无法被 OOP 干净地模块化。
典型的横切关注点:
日志记录 安全鉴权 事务管理 缓存 监控/埋点 异常处理
用 OOP 实现时,这些关注点会导致两种问题:
| 问题 | 说明 |
|---|---|
| 代码散射(Scattering) | 同一段日志/安全逻辑分散在数百个类中,改动一处需改动所有地方 |
| 代码纠缠(Tangling) | 业务逻辑与日志/事务代码混在一个方法里,职责不清晰 |
AOP 的解法:将横切关注点抽取为独立的切面(Aspect),通过织入(Weaving) 机制在不修改业务代码的前提下将其应用到目标位置。
"AOP complements OOP by providing another way of thinking about program structure." — Spring Framework 官方文档
核心术语
| 术语 | 英文 | 说明 |
|---|---|---|
| 切面 | Aspect | 横切关注点的模块化封装,包含 Pointcut + Advice |
| 连接点 | Join Point | 程序执行中可被拦截的点(Spring AOP 仅支持方法执行这一种) |
| 切入点 | Pointcut | 连接点的选择谓词("哪些方法"),用表达式描述 |
| 通知 | Advice | 在切入点处执行的动作("做什么"),分 Before/After/Around 等 |
| 目标对象 | Target | 被代理的原始对象 |
| 代理 | Proxy | 包裹目标对象的增强对象,AOP 的实际载体 |
| 织入 | Weaving | 将切面应用到目标的过程 |
| 引介 | Introduction | 为现有类动态添加新接口/方法(Spring AOP 支持,较少用) |
织入时机的三种选择
编译时织入(Compile-time weaving)
→ AspectJ 编译器(ajc)在编译阶段修改字节码
→ 性能最好,但需特殊编译流程
加载时织入(Load-time weaving, LTW)
→ JVM 加载类时通过 Java Agent 修改字节码
→ AspectJ 支持(-javaagent:aspectjweaver.jar)
运行时织入(Runtime weaving)
→ 通过动态代理在运行时创建增强对象 ← Spring AOP 采用此方式
→ 无需修改字节码,纯 Java 实现,灵活但有局限
Spring AOP vs AspectJ
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入方式 | 运行时动态代理 | 编译时 / 加载时 / 运行时 |
| Join Point | 仅方法执行 | 方法调用、字段读写、构造器、静态初始化等 |
| 依赖 | 纯 Spring,无需额外工具 | 需要 AspectJ 编译器(ajc)或 Weaving Agent |
| 性能 | 每次调用经过代理链,略有开销 | 字节码织入,接近原生性能 |
| 自调用 | ⚠️ 无法拦截(同类内部调用绕过代理) | ✅ 可以拦截 |
| final 类/方法 | ⚠️ CGLIB 无法代理 | ✅ 可以织入 |
| 适用场景 | 企业应用中 80% 的 AOP 需求 | 需要字段级拦截或高性能的场景 |
Spring 的选择:Spring AOP 使用 AspectJ 的注解语法(
@Aspect、@Pointcut、@Before等),但底层是自己的动态代理实现,而非 AspectJ 织入器。两者共用语法,实现独立。
实现原理
JDK 动态代理
// JdkDynamicAopProxy 实现 InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取该方法对应的拦截器链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
// 无拦截器,直接调用目标方法
return AopUtils.invokeJoinpointUsingReflection(target, method, args);
}
// 有拦截器,构建责任链
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
return invocation.proceed();
}
条件:目标类实现了至少一个接口。代理类实现相同接口,Proxy.newProxyInstance() 生成。
CGLIB 代理
CGLIB 通过 ASM 操作字节码,生成目标类的子类,覆盖父类方法并在其中嵌入拦截逻辑。
限制:
- 目标类不能是
final(无法继承) - 目标方法不能是
final/private(无法覆盖) - 构造器会被调用两次(生成子类 + 实例化)
选择策略
// DefaultAopProxyFactory#createAopProxy
public AopProxy createAopProxy(AdvisedSupport config) {
if (config.isOptimize() || config.isProxyTargetClass()
|| hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config); // 目标本身是接口/代理类 → JDK
}
return new ObjenesisCglibAopProxy(config); // 否则 → CGLIB
}
return new JdkDynamicAopProxy(config); // 有接口且不强制 CGLIB → JDK
}
Spring Boot 2.x 起默认 spring.aop.proxy-target-class=true,强制 CGLIB。
源码链路
代理创建:BeanPostProcessor 阶段
Bean 初始化完成
→ AbstractAutoProxyCreator#postProcessAfterInitialization()
→ wrapIfNecessary(bean, beanName, cacheKey)
→ getAdvicesAndAdvisorsForBean() // 找所有匹配该 Bean 的 Advisor
→ createProxy(beanClass, beanName, advisors, targetSource)
→ ProxyFactory#getProxy(classLoader)
→ DefaultAopProxyFactory#createAopProxy() // 决定 JDK / CGLIB
→ 返回代理对象,替换容器中的原始 Bean
Advisor 匹配过程
// AbstractAdvisorAutoProxyCreator#findEligibleAdvisors(精简)
// 1. 从容器获取所有 Advisor Bean
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// 2. 过滤出能匹配当前 Bean 的 Advisor
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
// 3. 排序(@Order / PriorityOrdered)
sortAdvisors(eligibleAdvisors);
Advice 执行:责任链模式
代理方法调用
→ ReflectiveMethodInvocation#proceed()
→ 遍历 interceptorList(MethodInterceptor 列表)
├── ExposeInvocationInterceptor(暴露 invocation 到 ThreadLocal)
├── AspectJAroundAdvice(Around → 调用 proceed() 推进链)
├── MethodBeforeAdviceInterceptor(Before)
├── AspectJAfterAdvice(After,finally 块执行)
├── AfterReturningAdviceInterceptor(AfterReturning)
└── AspectJAfterThrowingAdvice(AfterThrowing)
→ 所有 interceptor 走完后调用目标方法
责任链用递归索引推进(currentInterceptorIndex++),Around 通知通过调用 joinPoint.proceed() 驱动链条继续。
通知类型与执行顺序
5 种通知
| 注解 | 执行时机 | 能获取返回值 | 能修改返回值 | 能捕获异常 |
|---|---|---|---|---|
@Before |
目标方法执行前 | ❌ | ❌ | ❌ |
@AfterReturning |
正常返回后 | ✅ | ❌(只能读) | ❌ |
@AfterThrowing |
抛出异常后 | ❌ | ❌ | ✅(只能读) |
@After |
方法退出后(finally) | ❌ | ❌ | ❌ |
@Around |
完全包裹目标方法 | ✅ | ✅ | ✅ |
@Around最强大,可以修改参数、返回值,吞掉或替换异常。@Before/@After只能"观察",无法改变结果。
单切面内执行顺序(Spring 5.2.7+ 修正版)
正常执行:
@Around(前半)→ @Before → 目标方法 → @Around(后半)→ @AfterReturning → @After
异常执行:
@Around(前半)→ @Before → 目标方法(抛异常)→ @AfterThrowing → @After
⚠️ Spring 5.2.7 之前,同一切面内
@AfterReturning和@After的顺序与上述相反(@After先于@AfterReturning),5.2.7 修正为更符合直觉的顺序。
多切面执行顺序
多个切面作用于同一方法时,按 @Order 值排序,值越小优先级越高:
@Order(1) 切面 Around 前
@Order(2) 切面 Around 前
目标方法
@Order(2) 切面 Around 后 / AfterReturning / After
@Order(1) 切面 Around 后 / AfterReturning / After
外层切面先进后出(类似 Filter 链)。不指定 @Order 时顺序不确定,多切面务必显式指定。
切入点表达式
execution(最常用)
execution([访问修饰符] 返回类型 [类路径.]方法名(参数类型) [throws 异常])
// 示例
execution(* com.example.service.*.*(..)) // service 包下所有方法
execution(public * com.example..*.save*(..)) // 所有 save 开头的 public 方法
execution(* *(..) throws IOException) // 声明抛出 IOException 的方法
通配符:
*匹配任意单个元素(单个包名、方法名、类型)..匹配任意多个(多级包路径、任意参数列表)
其他常用表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
@annotation(ann) |
方法上有指定注解 | @annotation(com.example.Log) |
@within(ann) |
类上有指定注解(该类所有方法) | @within(org.springframework.stereotype.Service) |
within(type) |
指定类型内的方法 | within(com.example.service.*) |
bean(name) |
指定 Spring Bean 名称 | bean(orderService) |
args(types) |
参数类型匹配 | args(java.io.Serializable) |
@args(ann) |
参数有指定注解 | @args(com.example.Valid) |
切入点复合
@Aspect
@Component
public class LogAspect {
// 定义可复用的切入点
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("@annotation(com.example.annotation.Log)")
public void logAnnotated() {}
// 复合:service 层 且 有 @Log 注解
@Around("serviceLayer() && logAnnotated()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
// ...
return pjp.proceed();
}
}
常见坑
| # | 现象 | 根因 | 解决方案 |
|---|---|---|---|
| 1 | 同类内部方法调用切面失效 | this 调用绕过代理对象 |
注入自身 Bean 调用,或改用 AspectJ LTW |
| 2 | private / final 方法切面失效 |
CGLIB 无法覆盖 private/final 方法 | 改为 public,或用 AspectJ |
| 3 | 接口上的注解切面失效(CGLIB 模式) | CGLIB 代理的是类,读不到接口注解 | 注解加在实现类/方法上,或切换 JDK 代理 |
| 4 | 多切面顺序混乱 | 未指定 @Order |
所有切面显式加 @Order(n) |
| 5 | @Around 忘记调用 proceed() |
目标方法不会执行 | 必须调用 pjp.proceed() 并返回结果 |
| 6 | @AfterReturning 拿不到返回值 |
未绑定 returning 参数 |
@AfterReturning(returning = "result") |
| 7 | @Transactional + 自定义切面顺序错误 |
事务切面和自定义切面 @Order 冲突 |
事务切面默认 Integer.MAX_VALUE,自定义切面设更大值则在事务外 |
坑 1 详解:自调用失效
@Service
public class OrderService {
// ❌ this.audit() 直接调用,绕过代理,@Log 切面不触发
public void placeOrder() {
this.audit("place");
}
@Log
public void audit(String action) { ... }
}
// ✅ 注入自身
@Service
public class OrderService {
@Autowired
private OrderService self;
public void placeOrder() {
self.audit("place"); // 通过代理调用,切面生效
}
@Log
public void audit(String action) { ... }
}
坑 5 详解:@Around 必须 proceed
// ❌ 忘记 proceed,目标方法永远不会执行
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
log.info("before");
// 漏了 pjp.proceed()!
return null;
}
// ✅ 正确写法
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("before: {}", pjp.getSignature());
Object result = pjp.proceed(); // 推进责任链/执行目标方法
log.info("after: {}", result);
return result; // 必须返回,否则调用方拿到 null
}
参考资料
- Spring Framework Reference — Aspect Oriented Programming with Spring
- The AspectJ™ Programming Guide
- Gregor Kiczales et al., "Aspect-Oriented Programming", ECOOP 1997
- Spring IoC 与 Bean 生命周期(BeanPostProcessor 是代理创建的触发点)
- Spring 事务管理(事务是 AOP 最典型的应用)
评论 (0)
发表评论