每日一个小知识 : Spring @Transactional 注解失效的“自调用” 问题及其底层原理。
·
1. 场景复现:代码看起来没问题,但事务没生效
假设你有一个 UserService 类,其中有两个方法:updateUser (普通方法) 和 updateUserPassword (事务方法)。
Java
@Service
public class UserService {
// 方法 A:没有事务注解
public void updateUser() {
// 做一些逻辑...
System.out.println("Updating user info...");
// 【关键点】:在类内部,普通方法调用了带事务注解的方法
this.updateUserPassword();
}
// 方法 B:标记了事务注解,期望抛出异常时回滚
@Transactional(rollbackFor = Exception.class)
public void updateUserPassword() {
System.out.println("Updating password...");
// 模拟数据库操作
// ...
// 抛出异常,期望事务回滚
throw new RuntimeException("DB Error");
}
}
问题: 当外部控制器调用 userService.updateUser() 时,updateUserPassword() 里的事务会生效吗?如果发生异常,数据库操作会回滚吗?
答案: 不会生效,也不会回滚。 这是一个典型的事务失效场景。
2. 核心原理:为什么会失效?
要理解这个问题,必须理解 Spring AOP (面向切面编程) 的实现机制。
Spring 的 @Transactional 是通过 动态代理 (Dynamic Proxy) 实现的。当你从 Spring 容器中注入 UserService 时,你拿到的实际上不是原生的 UserService 对象,而是一个 代理对象 (Proxy Object) 。
调用流程对比
-
外部调用 (正常生效):
- Controller 调用
userService.updateUserPassword()。 - 代理对象 拦截请求 -> 开启事务 -> 调用目标对象的方法 -> 捕获异常/提交事务。
- ✅ 事务生效。
- Controller 调用
-
内部自调用 (失效场景):
- Controller 调用
userService.updateUser()。 - 代理对象 拦截请求 ->
updateUser没有注解 -> 直接调用目标对象的updateUser。 - 关键点来了: 目标对象的
updateUser内部执行this.updateUserPassword()。 - 这里的
this指的是 目标对象本身 (Target Object) ,而不是代理对象。 - 因为代码直接在目标对象内部跳转,绕过了代理对象的拦截逻辑,所以事务切面根本没有执行。
- ❌ 事务失效。
- Controller 调用
3. 解决方案:如何修复?
针对这个问题,通常有三种解决方案,按推荐程度排序:
方案一:重构代码 (推荐 - 最干净)
将事务方法移动到另一个 Service 类中,然后通过注入的方式调用。
Java
@Service
public class UserPasswordService {
@Transactional
public void updatePassword() { ... }
}
@Service
public class UserService {
@Autowired
private UserPasswordService passwordService; // 注入另一个Service
public void updateUser() {
// 调用代理对象的方法,事务生效
passwordService.updatePassword();
}
}
方案二:自己注入自己 (简单粗暴)
在 Service 内部注入自己,通过注入的 Bean 调用方法(注入的 Bean 是代理对象)。
Java
@Service
public class UserService {
@Autowired
@Lazy // 防止循环依赖报错
private UserService self;
public void updateUser() {
// 使用注入的代理对象调用,而不是 this
self.updateUserPassword();
}
@Transactional
public void updateUserPassword() { ... }
}
方案三:使用 AopContext (不推荐,侵入性强)
显式获取当前的代理对象。注意需要在配置类开启 @EnableAspectJAutoProxy(exposeProxy = true)。
Java
public void updateUser() {
// 强制获取当前代理对象
((UserService) AopContext.currentProxy()).updateUserPassword();
}
4. 延伸:其他常见的 @Transactional 失效场景
为了让你更全面地掌握这个知识点,这里总结了其他几个常见的“坑”:
| 场景 | 原因 |
|---|---|
| 方法修饰符不是 public | Spring 默认只代理 public 方法。如果是 private 或 protected,事务会被忽略。 |
| 异常被 try-catch 吃掉了 | 如果你在方法内部 catch 了异常且没有抛出,代理捕获不到异常,认为是成功,会提交事务。 |
| 异常类型不匹配 | 默认只回滚 RuntimeException 和 Error。如果抛出的是 Checked Exception (如 IOException) 且没配置 rollbackFor,不会回滚。 |
| 数据库引擎不支持 | 例如 MySQL 使用了 MyISAM 引擎(不支持事务),Spring 再努力也没用。 |
总结
记住一句话:Spring 的事务是基于代理的“外壳”机制。如果在对象内部“走后门”直接调用 (this.xxx),外壳的逻辑(事务、AOP日志等)统统不会触发。
更多推荐



所有评论(0)