基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

如果一个类中的方法A(没有@Transactional)调用了方法B(有@Transactional),事务会生效吗?

知识点图片

类内无事务方法调用有事务方法,事务会失效。因为this调用会绕过Spring AOP代理,导致@Transactional注解无法被拦截。必须通过注入代理或拆分Service等方式,确保调用经过代理对象。

这是一个非常经典且常见的 Spring 事务问题。

直接回答:通常情况下,事务不会生效。

下面我将详细解释为什么,以及如何解决这个问题。

为什么事务会失效?

这个问题的核心在于 Spring A-OP(面向切面编程)的代理(Proxy)机制。Spring 的声明式事务(@Transactional)是通过 AOP 实现的。

  1. 代理的工作原理

    • 当 Spring 容器启动时,它会扫描带有 @Transactional 注解的 bean。
    • 对于这样的 bean,Spring 不会直接返回原始的对象实例,而是创建一个该对象的代理对象
    • 当外部代码调用这个 bean 的方法时,实际上是调用了这个代理对象的方法。
  2. 代理的职责

    • 代理对象就像一个“门卫”。在调用实际的目标方法(你的业务代码)之前,它会检查方法上是否有 @Transactional 注解。
    • 如果发现有,代理会开启一个事务。
    • 然后,代理再调用原始对象的真正方法。
    • 方法执行完毕后,代理会根据执行结果(是否抛出异常)来提交或回滚事务。
  3. 问题所在:内部方法调用(Self-invocation)

    • 在你的场景中,外部调用的是 方法A
    • 因为 方法A 上没有 @Transactional 注解,所以代理对象(门卫)检查后发现不需要开启事务,于是就直接调用了原始对象的 方法A
    • 当代码执行到 方法A 内部,调用 this.方法B() 时,这个调用是在原始对象内部发生的。它使用的是 this 引用,直接指向了原始对象自身,完全绕过了代理对象
    • 因为没有经过代理对象,所以“门卫”根本没有机会检查 方法B 上的 @Transactional 注解,自然也就不会为 方法B 开启事务。

简单来说: 事务的控制权在代理对象手上,而内部方法调用(this.method())绕过了代理,直接在原始对象内执行,导致代理的功能失效。

图解说明

plaintext
外部调用 ---> [MyService 代理对象] ---(调用方法 A)---> [MyService 原始对象]
                 |                                      |
                 | 检查方法 A,无 @Transactional         |
                 | 不开启事务                             |
                 +-------------------------------------> 执行方法 A 的代码
                                                        |
                                                        |  this.方法B()
                                                        |  (绕过了代理!)
                                                        +------> 执行方法 B 的代码
                                                                 (在没有事务的环境下运行)

如何解决这个问题?

有以下几种常见的解决方案,推荐顺序从上到下:

方案一:将事务方法移到另一个 Service 中(最佳实践)

这是最推荐、设计上也最清晰的方法。它遵循了单一职责原则。

  1. 创建一个新的 Service,例如 ServiceB
  2. 方法B 移动到 ServiceB 中。
  3. 在原来的 Service(我们称之为 ServiceA)中注入 ServiceB,然后调用它。
java
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    public void methodA() {
        // ... 其他业务逻辑
        serviceB.methodB(); // 调用另一个Bean的方法,会经过代理
    }
}

@Service
public class ServiceB {
    @Transactional
    public void methodB() {
        // 数据库操作,现在会处于事务中
    }
}

优点:代码结构清晰,职责分明,完全符合 Spring AOP 的工作模式。

方案二:依赖注入自己(Self-injection)

在同一个类中注入自身的代理对象,然后通过代理对象来调用事务方法。

java
@Service
public class MyService {
    // 注入自身的代理对象
    @Autowired
    private MyService self;

    public void methodA() {
        // ... 其他业务逻辑
        // 通过代理对象调用方法B,而不是 this
        self.methodB();
    }

    @Transactional
    public void methodB() {
        // 数据库操作
    }
}

注意:这种方式可能会导致循环依赖问题。在较新的 Spring Boot 版本中,循环依赖默认是禁止的。你可能需要添加配置 spring.main.allow-circular-references=true 或者使用 @Lazy 注解来解决。

优点:简单快捷,不需要创建新类。
缺点:代码看起来有些奇怪,且可能引入循环依赖问题。

方案三:使用 AopContext.currentProxy()

通过 AopContext 手动获取当前的代理对象。

java
@Service
public class MyService {

    public void methodA() {
        // ... 其他业务逻辑
        // 获取当前代理对象并调用方法B
        ((MyService) AopContext.currentProxy()).methodB();
    }

    @Transactional
    public void methodB() {
        // 数据库操作
    }
}

注意:要使用此方法,必须在你的启动类或配置类上添加 @EnableAspectJAutoProxy(exposeProxy = true)

优点:不需要额外注入字段。
缺点:与 Spring AOP 框架高度耦合,代码可读性稍差。

方案四:将 @Transactional 注解移到 方法A 上(最简单,但需考虑业务场景)

如果 方法A 的所有逻辑都应该包含在一个事务中,那么最简单的办法就是直接把注解加在入口方法上。

java
@Service
public class MyService {

    @Transactional // 将事务注解加在入口方法上
    public void methodA() {
        // ... 其他业务逻辑
        this.methodB(); // 此时方法B会加入到方法A已经开启的事务中
    }

    // 方法B可以有也可以没有 @Transactional 注解
    // 如果有,其传播行为会决定它是加入现有事务还是开启新事务
    public void methodB() {
        // 数据库操作
    }
}

优点:最简单直接的修改。
缺点:扩大了事务的范围,可能会影响性能或不符合业务逻辑要求(比如 方法A 中有些操作不希望被回滚)。

总结

解决方案 优点 缺点 推荐度
拆分到新Service 设计清晰,符合单一职责,无副作用 需要创建新类,稍微繁琐 ⭐⭐⭐⭐⭐
依赖注入自己 简单快捷,代码在同一类中 可能导致循环依赖,代码略显怪异 ⭐⭐⭐⭐
使用 AopContext 无需额外注入,逻辑明确 与Spring AOP框架耦合 ⭐⭐⭐
注解移到调用方法 最简单 改变了事务边界,不一定符合业务 ⭐⭐
00:00
00:00