Spring Boot · 第 04 章

AOP 与代理:最重要的一种"加工实例"

第 1 章说 BeanPostProcessor 会"加工实例"——这一章揭穿其中最重要的一种加工:把你的 Bean 换成代理。

本章你将建立的 schema

  • AOP 代理 = BeanPostProcessor 在 init 后包一层,是第 1 章 BPP 轨道的具体实例
  • JDK 动态代理 vs CGLIB:Boot 默认 spring.aop.proxy-target-class=true → CGLIB
  • @Transactional / @Async / @Cacheable 共用同一套代理机制;self-invocation 根因是 this 绕过代理

4.1代理是谁创建的

Spring AOP 代理由 AbstractAutoProxyCreator 这个 BeanPostProcessor 在 Bean 初始化完成后创建,并用代理对象替换容器里的原始 Bean。

为什么需要它

事务、缓存、异步等横切关注点如果散落在每个业务类里,代码会被反复污染。代理把这些逻辑集中在 Advice 里,业务类对此无感知——前提是调用路径必须经过代理对象。

AbstractAutoProxyCreator 实现了 SmartInstantiationAwareBeanPostProcessor(BPP 的子接口),在 第 1 章 描述的 BPP 轨道中属于 postProcessAfterInitialization 阶段——即 Bean 完成属性注入、Aware 回调、@PostConstruct 初始化之后。调用链为:

AbstractAutoProxyCreator.java(示意)Java
// postProcessAfterInitialization 是 BPP 的钩子
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    return wrapIfNecessary(bean, beanName, ...);  // 判断是否需要代理
}

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 找到适用于此 bean 的所有 Advisor(Pointcut + Advice)
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(...);
    if (specificInterceptors != DO_NOT_PROXY) {
        return createProxy(...);  // 包成代理,返回给容器
    }
    return bean;  // 无需代理,原样返回
}

容器随后把这个代理对象存入单例池,后续所有依赖注入拿到的都是代理,而非原始 Bean。这是"@Transactional 的魔法"——本质上是第 1 章 BPP 加工实例的一个具体实例,没有任何额外机制。

调用方 (其他 Bean) 代理对象 Advice 拦截链 (Before / Around / After) 真实 Bean (原始实例) 注入的是代理 执行目标方法 容器单例池存放的是代理对象,不是原始 Bean
图 4.1调用方拿到并调用的是代理对象,Advice 拦截链在代理层执行后才委托给真实 Bean。注意:容器单例池里保存的是代理,原始 Bean 对调用方不可见。

4.2JDK 动态代理 vs CGLIB

DefaultAopProxyFactory.createAopProxy() 的决策逻辑如下:

  • 若 proxyTargetClass=true,或目标类没有用户接口(实现的接口只有 Spring 内部接口不算)→ 选 CGLIB(运行时用 ObjenesisCglibAopProxy 生成子类)。
  • 否则 → 选 JDK 动态代理(JdkDynamicAopProxy,实现目标类的接口)。

Spring Boot 的 AopAutoConfiguration 把 spring.aop.proxy-target-class 默认设为 true,因此 Boot 默认选 CGLIB——这与裸 Spring(偏好 JDK 动态代理)不同。这就是"为什么我的 Bean 实例是个 CGLIB 子类"的答案。

application.properties(若需切换为 JDK 代理)Properties
# Boot 默认 true(CGLIB);设 false 则在有接口时回退 JDK 动态代理
spring.aop.proxy-target-class=false
JDK 动态代理 vs CGLIB 对比
维度 JDK 动态代理 CGLIB
代理方式 实现目标类的接口 运行时生成目标类的子类
能否代理无接口类 否(必须有用户接口) 是
final 方法 不受影响(接口方法不能 final) 无法覆盖,代理对 final 方法失效
final 类 不受影响 无法继承,代理创建失败
Boot 4.0 默认 否 是(proxy-target-class=true)
依赖 JDK 标准库 Byte Buddy(Spring 5+ 替换旧 cglib)
想一想

一个类实现了 UserService 接口,Boot 默认情况下注入点拿到的是 JDK 代理还是 CGLIB 代理?

展开答案(先停 10 秒)

CGLIB 代理。Boot 的 AopAutoConfiguration 默认 spring.aop.proxy-target-class=true,无论有无接口一律选 CGLIB。若需要 JDK 动态代理,须显式设 spring.aop.proxy-target-class=false,且目标类必须有用户接口。

4.3@Transactional / @Async / @Cacheable 都是代理

三个注解背后是同一套机制——对应 BPP 分别由不同的 AbstractAutoProxyCreator 子类或 Advisor 注册,代理拦截到方法调用后织入各自的 Advice:

  • @Transactional:TransactionInterceptor 在方法进入前 begin 事务,正常返回后 commit,抛出指定异常后 rollback。依赖 @EnableTransactionManagement(Boot 的 TransactionAutoConfiguration 自动开启)。
  • @Async:AsyncExecutionInterceptor 把方法体提交到指定线程池并立即返回(返回 Future / CompletableFuture)。依赖 @EnableAsync。
  • @Cacheable:CacheInterceptor 在方法调用前查缓存,命中则直接返回;未命中则执行方法并写入缓存。依赖 @EnableCaching。
OrderService.javaJava
@Service
public class OrderService {

    private final OrderRepository repo;

    public OrderService(OrderRepository repo) {
        this.repo = repo;
    }

