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 初始化之后。调用链为:
// 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 加工实例的一个具体实例,没有任何额外机制。
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 子类"的答案。
# Boot 默认 true(CGLIB);设 false 则在有接口时回退 JDK 动态代理
spring.aop.proxy-target-class=false
| 维度 | 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。
@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,是代理模式的固有特性:代理只拦截从外部(经由注入引用)发起的调用。
a() 通过 this.b() 调用,代理被绕过,Advice 不触发。注意:代理是"套在外层的壳",this 永远指向内层原始对象。AbstractAutoProxyCreator 挂在 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
先合上教程,把答案写下来再展开对照。
- Boot 默认用 JDK 动态代理还是 CGLIB?由哪个配置项控制、谁设置了它的默认值?
- 为什么
this.method()自调用会让@Transactional失效?要答到"代理不在调用栈"这一层。 @Async不生效有哪几种根因?如何逐一排查?
答案(先做完再展开)
- CGLIB。
spring.aop.proxy-target-class控制,Boot 的AopAutoConfiguration把它默认设为true。裸 Spring 没有这个默认值,偏好 JDK 动态代理(有接口时)。 - 容器注入给调用方的是代理对象,但 Bean 内部的
this始终指向原始对象。this.txMethod()直接落在原始对象上,代理不在此调用栈里,TransactionInterceptor无从触发,事务不开启。 - (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() 也能织入——代价是构建复杂度大幅上升。思考:哪种方案在可维护性和侵入性上最平衡?