解决方法的核心在于理解Spring事务管理的实现原理——代理模式(Proxy)。
首先拆解一下这个问题和解决方案。
1. 什么是“自调用”?
假设你有一个 UserService类:
@Service
public class UserService {
public void a() {
// ... 一些业务逻辑
this.b(); // 自调用:在同一个类中,通过`this`调用方法b()
// ... 其他逻辑
}
@Transactional // 声明了事务
public void b() {
// ... 需要对数据库进行操作,期望在事务中执行
userRepository.save(...);
}
}当你从外部调用 userService.a()时,你期望方法 b()中的数据库操作在一个事务中运行,但实际上它没有。事务注解 @Transactional失效了。
2. 为什么绕过了代理?
我们要先知道Spring是如何实现事务管理的:
Spring使用代理(Proxy):当你给一个Bean的方法加上
@Transactional注解后,Spring在启动时会为这个Bean创建一个代理对象(可以理解为这个Bean的“管家”或“替身”),并把这个代理对象放入Spring容器中。外部调用走代理:当其他组件(如Controller)通过
@Autowired注入UserService并调用其方法时,它实际上拿到的是那个代理对象,而不是原始的UserService对象。你调用
proxy.a()代理先做事务管理(如开启事务)
代理再去调用原始对象的
a()方法最后代理提交或回滚事务
自调用不走代理:但在上面的例子中,你在方法
a()内部使用的是this.b()。这里的this指的是原始的UserService对象本身,而不是它的代理对象。流程变成了:
Controller->代理.a()->原始对象.a()->原始对象.b()调用
b()方法时,完全绕过了代理管家。管家根本没有机会为b()方法开启事务。
比喻的说法便于理解:
代理对象就像你的秘书。
你(原始对象)让秘书(代理)去处理一项工作(调用方法
a()),秘书会先做准备工作(开启事务)。但你在处理工作
a()的过程中,自己亲自去做了另一项本该由秘书安排的工作b()(直接this.b())。秘书不知道你亲自做了
b(),所以也就没有为b()做任何准备工作(开启事务)。
3. 首选解决方案:“放到另一个Service类中”
解决方案 a的代码示例:
// ServiceA.java
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // 注入另一个Service
public void a() {
// ... 一些业务逻辑
serviceB.b(); // 通过代理调用另一个Service的事务方法
// ... 其他逻辑
}
}// ServiceB.java
@Service
public class ServiceB {
@Transactional // 声明了事务
public void b() {
// ... 需要对数据库进行操作,期望在事务中执行
userRepository.save(...);
}
}为什么这样就能解决问题?
调用路径变化:现在,
ServiceA中的方法a()是通过@Autowired注入的serviceB来调用方法b()的。注入的是代理:Spring注入的
serviceB并不是原始的ServiceB对象,而是Spring为ServiceB创建的代理对象。代理生效:所以,调用
serviceB.b()的流程是:ServiceA.a()->ServiceB的代理.b()代理先开启事务
代理再调用原始ServiceB对象的
b()方法代理根据执行结果提交或回滚事务
这样一来,事务管理就完全正常了。
其他解决方案
b. 注入自身(Self-Injection):在
UserService里@Autowired一个UserService,然后通过这个注入的代理来调用b()。代码丑陋,显得很绕,不易理解。
可能引起循环依赖的警告。
c. 通过AopContext获取代理:需要暴露代理(
@EnableAspectJAutoProxy(exposeProxy = true)),然后在代码里写((UserService) AopContext.currentProxy()).b()。严重侵入代码,使得代码与Spring框架强耦合。
性能有轻微开销。
总结:
当你发现因为自调用导致事务、缓存、异步等注解失效时,第一反应就应该是:“我应该把这个方法抽到一个新的Bean里