Spring Boot 默认
proxyTargetClass=true(CGLIB 代理)。事务类被final修饰或事务方法被final修饰,CGLIB 都无法生成代理——类 final 时启动直接抛AopConfigException,方法 final 时代理能建起来但注解被静默绕过,比硬报错更隐蔽。
Issue & Env(案发现场与环境)
- 框架:Spring Boot 2.0+(
spring.aop.proxy-target-class默认为true,所有@TransactionalBean 走 CGLIB 代理) - 场景:事务服务类或事务方法被
final修饰 - 触发条件:CGLIB 代理的原理是“继承目标类生成子类,override 业务方法插入拦截器”,这决定了两条硬限制——final 类无法被继承,子类都生成不出来;final 方法无法被 override,代理子类里插不进拦截器,调用直接落到原始方法。
StackTrace(堆栈日志)
形态 1:类 final → 启动硬报错,容易发现。
org.springframework.aop.framework.AopConfigException:
Could not generate CGLIB subclass of class com.example.OrderService:
Common causes of this problem include using a final class or a non-visible class
at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:251)
形态 2:方法 final → 事务静默失效,没有任何报错。开 DEBUG 日志后,正常事务应打出 Creating new transaction,这里一句都没有——和自调用失效的表现一致。
Reproduce(复现代码)
@Service
public class OrderService {
@Transactional
public final void createOrder(Order o) { orderMapper.insert(o); }
}
启动正常、调用正常返回,但事务从来没被开启。代码层面打印验证:
System.out.println(AopUtils.isAopProxy(this)); // true,类确实被代理了
System.out.println(AopUtils.isCglibProxy(this)); // true
// 但 final 方法调用时事务日志一句没有,说明拦截器没织入这个方法
注意区分:isAopProxy 返回 true 只说明类被代理了,不代表每个方法都被织入。final 方法的失效就藏在这个缝隙里。
Root Cause(根因分析)
根因在 CGLIB 的字节码机制:代理子类通过 Enhancer 生成,override 目标方法插入 MethodInterceptor。Java 语言层面 final 方法禁止 override,Enhancer 只能跳过该方法,代理子类里这个方法直接调用 super.xxx()——拦截器没机会执行。
快速定位:
# 看事务方法所在类或方法有没有 final
grep -rn "final.*void.*(\|public final class" src/main/java --include="*Service*"
- 启动抛
Could not generate CGLIB subclass→ 查类的 final 修饰。 - 无报错但 DEBUG 下没有
Creating new transaction日志 → 查方法的 final 修饰,同时排查自调用(两者现象一致)。
The Fix(解决方案)
方案 A(推荐):去掉 final 修饰
只对需要被代理的类/方法去 final;final 字段不受影响,不必动:
@Service
public class OrderService {
@Transactional
public void createOrder(Order o) { orderMapper.insert(o); } // 去掉 final
}
方案 B:走 JDK 动态代理
类实现了接口时,可以 @Transactional(proxyTargetClass = false) 或 @EnableTransactionManagement(proxyTargetClass = false) 切换到 JDK 动态代理——它基于接口实现,不依赖子类化。但 Boot 默认已统一 CGLIB,混用两种代理模式容易引入新问题,非必要不推荐。
方案 C:对确实要 final 的场景,用编程式事务绕开
不可去 final 的类(例如被三方约束),方法内用 TransactionTemplate 手工圈事务边界,绕开注解代理:
transactionTemplate.executeWithoutResult(status -> orderMapper.insert(o));
团队规范里应禁止给 Spring Bean 的业务方法加 final,review 阶段就能拦住;编程式事务是绕开代理限制的通用兜底。
