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)

调用流程对比
  1. 外部调用 (正常生效):

    • Controller 调用 userService.updateUserPassword()
    • 代理对象 拦截请求 -> 开启事务 -> 调用目标对象的方法 -> 捕获异常/提交事务。
    • 事务生效
  2. 内部自调用 (失效场景):

    • Controller 调用 userService.updateUser()
    • 代理对象 拦截请求 -> updateUser 没有注解 -> 直接调用目标对象的 updateUser
    • 关键点来了: 目标对象的 updateUser 内部执行 this.updateUserPassword()
    • 这里的 this 指的是 目标对象本身 (Target Object) ,而不是代理对象。
    • 因为代码直接在目标对象内部跳转,绕过了代理对象的拦截逻辑,所以事务切面根本没有执行。
    • 事务失效

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 方法。如果是 privateprotected,事务会被忽略。
异常被 try-catch 吃掉了 如果你在方法内部 catch 了异常且没有抛出,代理捕获不到异常,认为是成功,会提交事务。
异常类型不匹配 默认只回滚 RuntimeExceptionError。如果抛出的是 Checked Exception (如 IOException) 且没配置 rollbackFor,不会回滚。
数据库引擎不支持 例如 MySQL 使用了 MyISAM 引擎(不支持事务),Spring 再努力也没用。

总结

记住一句话:Spring 的事务是基于代理的“外壳”机制。如果在对象内部“走后门”直接调用 (this.xxx),外壳的逻辑(事务、AOP日志等)统统不会触发。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