Spring AOP 事务失效与底层代理机制深度总结

img

1. 核心问题:为什么自调用导致事务失效?

现象

在同一个类中,方法 A 调用方法 B(this.methodB()),即使方法 B 上加了 @Transactional 注解,事务依然不生效。

根本原因

Spring AOP 的默认实现基于 代理模式 (Proxy Pattern)。Spring 容器中注入的 Bean 实际上是一个 代理对象 (Proxy),它持有你编写的 目标对象 (Target)

  • 生效路径:外部调用 -> Proxy -> 事务拦截器 (Interceptor) -> Target。
  • 失效路径:Target 内部调用 -> this.methodB() -> Target。

此时,this 指针指向的是目标对象本身。代码执行流直接在目标对象内部流转,完全绕过了外层的代理对象及其持有的事务拦截器。

形象比喻

  • Proxy (代理):大楼门口的安检员
  • Target (目标):大楼里的员工
  • 外部访问:访客找员工,必须经过安检(事务生效)。
  • 内部自调用:员工之间互找,直接在楼里见面,不需要跑出去再过一次安检(事务失效)。

2. 解决方案 (按架构师推荐度排序)

方案 具体做法 评价 适用场景
1. 架构重构 (Refactoring) 将需要事务的方法提取到独立的 Service 中,通过 Bean 注入调用。 ⭐⭐⭐⭐⭐ (强烈推荐) 符合单一职责原则 (SRP),代码结构最清晰。
2. 自我注入 (Self-Injection) 在类中注入自身 (@Lazy 解决循环依赖),调用 self.methodB() ⭐⭐⭐ (可用) 业务逻辑紧密耦合,不适合拆分文件时。
3. AopContext 使用 AopContext.currentProxy() 强转获取当前代理对象。 ⭐ (不推荐) 代码侵入性强,与 Spring API 强耦合。
4. AspectJ 放弃代理模式,使用字节码织入 (Weaving)。 N/A (特殊需求) 追求极致性能或必须在 private/自调用中生效时。

3. 深度辨析:JDK 动态代理 vs CGLIB

SpringAOP

常见误区

误区:”JDK Proxy 会导致自调用失效,换成 CGLIB 就能解决了。”

真相错误的。 无论是 JDK Proxy 还是 CGLIB,只要是基于代理模式,自调用都会失效。因为它们底层架构都是 Proxy 持有 Target

技术对比表

维度 JDK 动态代理 CGLIB (Code Generation Library)
实现机制 基于 接口 (Interface) 基于 继承 (Subclass)
类关系 代理类与目标类是 兄弟关系 代理类是目标类的 子类
核心限制 目标类 必须实现接口 目标类或方法 不能是 Final
调用方式 反射 (Method.invoke) FastClass 索引 (直接调用)
自调用支持 ❌ 不支持 ❌ 不支持
Spring 默认 旧版本 (有接口时默认) Spring Boot 2.0+ 默认

4. 性能黑科技:CGLIB 的 FastClass 机制

CGLIB 在运行时之所以高效,是因为它通过 FastClass 机制规避了 Java 反射 API 的开销。

FastClass 原理

CGLIB 在生成代理类时,会利用 ASM 字节码技术额外生成一个 FastClass。它相当于把”反射查找”变成了”硬编码的索引跳转”。

伪代码逻辑:

Java

1
2
3
4
5
6
7
8
9
10
// FastClass 就像一个巨大的 switch-case,建立了 方法签名 -> 索引 的映射
public Object invoke(int index, Object target, Object[] args) {
MyService service = (MyService) target;
// 直接调用,没有反射的 invoke() 开销
switch (index) {
case 1: return service.login();
case 2: return service.logout();
}
return null;
}

总结 FastClass

  • 本质空间换时间。通过生成更多的字节码类,建立索引。
  • 优势运行时 (Runtime) 调用速度极快,接近原生 Java 方法调用,不仅省去了反射的安全检查,还能享受 JVM 的内联优化。
  • 劣势启动时间稍慢(需要生成和加载字节码)。

5. 架构师建议 (“The Spring Way”)

  1. 首选重构:遇到自调用失效,首先反思类的职责是否过重。将事务逻辑拆分到不同 Service 是最优雅的解法。
  2. 拥抱 CGLIB:在 Spring Boot 2.x/3.x 时代,默认使用 CGLIB 是最佳实践(稳定、高效、无需接口),除非你有特殊的 JDK 原生洁癖。
  3. 避坑 Final:使用 Spring 管理的 Bean(尤其是涉及 AOP/事务的),严禁将类或方法设为 final,否则 CGLIB 无法生成子类代理,会导致启动报错或 AOP 失效。
  4. 理解原理:不要盲目背诵“CGLIB 比 JDK 快”,要理解它是通过“启动时的复杂”换取了“运行时的简单”。