Spring 事務(wù)失效了,怎么辦?
這是小伙伴們?cè)谖⑿派蠁柕囊粋€(gè)問題:
這個(gè)問題比較典型,讓我想到面試時(shí)有一個(gè) Spring 事務(wù)失效的問題,跟這個(gè)原因以及解決方案是一模一樣的,因此,抽空整篇文章和小伙伴們分享下。
1. AOP 的原理
小伙伴們知道,AOP 底層就是動(dòng)態(tài)代理,動(dòng)態(tài)代理有兩種實(shí)現(xiàn)方式:
- JDK 動(dòng)態(tài)代理:利用攔截器(必須實(shí)現(xiàn) InvocationHandler)加上反射機(jī)制生成一個(gè)代理接口的匿名類,在調(diào)用具體方法前調(diào)用 InvokeHandler 來(lái)處理。舉個(gè)例子,假設(shè)有一個(gè)接口 A,A 有一個(gè)實(shí)現(xiàn)類 B,現(xiàn)在要給 B 生成代理對(duì)象,那么實(shí)際上是給 A 接口自動(dòng)生成了一個(gè)匿名實(shí)現(xiàn)類,并且在這個(gè)匿名實(shí)現(xiàn)類中調(diào)用到 B 中的方法。
- CGLIB 動(dòng)態(tài)代理:利用 ASM 框架,對(duì)代理對(duì)象類生成的 class 文件加載進(jìn)來(lái),通過(guò)修改其字節(jié)碼生成子類來(lái)處理。舉個(gè)例子,現(xiàn)在有一個(gè)類 A,A 沒有接口,現(xiàn)在想給 A 生成一個(gè)代理對(duì)象,那么實(shí)際上是自動(dòng)給 A 生成了一個(gè)子類,在這個(gè)子類中覆蓋了 A 中的方法,所以,小伙伴們要注意,A 類以及它里邊的方法不能是 final 類型的,否則無(wú)法生成代理。
如果被代理的對(duì)象有接口,則可以使用 JDK 動(dòng)態(tài)代理,沒有接口就可以使用 CGLIB 動(dòng)態(tài)代理。
在 Spring 中,默認(rèn)情況下,如果被代理的對(duì)象有接口,就使用 JDK 動(dòng)態(tài)代理,如果被代理的對(duì)象沒有接口,則使用 CGLIB 動(dòng)態(tài)代理。
在 Spring Boot 中,2.0 之前也跟 Spring 中的規(guī)則一樣,2.0 之后則統(tǒng)一都使用 CGLIB 動(dòng)態(tài)代理。
不過(guò)這些都是默認(rèn)的規(guī)則,如果有接口,但是你又希望使用 CGLIB 動(dòng)態(tài)代理,通過(guò)修改配置,也都是可以實(shí)現(xiàn)的:
如果是 XML 配置,想使用 CGLIB 動(dòng)態(tài)代理,可以按如下方式實(shí)現(xiàn):
<aop:config proxy-target-class="true">
<aop:pointcut id="pc1" expression="。。。"/>
<aop:aspect ref="logAdvice">
。。。
</aop:aspect>
</aop:config>
如果是 Java 配置,想使用 CGLIB 動(dòng)態(tài)代理,可以按如下方式實(shí)現(xiàn):
@Component
@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class LogAspect {
}
當(dāng)然,在新版 Spring Boot 項(xiàng)目中,有接口的類默認(rèn)就是使用 CGLIB 動(dòng)態(tài)代理的。但是此時(shí)如果有接口的類你又想使用 JDK 動(dòng)態(tài)代理,那么可以通過(guò)如下配置:
spring.aop.proxy-target-class=false
關(guān)于 Spring Boot 中的 AOP 代理問題,可以參考去年松哥寫的文章:Spring Boot 中的 AOP,到底是 JDK 動(dòng)態(tài)代理還是 Cglib 動(dòng)態(tài)代理?。
2. 實(shí)際用的類
基于第一小節(jié)的講解,小伙伴們知道,當(dāng)你在項(xiàng)目中用到了 AOP 之后,其實(shí)你所以見到的類,并不是原本的類了。
雖然是解決不同的問題,但是有一個(gè)共同的點(diǎn),那就是都是通過(guò)自定義注解+ AOP 解決問題的。
現(xiàn)在我就以手把手教你玩多數(shù)據(jù)源動(dòng)態(tài)切換!為例,來(lái)和大家說(shuō)說(shuō)這里的動(dòng)態(tài)代理到底是咋回事,沒看過(guò)這篇文章的小伙伴可以先看下。
小伙伴們看下,我的 UserService 大致上是下面這樣:
@Service
public class UserService {
@Autowired
UserMapper userMapper;
@DS("master")
public Integer count() {
return userMapper.getCount();
}
}
小伙伴們看到,count() 方法上加了 @DS 注解,所以這個(gè) count() 方法將來(lái)是要被自動(dòng)代理的。換言之,當(dāng)你在另外一個(gè)類中注入 UserService 的時(shí)候,其實(shí)不是這個(gè) UserService,我 DEBUG 小伙伴們來(lái)看一下:
小伙伴們從圖中可以看到,此時(shí)我注入的 UserService 并不是真正的 UserService,而是一個(gè)通過(guò) CGLIB 動(dòng)態(tài)代理為 UserService 生成的子類,這個(gè)子類里邊的 count 方法大致邏輯類似下面這樣(其實(shí)就是 AOP 中的代碼,具體小伙伴們可以參考 手把手教你玩多數(shù)據(jù)源動(dòng)態(tài)切換!一文):
# 切換數(shù)據(jù)源
# 去數(shù)據(jù)庫(kù)查詢 count
# 清空 ThreadLocal 中的變量
# ...
但是,如果我的調(diào)用邏輯是這樣呢:
@Service
public class UserService {
@Autowired
UserMapper userMapper;
public Integer count2() {
return count();
}
@DS("master")
public Integer count() {
return userMapper.getCount();
}
}
小伙伴們來(lái)看,count2 方法,這個(gè)時(shí)候直接在 count2 方法中調(diào)用了 count 方法,當(dāng)然,count2() 方法中的調(diào)用也可以寫作 this.count();,這樣看起來(lái)就更明確了,我們調(diào)用 count 方法,使用的是當(dāng)前對(duì)象,而當(dāng)前對(duì)象是不包含代理對(duì)象中的代碼的,我們通過(guò) DEBUG 來(lái)看下:
所以,當(dāng)我們?cè)?count2 中直接調(diào)用 count 方法的時(shí)候,那么加在 count 方法上的注解就會(huì)失效。
3. 問題解決
這個(gè)問題存在于所有使用了 AOP 的地方,存在的原因第二小節(jié)已經(jīng)分析的很清楚了。
解決辦法其實(shí)也有很多種,最為簡(jiǎn)單省事的一種,就是在當(dāng)前類中注入代理對(duì)象,然后通過(guò)代理對(duì)象去調(diào)用其他方法,如下:
@Service
public class UserService {
@Autowired
UserMapper userMapper;
@Autowired
UserService userService;
public Integer count2() {
return userService.count();
}
@Transactional
@DS("master")
public Integer count() {
return userMapper.getCount();
}
}
雖然問題解決了,不過(guò)這畢竟不是一個(gè)好的解決辦法(因?yàn)樽约褐凶⑷胱约海谛掳?Spring Boot 中要開啟循環(huán)依賴才能實(shí)現(xiàn)),大家在實(shí)際開發(fā)中,還是要從設(shè)計(jì)上盡量避免這種問題。
好啦,這個(gè)問題搞明白了,那么事務(wù)失效這個(gè)問題,也不用我多說(shuō)了吧!