SPRING AOP · JDK DYNAMIC PROXY · CGLIB · METHODINVOCATION
你拿到的不是它本人,是它的替身
给方法加上 @Transactional、@Async 或自己写的切面后,容器交给你的就不再是原来那个对象,而是一个替身。每次调用先到替身,替身带着调用穿过一串拦截器,最后才交给真身。第 02 篇讲了 AOP 怎么用,这一篇讲替身本身是怎么回事。
OrderController以为自己拿着 OrderServiceOrderService$$SpringCGLIB$$0重写了方法,先把调用拦下OrderService你写的那个类,字段都注入好了Proxy长得和真身一样,手里攥着真身的引用。外人只看得到它。
JDK 动态代理或 CGLIB 子类target你写的那个对象。真正的业务代码在它身上跑。
容器不直接把它交给别人AutoProxyCreator就是第 04 篇那个改装师:Bean 初始化完成后看看要不要换成替身。
AnnotationAwareAspectJAutoProxyCreatorAdvisor切点(管哪些方法)加通知(做什么)。一个顾问变成链上的一个拦截器。
Pointcut + Advice替身是什么时候换上去的
在 Bean 一生的第 10 站(第 04 篇):初始化全做完之后,改装师最后过一遍手。放上成品架的、注入给别人的,都是替身。
真身出生 真身的字段都有值了 还在真身身上 这里换成替身 放的是替身OrderService@7a3fOrderService$$SpringCGLIB$$0@5e21public Object postProcessAfterInitialization(Object bean, String beanName) {
return wrapIfNecessary(bean, beanName);
}
protected Object wrapIfNecessary(Object bean, String beanName) {
// 1. 找出管得着这个 Bean 的顾问:
// 所有 Advisor Bean + 所有 @Aspect 里的通知方法,逐个用切点去匹配它的方法
Object[] advisors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName);
if (advisors == DO_NOT_PROXY) {
return bean; // 一个都没有:原样返回,不做替身
}
// 2. 有的话,用 ProxyFactory 做一个替身,把真身和顾问都交给它
ProxyFactory pf = new ProxyFactory(bean);
pf.addAdvisors(advisors);
return pf.getProxy(); // 返回的替身,代替真身放上成品架
}
getEarlyBeanReference 里提前把替身做出来,保证借出去的和最后放上架的是同一个替身。这就是三级缓存存在的原因(第 04 篇)。两种做替身的办法
JDK 动态代理冒充“同一个接口”,CGLIB 冒充“同一个类”。两种替身都攥着真身的引用,调用最后都转到真身上。
JDK 动态代理JdkDynamicAopProxy
(真身)
(替身)
替身的每个方法都转给 InvocationHandler.invoke,再由它走拦截器链
- JDK 自带,不用额外的库
- 只能冒充接口:只有接口里的方法会被拦截
- 注入时只能用接口类型接,用实现类接会报错
CGLIB 子类CglibAopProxy
替身重写每个能重写的方法,转给 MethodInterceptor,再走拦截器链
- 不需要接口,用类类型注入也没问题
- final 类不能代理,final、private、static 方法拦不到
- 替身对象用 Objenesis 造,不调构造器,也没注入过字段
默认 proxyTargetClass = false:真身实现了接口就用 JDK 动态代理,没有接口才用 CGLIB。
默认 spring.aop.proxy-target-class=true:有没有接口都用 CGLIB,省得注入时类型对不上。想改回来就把它设成 false。
一次调用怎么穿过替身
OrderController 调 orderService.placeOrder(o)。placeOrder 上有 @Transactional,还被一个 @Order(1) 的日志切面管着。选一个剧情,一步一步看调用走到了哪。
orderService.placeOrder(o)DynamicAdvisedInterceptor框架自动加在最前面@Order(1)@TransactionalplaceOrder 的业务代码拦截器链:一个下标,一层层往里
链是一个列表,ReflectiveMethodInvocation 手里攥着一个下标。每个拦截器在自己的代码中间调 proceed(),下标加一,交给下一个;走到头就调真身。
private int currentInterceptorIndex = -1;
public Object proceed() throws Throwable {
if (currentInterceptorIndex == interceptors.size() - 1) {
return invokeJoinpoint(); // 链走完了:反射调用真身的方法
}
Object next = interceptors.get(++currentInterceptorIndex);
return ((MethodInterceptor) next).invoke(this);
// 下一个拦截器做完前半段,会再调 proceed(),于是又回到这里
}
@Aspect
@Component // 只写 @Aspect 不会被当成 Bean
@Order(1) // 数字越小越在外层
public class LogAspect {
@Around("execution(* com.shop.service..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long t = System.currentTimeMillis();
try {
return pjp.proceed(); // 交给链上的下一个
} finally {
log.info("{} 用时 {} ms", pjp.getSignature().getName(),
System.currentTimeMillis() - t);
}
}
}
| 注解 | 背后的顾问 / 拦截器 | 谁负责做替身 | 默认顺序 |
|---|---|---|---|
| @Aspect 里的通知 | 每个通知方法变成一个 Advisor | AnnotationAwareAspectJAutoProxyCreator | 看切面类上的 @Order,没写就排最后 |
| @Transactional | TransactionInterceptor | 同上 | 最低优先级,可用 @EnableTransactionManagement(order = …) 调 |
| @Cacheable | CacheInterceptor | 同上 | 最低优先级,可用 @EnableCaching(order = …) 调 |
| @Async | AsyncExecutionInterceptor | AsyncAnnotationBeanPostProcessor,另一个改装师 | 加到已有替身的拦截器链最前面 |
| @Validated 方法校验 | MethodValidationInterceptor | MethodValidationPostProcessor | 加到已有替身的拦截器链末尾 |
一个 Bean 被几个顾问管着,通常也只有一个替身,所有拦截器排在同一条链上。
替身管不到的地方
只要调用没经过替身,或者替身重写不了那个方法,切面就不起作用。不报错,静悄悄失效。
| 情况 | JDK 代理 | CGLIB | 为什么 | 怎么办 |
|---|---|---|---|---|
| 同一个类里 this.xxx() | 拦不到 | 拦不到 | this 是真身,调用没经过替身 | 挪到另一个 Bean;或注入自己再调 |
| final 方法 | 不涉及 | 拦不到,还可能 NPE | 子类重写不了,直接在替身身上执行,替身的字段是空的 | 去掉 final |
| final 类 | 不涉及 | 启动报错 | 没法生成子类 | 去掉 final,或改用接口 + JDK 代理 |
| private、static 方法 | 拦不到 | 拦不到 | 不能重写,也不在接口里 | 改成 public 实例方法 |
| 接口里没有的方法 | 拦不到 | 能拦 | JDK 替身只实现了接口 | 加到接口里,或用 CGLIB |
| 自己 new 的对象 | 拦不到 | 拦不到 | 不是容器造的,没人给它换替身 | 从容器里拿 |
| 构造器、@PostConstruct 里调本类方法 | 拦不到 | 拦不到 | 那时替身还没换上,而且也是 this 调用 | 放到 ApplicationReadyEvent 之后 |
JDK 8 和 JDK 17:Spring 5 和 Spring 6
做替身的时机、两种替身、拦截器链,两边都一样。肉眼能看到的区别,主要是替身的类名。
Spring 5 · Boot 2
JDK 8+- CGLIB 替身的类名:
OrderService$$EnhancerBySpringCGLIB$$a1b2c3d4 - JDK 替身在 JDK 8 上叫
com.sun.proxy.$Proxy75 - @Transactional 在 CGLIB 替身上只对 public 方法生效
Spring 6 · Boot 3
JDK 17+- CGLIB 替身的类名变短:
OrderService$$SpringCGLIB$$0 - JDK 16 起,JDK 替身放在动态模块里:
jdk.proxy2.$Proxy75 - CGLIB 替身上,protected 和包可见方法的 @Transactional 也生效
- 提前编译(AOT)时,CGLIB 替身类在构建期就生成好,打成原生程序也能用
常踩的坑
先问一句:这次调用,到底有没有经过替身?
this 是真身。把方法挪到另一个 Bean;或者注入自己(@Lazy 注入 self);或者 @EnableAspectJAutoProxy(exposeProxy = true) 后用 AopContext.currentProxy()。
CGLIB 重写不了它,代码直接在替身对象上跑,而替身没注入过字段。去掉 final。Kotlin 的类和方法默认是 final,要用 kotlin-spring 插件。
用了 JDK 代理,却用实现类类型注入。改用接口类型注入,或者设 proxyTargetClass = true。
切面类只写了 @Aspect,没写 @Component;或者切点表达式的包名写错了,一个方法都没匹配上。
两个都是最低优先级时,谁在外层不确定。给切面加 @Order,或用 @EnableTransactionManagement(order = …) 把顺序定死。
getClass() 返回的是替身类。用 AopUtils.getTargetClass(bean) 或 AnnotationUtils.findAnnotation(…)。
这个 Bean 被某个改装师提前用到了,造得太早,错过了做替身那一步,上面的切面不生效。改装师的依赖用 ObjectProvider 延迟获取。
@Around 里 catch 住异常没再抛出去,外层的事务也以为成功,会照常提交。记完日志要原样抛出。