如果一个类中的方法A(没有@Transactional)调用了方法B(有@Transactional),事务会生效吗?
类内无事务方法调用有事务方法,事务会失效。因为
this调用会绕过Spring AOP代理,导致@Transactional注解无法被拦截。必须通过注入代理或拆分Service等方式,确保调用经过代理对象。
这是一个非常经典且常见的 Spring 事务问题。
直接回答:通常情况下,事务不会生效。
下面我将详细解释为什么,以及如何解决这个问题。
为什么事务会失效?
这个问题的核心在于 Spring A-OP(面向切面编程)的代理(Proxy)机制。Spring 的声明式事务(@Transactional)是通过 AOP 实现的。
代理的工作原理:
- 当 Spring 容器启动时,它会扫描带有
@Transactional注解的 bean。 - 对于这样的 bean,Spring 不会直接返回原始的对象实例,而是创建一个该对象的代理对象。
- 当外部代码调用这个 bean 的方法时,实际上是调用了这个代理对象的方法。
- 当 Spring 容器启动时,它会扫描带有
代理的职责:
- 代理对象就像一个“门卫”。在调用实际的目标方法(你的业务代码)之前,它会检查方法上是否有
@Transactional注解。 - 如果发现有,代理会开启一个事务。
- 然后,代理再调用原始对象的真正方法。
- 方法执行完毕后,代理会根据执行结果(是否抛出异常)来提交或回滚事务。
- 代理对象就像一个“门卫”。在调用实际的目标方法(你的业务代码)之前,它会检查方法上是否有
问题所在:内部方法调用(Self-invocation)
- 在你的场景中,外部调用的是
方法A。 - 因为
方法A上没有@Transactional注解,所以代理对象(门卫)检查后发现不需要开启事务,于是就直接调用了原始对象的方法A。 - 当代码执行到
方法A内部,调用this.方法B()时,这个调用是在原始对象内部发生的。它使用的是this引用,直接指向了原始对象自身,完全绕过了代理对象。 - 因为没有经过代理对象,所以“门卫”根本没有机会检查
方法B上的@Transactional注解,自然也就不会为方法B开启事务。
- 在你的场景中,外部调用的是
简单来说: 事务的控制权在代理对象手上,而内部方法调用(this.method())绕过了代理,直接在原始对象内执行,导致代理的功能失效。
图解说明
外部调用 ---> [MyService 代理对象] ---(调用方法 A)---> [MyService 原始对象]
| |
| 检查方法 A,无 @Transactional |
| 不开启事务 |
+-------------------------------------> 执行方法 A 的代码
|
| this.方法B()
| (绕过了代理!)
+------> 执行方法 B 的代码
(在没有事务的环境下运行)
如何解决这个问题?
有以下几种常见的解决方案,推荐顺序从上到下:
方案一:将事务方法移到另一个 Service 中(最佳实践)
这是最推荐、设计上也最清晰的方法。它遵循了单一职责原则。
- 创建一个新的 Service,例如
ServiceB。 - 将
方法B移动到ServiceB中。 - 在原来的 Service(我们称之为
ServiceA)中注入ServiceB,然后调用它。
@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)
在同一个类中注入自身的代理对象,然后通过代理对象来调用事务方法。
@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 手动获取当前的代理对象。
@Service
public class MyService {
public void methodA() {
// ... 其他业务逻辑
// 获取当前代理对象并调用方法B
((MyService) AopContext.currentProxy()).methodB();
}
@Transactional
public void methodB() {
// 数据库操作
}
}
注意:要使用此方法,必须在你的启动类或配置类上添加 @EnableAspectJAutoProxy(exposeProxy = true)。
优点:不需要额外注入字段。
缺点:与 Spring AOP 框架高度耦合,代码可读性稍差。
方案四:将 @Transactional 注解移到 方法A 上(最简单,但需考虑业务场景)
如果 方法A 的所有逻辑都应该包含在一个事务中,那么最简单的办法就是直接把注解加在入口方法上。
@Service
public class MyService {
@Transactional // 将事务注解加在入口方法上
public void methodA() {
// ... 其他业务逻辑
this.methodB(); // 此时方法B会加入到方法A已经开启的事务中
}
// 方法B可以有也可以没有 @Transactional 注解
// 如果有,其传播行为会决定它是加入现有事务还是开启新事务
public void methodB() {
// 数据库操作
}
}
优点:最简单直接的修改。
缺点:扩大了事务的范围,可能会影响性能或不符合业务逻辑要求(比如 方法A 中有些操作不希望被回滚)。
总结
| 解决方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 拆分到新Service | 设计清晰,符合单一职责,无副作用 | 需要创建新类,稍微繁琐 | ⭐⭐⭐⭐⭐ |
| 依赖注入自己 | 简单快捷,代码在同一类中 | 可能导致循环依赖,代码略显怪异 | ⭐⭐⭐⭐ |
| 使用 AopContext | 无需额外注入,逻辑明确 | 与Spring AOP框架耦合 | ⭐⭐⭐ |
| 注解移到调用方法 | 最简单 | 改变了事务边界,不一定符合业务 | ⭐⭐ |