解决方法的核心在于理解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里