事故现场:明明加了 @Transactional
订单方法 saveOrder() 内部要同步写审计日志,于是调用了本类的 audit() 方法——audit 上标注了 @Transactional。上线后奇怪的事发生了:audit 抛异常,订单照存,日志表还多了半条脏数据。配置没问题、注解没拼错、依赖都在。真相:类内部的 this 调用没走代理,@Transactional 形同虚设。这是 Spring 世界每天都在发生的事故,也是本系列开篇的最佳标本——它暴露了一个根本认知:在 Spring 里,你拿到的从来不是你写的那个对象。
第一原理:容器里住的是代理
你写的类是一张白纸;交给 Spring 之后,容器返回给你的可能是它的增强版:加事务、加日志、加缓存——全靠 AOP 动态代理在方法前后织入逻辑。增强要生效,调用必须经过代理对象;而类内部用 this 调用,等于绕过代理直捣本体,织入的逻辑全部失联。这不是 bug,是动态代理的工作原理:代理只能拦截「穿过它」的调用。记住这句话,本系列一半的事故分析都有解了。
代码复现
@Service
public class OrderService {
public void saveOrder() {
// this.audit():this 是原始对象,不经过代理 → 事务失效
audit();
}
@Transactional
public void audit() {
System.out.println("当前是否存在事务:"
+ TransactionSynchronizationManager.isActualTransactionActive());
}
}外部直接调 audit()(比如 Controller 里 orderService.audit()),输出 true;saveOrder() 里 this 调用,输出 false——同一行代码,两种命运,只差「调用是否经过代理」。修法三条:注入自身代理再调(@Lazy 自注入)、把 audit 拆到另一个类、或开启 exposeProxy 后用 AopContext.currentProxy()。细节留给事务失效篇(第 9 篇)展开,今天先立认知。
这个系列的路线图
十八篇分五段:IoC 与 Bean 生命周期(九步旅程、三级缓存与循环依赖、BeanPostProcessor、FactoryBean);AOP 双篇(JDK 动态代理与 CGLIB、切面织入);事务三篇(实现原理、失效八场景、传播行为七式);注入与事件(@Autowired 与 @Resource、循环依赖进阶、事件机制);启动与装配(Spring Boot 启动流程、自动装配、自定义 Starter、常见扩展点)。收官放一张 Spring 作战地图。
准备工作
JDK 17 + Spring Boot 3.x,每个知识点配最小可跑 Demo。两个好用的观察工具:TransactionSynchronizationManager.isActualTransactionActive() 判断当前线程事务是否激活,比翻日志直观;AopUtils.isAopProxy(bean) 一行代码看 Bean 是真身还是代理。
下一篇从容器的心脏讲起:getBean 的背后,一个 Bean 从实例化到销毁的完整旅程——九步流水线,步步有角色。
评论 (0)