    // 容器注入给调用方的是 CGLIB 代理;
    // 代理在进方法前 begin 事务,正常退出 commit,RuntimeException rollback
    @Transactional
    public void placeOrder(Order order) {
        repo.save(order);
        // 若此处抛出 RuntimeException,代理层触发 rollback
        chargePayment(order);
    }

    // @Cacheable 也走代理——同样要避免 self-invocation(见 4.4 节)
    @Cacheable("orders")
    public Order findById(Long id) {
        return repo.findById(id).orElseThrow();
    }
}

三者可以叠加在同一方法上:代理链的执行顺序由各 Advisor 的 order 决定,事务通常在最外层(先 begin,最后 commit/rollback),缓存在其内侧。

4.4self-invocation 根因与失效场景

this.method() 直接落在原始 Bean 上,代理不在调用栈里,Advice 永远无法触发。

容器把代理注入给调用方,但 Bean 内部的 this 引用始终指向原始对象——代理套在外层,内部的 this 调用绕过了它。这不是 Bug,是代理模式的固有特性:代理只拦截从外部(经由注入引用)发起的调用。

外部调用(Advice 生效) self-invocation(Advice 跳过) 调用方 代理 Advice 触发 ✓ @Transactional 生效 Bean Bean.a()(无注解) 调用 this.b() Bean.b() @Transactional this.b() 调用方 代理存在 但 this 绕过它 Advice 不触发,@Transactional 失效 代理不在 this.b() 的调用栈中
图 4.2左:外部调用经代理,Advice 链完整执行。右:a() 通过 this.b() 调用,代理被绕过,Advice 不触发。注意:代理是"套在外层的壳",this 永远指向内层原始对象。
实例化 (构造器) 属性注入 @Autowired BPP.before + @PostConstruct init 回调 BPP.after postProcessAfter- Initialization → wrapIfNecessary 代理对象进入单例池 ← 第 1 章 Bean 生命周期 BPP 轨道
图 4.3AbstractAutoProxyCreator 挂在 postProcessAfterInitialization 阶段——Bean 初始化完毕后才包代理,此时依赖注入已完成。注意:代理创建必须在 init 之后、进入单例池之前。

失效场景汇总

代理失效场景 → 根因 → 修复
场景 根因 修复
self-invocation:this.txMethod() this 指向原始对象,代理不在调用栈 拆到另一个 Bean;或注入自身代理 @Autowired OrderService self;或 AspectJ 编译期织入
private 方法加 @Transactional CGLIB 子类不能覆盖 private 方法,代理无法拦截 改为 public(或 protected)
final 方法加 @Transactional CGLIB 子类不能覆盖 final 方法 去掉 final
@Async 不生效 缺少 @EnableAsync;或自调用;或返回类型不是 void/Future/CompletableFuture 确认 @EnableAsync 已加;排查是否自调用;检查返回类型
@Cacheable 缓存不生效 缺 @EnableCaching;或自调用;或未配置 CacheManager 确认 @EnableCaching;排查自调用;检查 CacheManager Bean
陷阱

自注入代理(@Autowired OrderService self)是解决 self-invocation 的最小改动,但会在容器启动时触发循环依赖警告(Spring Boot 2.6+ 默认禁止循环引用)。更稳健的方案是把 b() 拆到独立的 Bean 里——这同时改善了单一职责,是推荐的结构性修法。

§本章 self-check

先合上教程,把答案写下来再展开对照。

  1. Boot 默认用 JDK 动态代理还是 CGLIB?由哪个配置项控制、谁设置了它的默认值?
  2. 为什么 this.method() 自调用会让 @Transactional 失效?要答到"代理不在调用栈"这一层。
  3. @Async 不生效有哪几种根因?如何逐一排查?
答案(先做完再展开)
  1. CGLIB。spring.aop.proxy-target-class 控制,Boot 的 AopAutoConfiguration 把它默认设为 true。裸 Spring 没有这个默认值,偏好 JDK 动态代理(有接口时)。
  2. 容器注入给调用方的是代理对象,但 Bean 内部的 this 始终指向原始对象。this.txMethod() 直接落在原始对象上,代理不在此调用栈里,TransactionInterceptor 无从触发,事务不开启。
  3. (a)缺 @EnableAsync(或 Boot 的自动配置未触发)——代理根本没有注册;(b)self-invocation:this.asyncMethod() 绕过代理;(c)方法是 private 或 final,CGLIB 无法覆盖;(d)返回类型错误(必须是 void 或 Future 系列,否则异步语义不成立)。排查顺序:先确认 @EnableAsync,再检查是否自调用,再检查方法修饰符,最后看返回类型。
进阶挑战 · 刚好够不着

a() 内部调用 @Transactional 的 b()——不改调用方,让事务生效

同一个类有 a()(无注解)和 b()(@Transactional)。外部调用 a(),a() 内部调 this.b()。不修改任何调用方代码,只改这一个类,让 b() 的事务在 a() 内调用时也完整生效。给出至少两种方案并说明各自的代价。

提示(卡住再展开)

核心约束是:必须让 b() 的调用路径经过代理。三条思路:① 把 b() 移到另一个 Bean,a() 注入该 Bean 并调用——调用方不变,但类结构变了;② 在当前类注入自身代理 @Autowired OrderService self,用 self.b() 代替 this.b()——改动最小,但引入"循环自注入",Boot 2.6+ 默认报错需额外开关;③ 改用 AspectJ 编译期/加载期织入,不走代理模式,this.b() 也能织入——代价是构建复杂度大幅上升。思考:哪种方案在可维护性和侵入性上最平衡?