SPRING AOP · JDK DYNAMIC PROXY · CGLIB · METHODINVOCATION

你拿到的不是它本人,是它的替身

给方法加上 @Transactional、@Async 或自己写的切面后,容器交给你的就不再是原来那个对象,而是一个替身。每次调用先到替身,替身带着调用穿过一串拦截器,最后才交给真身。第 02 篇讲了 AOP 怎么用,这一篇讲替身本身是怎么回事。

orderService.getClass() → class com.shop.service.OrderService$$SpringCGLIB$$0
调用方OrderController以为自己拿着 OrderService
替身OrderService$$SpringCGLIB$$0重写了方法,先把调用拦下
拦截器链
LogAspect(@Order(1))TransactionInterceptor
每个做完自己的事,再 proceed()
真身OrderService你写的那个类,字段都注入好了
替身Proxy

长得和真身一样,手里攥着真身的引用。外人只看得到它。

JDK 动态代理或 CGLIB 子类
真身target

你写的那个对象。真正的业务代码在它身上跑。

容器不直接把它交给别人
做替身的人AutoProxyCreator

就是第 04 篇那个改装师:Bean 初始化完成后看看要不要换成替身。

AnnotationAwareAspectJAutoProxyCreator
顾问Advisor

切点(管哪些方法)加通知(做什么)。一个顾问变成链上的一个拦截器。

Pointcut + Advice

替身是什么时候换上去的

在 Bean 一生的第 10 站(第 04 篇):初始化全做完之后,改装师最后过一遍手。放上成品架的、注入给别人的,都是替身。

3new OrderService(…) 真身出生
5注入依赖 真身的字段都有值了
7–9@PostConstruct 等初始化 还在真身身上
10postProcessAfterInitialization 这里换成替身
11放上成品架 放的是替身
真身OrderService@7a3f
替身(里面攥着真身)OrderService$$SpringCGLIB$$0@5e21
AbstractAutoProxyCreator简化版
public 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

interface OrderApi
↑ 两个都实现这个接口 ↑
OrderServiceImpl
(真身)
jdk.proxy2.$Proxy75
(替身)

替身的每个方法都转给 InvocationHandler.invoke,再由它走拦截器链

  • JDK 自带,不用额外的库
  • 只能冒充接口:只有接口里的方法会被拦截
  • 注入时只能用接口类型接,用实现类接会报错

CGLIB 子类CglibAopProxy

class OrderService(真身)
↑ 替身是它的子类 ↑
OrderService$$SpringCGLIB$$0(替身)

替身重写每个能重写的方法,转给 MethodInterceptor,再走拦截器链

  • 不需要接口,用类类型注入也没问题
  • final 类不能代理,final、private、static 方法拦不到
  • 替身对象用 Objenesis 造,不调构造器,也没注入过字段
纯 Spring:@EnableAspectJAutoProxy

默认 proxyTargetClass = false:真身实现了接口就用 JDK 动态代理,没有接口才用 CGLIB。

Spring Boot 2.0 起

默认 spring.aop.proxy-target-class=true:有没有接口都用 CGLIB,省得注入时类型对不上。想改回来就把它设成 false。

一次调用怎么穿过替身

OrderController 调 orderService.placeOrder(o)。placeOrder 上有 @Transactional,还被一个 @Order(1) 的日志切面管着。选一个剧情,一步一步看调用走到了哪。

调用方OrderControllerorderService.placeOrder(o)
替身OrderService$$SpringCGLIB$$0DynamicAdvisedInterceptor
#0拦截器ExposeInvocationInterceptor框架自动加在最前面
#1拦截器LogAspect.around@Order(1)
#2拦截器TransactionInterceptor@Transactional
真身OrderServiceplaceOrder 的业务代码
ReflectiveMethodInvocation 的currentInterceptorIndex = —
调用经过(缩进表示嵌套在谁里面)
      1/9

      拦截器链:一个下标,一层层往里

      链是一个列表,ReflectiveMethodInvocation 手里攥着一个下标。每个拦截器在自己的代码中间调 proceed(),下标加一,交给下一个;走到头就调真身。

      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(),于是又回到这里
      }
      你写的切面@Around 的 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 里的通知每个通知方法变成一个 AdvisorAnnotationAwareAspectJAutoProxyCreator看切面类上的 @Order,没写就排最后
      @TransactionalTransactionInterceptor同上最低优先级,可用 @EnableTransactionManagement(order = …) 调
      @CacheableCacheInterceptor同上最低优先级,可用 @EnableCaching(order = …) 调
      @AsyncAsyncExecutionInterceptorAsyncAnnotationBeanPostProcessor,另一个改装师加到已有替身的拦截器链最前面
      @Validated 方法校验MethodValidationInterceptorMethodValidationPostProcessor加到已有替身的拦截器链末尾

      一个 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 替身类在构建期就生成好,打成原生程序也能用
      没变的:第 10 站换替身JDK 代理和 CGLIBBoot 默认用 CGLIBproceed() 一层层往里this 调用拦不到

      常踩的坑

      先问一句:这次调用,到底有没有经过替身?

      同一个类里调另一个带 @Transactional 的方法,事务没开

      this 是真身。把方法挪到另一个 Bean;或者注入自己(@Lazy 注入 self);或者 @EnableAspectJAutoProxy(exposeProxy = true) 后用 AopContext.currentProxy()。

      调 final 方法,里面注入的字段是 null

      CGLIB 重写不了它,代码直接在替身对象上跑,而替身没注入过字段。去掉 final。Kotlin 的类和方法默认是 final,要用 kotlin-spring 插件。

      Bean named '…' is expected to be of type 'OrderServiceImpl' but was actually of type 'jdk.proxy2.$Proxy75'

      用了 JDK 代理,却用实现类类型注入。改用接口类型注入,或者设 proxyTargetClass = true。

      切面写好了,一次都没进来

      切面类只写了 @Aspect,没写 @Component;或者切点表达式的包名写错了,一个方法都没匹配上。

      日志切面记的耗时里,没算上事务提交

      两个都是最低优先级时,谁在外层不确定。给切面加 @Order,或用 @EnableTransactionManagement(order = …) 把顺序定死。

      getClass().getAnnotation(…) 拿不到类上的注解

      getClass() 返回的是替身类。用 AopUtils.getTargetClass(bean) 或 AnnotationUtils.findAnnotation(…)。

      日志:is not eligible for getting processed by all BeanPostProcessors

      这个 Bean 被某个改装师提前用到了,造得太早,错过了做替身那一步,上面的切面不生效。改装师的依赖用 ObjectProvider 延迟获取。

      切面里把异常吞了,调用方以为成功了

      @Around 里 catch 住异常没再抛出去,外层的事务也以为成功,会照常提交。记完日志要原样抛出。