核心流程解讀
我們從一段簡單的代碼入手,從頭到尾分析以下其中的奧秘。
如果在一個controller中調用service方法,該方法被@Transaction注解修飾。
controller方法:
@GetMapping("/save")
public String saveStudent() {
testService.save();
return "success";
}
service方法:
@Transactional
public void save() {
//資料操作
}
那麼Spring在啟動時,将會在主流程refresh的registerBeanPostProcessors的方法中首先加載所有的BeanPostProcessor,接着在
finishBeanFactoryInitialization中去初始化所有非懶加載的Bean。
首先對該controller進行加載,在屬性指派階段,發現依賴service,于是轉頭去加載該service。
Bean的生命周期主要包含以下階段:
接着執行個體化該service,執行屬性指派與初始化,在初始化的末尾,會執行BeanPostProcessor實作類AbstractAutoProxyCreator的postProcessAfterInitialization方法,該方法會判斷是否需要為該service對象生成代理。
在Bean的生命周期全貌中,postProcessAfterInitialization方法位于綠色箭頭标記的地方。
(對Spring Bean的生命周期不熟悉的同學,可以參考我的另外一篇文章還記不住Spring Bean的生命周期?看這篇你就知道方法了!)
接着進行判斷是否進行事務代理,如果需要代理,則進入到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法中,嘗試擷取事務屬性。
到這裡,我們可以從代理角度得到事務不生效的兩個原因
從代理角度來看
computeTransactionAttribute方法中,明确表示,如果目前方法的修飾為不為public,則傳回null,代表不支援事務。
Spring可能覺得既然不是公開的方法,代表應該由本類自己控制,而不應該被代理控制。另外final方法、static方法也不支援事務。
當然,如果你對private方法應用@Transaction注解,則你的idea會進行警告,但真正運作時,并不會出現任何報錯,給人一種沒有問題的假象。
如果,你就是想對private應用該注解,并且也想讓事務生效,則可以使用AspectJ。
AspectJ是一種靜态代理方式,直接在編譯期間修改位元組碼,性能比較好。而且不受修飾符的限制,不管你是private、final還是static,都可以進行代理。
還有一個原因比較簡單,如果你的service沒有加上@Service注解,則代表該service沒有被Spring容器所管理,Spring更不會閑着沒事生成代理對象,自然就不具有事務的功能。
我們繼續走完代碼,最終會來自SpringTransactionAnnotationParser的parseTransactionAnnotation方法,該方法會檢查是否存在@Transaction注解。
如果存在@Transaction注解,則為該service生成代理對象,注入到controller的service變量中。
之後在調用controll的接口時,首先tomcat還會為本次請求配置設定一個線程,先執行所有的過濾器,接着進入DispatcherServlet的doDispatch方法中。
doDispatch調用handle方法,接着使用反射調用controller的接口方法,調用handle方法在SpringMVC主流程中被綠色箭頭标記的位置。
(對Spring MVC主流程、過濾器與攔截器不清楚的同學,可以參考我的另外一篇文章從源碼角度結合詳細圖例剖析過濾器與攔截器)
接着controller調用service方法,注意,spring為該service變量注入的是service的代理對象。
曆經千辛萬苦終于調用到了service的代理對象方法,接着代理方法會回調CglibAopProxy的靜态内部類DynamicAdvisedInterceptor的intercept方法,該方法内部會拿到事務攔截器TransactionInterceptor,調用攔截器的invoke方法:
public Object invoke(MethodInvocation invocation) throws Throwable {
//擷取代理方法所處的被代理類,即原始類
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
invokeWithinTransaction方法處于父類TransactionAspectSupport中
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
//擷取事務屬性源,如果為null的話,代表該方法沒有開啟事務
TransactionAttributeSource tas = getTransactionAttributeSource();
//擷取事務屬性
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
//擷取事務管理器
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
//擷取連接配接點辨別,格式為被代理類的全限定名+方法名
//例如:com.yang.ym.service.StudentService.save
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
//根據事務傳播行為,看是否有必要建立出一個事務,判斷邏輯位于AbstractPlatformTransactionManager的getTransaction方法中
//如果有必要建立一個事務時,會立即開啟一個新事務,并關閉自動送出
//最後會利用ThreadLocal将事務資訊綁定到目前線程中
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
//調用攔截鍊中的下一個攔截器,位于ReflectiveMethodInvocation中
//proceedWithInvocation會判斷目前攔截器是否是最後一個,如果是的話,則直接調用連接配接點方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
//當被代理方法執行異常時,會依據rollBackFor判斷是否進行復原
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
//将此事務資訊從目前線程中移除
cleanupTransactionInfo(txInfo);
}
//送出事務
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
//程式設計式事務,本次不作分析
}
}
這裡給到了我們新的思路,在異常處理這塊,也可能導緻事務不生效或未復原。
從異常處理以及異常類型角度來看
從invokeWithinTransaction中看出,如果我們在事務方法中,手動捕獲了異常,但并未向上抛出,則已經修改的資料不會復原。
例如:
@Transactional
public void save() {
try {
//a将不會進行復原
Student a = new Student(null, "a", 18);
studentDao.insert(a);
//模拟業務異常
int i = 1 / 0;
Student b = new Student(null, "b", 18);
studentDao.insert(b);
} catch (Exception e) {
e.printStackTrace();
}
}
當然,也不是抛出什麼異常都會進行復原的。
首先當原方法出現異常時,會進入到completeTransactionAfterThrowing中,最終會在RuleBasedTransactionAttribute的rollbackOn方法中進行判斷。
public boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
//@Transactional中的rollbackFor的值将會在項目啟動的時候注入到rollbackRules中
//将抛出的異常在rollbackRules中進行比對
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
//如果沒有比對到,則采用預設比對政策
if (winner == null) {
return super.rollbackOn(ex);
}
return !(winner instanceof NoRollbackRuleAttribute);
}
預設比對政策處于父類DefaultTransactionAttribute中
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
可以看出,如果抛出的異常在rollbackFor中比對不到時,将會采用預設政策。在預設政策中,如果異常屬于運作時異常或錯誤時,也将進行復原。
在Java的異常體系中,Throwable下的繼承關系如下圖:
Throwable分為Error和Exception,其中Exception又分為受檢異常(也稱運作時異常)和受檢查異常(編譯器強制手動捕獲)。
如果不在rollbackFor中指定受檢異常時,事務方法将不會進行復原。
例如:
@Transactional
public void save() throws FileNotFoundException {
//不會進行復原
Student a = new Student(null, "a", 18);
studentDao.insert(a);
throw new FileNotFoundException();
}
隻有在rollbackFor中指定受檢異常,或直接指定Exception時,才會進行復原。
@Transactional(rollbackFor = Exception.class)
public void save() throws FileNotFoundException {
//會進行復原
Student a = new Student(null, "a", 18);
studentDao.insert(a);
throw new FileNotFoundException();
}
從傳播行為角度來看
多個事務之間可能存在互相調用,那麼在發生異常的情況下,怎麼控制多個事務的復原呢?
怎麼指定部分事務不復原,其餘部分事務進行復原呢?這就用到了事務的傳播行為。
在Propagation枚舉類中,共定義了七種傳播行為,它們的特性為(針對于子方法):
傳播行為 | 特性 |
REQUIRED |
|
SUPPORTS |
|
MANDATORY |
|
REQUIRES_NEW |
|
NOT_SUPPORTED |
|
NEVER |
|
NESTED |
|
假如在某次開發中,本來所有的方法調用應該在一個事務中(propagation = Propagation.REQUIRED),但錯誤地指定為了REQUIRES_NEW。
首先我們調用TestService的save方法:
@Transactional
public void save() {
studentService.saveA();
studentService.saveB();
}
saveA與saveB在另外一個service類中:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveA() {
Student a = new Student(null, "a", 18);
studentDao.insert(a);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB() {
Student b = new Student(null, "b", 20);
studentDao.insert(b);
//模拟業務異常
int i = 1 / 0;
}
這樣saveB在出現異常後,資料b可以被復原,但資料a不會被復原,形成髒資料。
對七種傳播行為的測試,可以參考我的另外一篇部落格Spring事務的傳播行為
從方法調用角度來看
如果在一個service中,非事務方法調用事務方法,接着controller調用非事務方法時,則事務方法内的事務失效。
即調用saveA時,saveB裡的事務會失效,即資料a不會被復原。
public void saveA(){
saveB();
}
@Transactional
public void saveB() {
Student a = new Student(null, "a", 18);
studentDao.insert(a);
int i = 1 / 0;
}
這是為什麼呢?很簡單
當controller調用saveA時,并不會被代理對象攔截到,因為不需要為saveA生成代理方法。那麼就會直接執行原對象的方法,這時候的saveB,相當于this.saveB,即調用原對象的saveB方法,沒經過代理對象調用,是以事務不會生效。
那怎麼修改,使得事務生效呢?
可以把this.saveB改為代理對象.saveB,需要先在該service中注入自己。
@Service
public class TestService {
@Autowired
TestService testService;
public void saveA() {
testService.saveB();
}
@Transactional
public void saveB() {
//事務生效
Student a = new Student(null, "a", 18);
studentDao.insert(a);
int i = 1 / 0;
}
}
在本類中注入自己,很奇怪啊,不會發生循環依賴嗎?
當然,Spring本身具有解決循環依賴的能力,下圖以A依賴A為例,解釋解決循環依賴的具體過程:
如果想要了解更多關于Spring是如何解決循環依賴的知識點,可以參考我的另外一篇文章手把手教你解決循環依賴,一步一步地來窺探出三級緩存的奧秘
那事務方法調用事務方法,不過是以多線程的方式調用的呢?
例如:
@Transactional
public void saveA() {
Student a = new Student(null, "a", 18);
studentDao.insert(a);
new Thread(()->{
Student b = new Student(null, "b", 18);
studentDao.insert(b);
}).start();
int i=1/0;
}
資料b也不會被復原,事務失效了。
在TransactionSynchronizationManager類中,會将目前的事務資訊利用ThreadLocal綁定到線程中,也就是說,每個線程使用的是單獨的資料庫連接配接,分别存在于不同的事務中。是以出現異常後,資料a所處的事務會復原,而資料b處在另外一個事務中,不會進行復原。
從外部條件角度來看
在MySQL5.5版本之前,其預設的存儲引擎為MyISAM,它特點有
- 不支援事務
- 不支援外鍵
- 不支援行鎖,隻支援表鎖
- 為聚集索引
- 支援全文索引
如果我們操作的表屬于MyISAM類型,則事務是不可能生效的。
MySQL5.5之後,預設存儲引擎改為了InnoDB,這個是支援事務的。
是以,小夥伴們在發現事務不生效,抓耳撓腮始終找不到原因時,别忘了偷瞄一下表的類型。
總結
事務失效或不復原的情況,可以從以下的角度來思考:
從代理角度來看
1、方法的修飾符不是public類型的,另外final與static也是不支援事務的。
2、事務方法所處的對象并沒有Spring管理,自然也不會生成代理對象。
從異常處理以及異常類型角度來看
1、事務方法中手動捕獲異常,但并未向上抛出,那麼該方法不會進行復原。
2、事務方法中抛出的異常,不在rollbackFor中,也不屬于RuntimeException或者Error時,不會進行復原。
從傳播行為角度來看
1、傳播行為指定錯誤,例如:内部方法和外部方法不在一個事務中,内部方法復原後,外部方法沒有進行復原