javascript
事务里面捕获异常_三问Spring事务:解决什么问题?如何解决?存在什么问题?...
1. 解決什么問題
讓我們先從事務(wù)說起,“什么是事務(wù)?我們?yōu)槭裁葱枰聞?wù)?”。
事務(wù)是一組無法被分割的操作,要么所有操作全部成功,要么全部失敗。我們在開發(fā)中需要通過事務(wù)將一些操作組成一個單元,來保證程序邏輯上的正確性,例如全部插入成功,或者回滾,一條都不插入。作為程序員的我們,對于事務(wù)管理,所需要做的便是進行事務(wù)的界定,即通過類似begin transaction和end transaction的操作來界定事務(wù)的開始和結(jié)束。
下面是一個基本的JDBC事務(wù)管理代碼:
// 開啟數(shù)據(jù)庫連接Connection con = openConnection();try { // 關(guān)閉自動提交 con.setAutoCommit(false); // 業(yè)務(wù)處理 // ... // 提交事務(wù) con.commit();} catch (SQLException | MyException e) { // 捕獲異常,回滾事務(wù) try { con.rollback(); } catch (SQLException ex) { ex.printStackTrace(); }} finally { // 關(guān)閉連接 try { con.setAutoCommit(true); con.close(); } catch (SQLException e) { e.printStackTrace(); }}直接使用JDBC進行事務(wù)管理的代碼直觀上來看,存在兩個問題:
而如果我們需要更換其他數(shù)據(jù)訪問技術(shù),例如Hibernate、MyBatis、JPA等,雖然事務(wù)管理的操作都類似,但API卻不同,則需使用相應(yīng)的API來改寫。這也會引來第三個問題:
上文列出了三個待解決的問題,下面我們看Spring事務(wù)是如何解決。
2. 如何解決
2.1 繁雜的事務(wù)管理API
針對該問題,我們很容易可以想到,在眾多事務(wù)管理的API上抽象一層。通過定義接口屏蔽具體實現(xiàn),再使用策略模式來決定具體的API。下面我們看下Spring事務(wù)中定義的抽象接口。
在Spring事務(wù)中,核心接口是PlatformTransactionManager,也叫事務(wù)管理器,其定義如下:
public interface PlatformTransactionManager extends TransactionManager { // 獲取事務(wù)(新的事務(wù)或者已經(jīng)存在的事務(wù)) TransactionStatus getTransaction(@Nullable TransactionDefinition definition)throws TransactionException; // 提交事務(wù) void commit(TransactionStatus status) throws TransactionException; // 回滾事務(wù) void rollback(TransactionStatus status) throws TransactionException;}getTransaction通過入?yún)ransactionDefinition來獲得TransactionStatus,即通過定義的事務(wù)元信息來創(chuàng)建相應(yīng)的事務(wù)對象。在TransactionDefinition中會包含事務(wù)的元信息:
- PropagationBehavior:傳播行為;
- IsolationLevel:隔離級別;
- Timeout:超時時間;
- ReadOnly:是否只讀。
根據(jù)TransactionDefinition獲得的TransactionStatus中會封裝事務(wù)對象,并提供了操作事務(wù)和查看事務(wù)狀態(tài)的方法,例如:
- setRollbackOnly:標(biāo)記事務(wù)為Rollback-only,以使其回滾;
- isRollbackOnly:查看是否被標(biāo)記為Rollback-only;
- isCompleted:查看事務(wù)是否已完成(提交或回滾完成)。
還支持嵌套事務(wù)的相關(guān)方法:
- createSavepoint:創(chuàng)建savepoint;
- rollbackToSavepoint:回滾到指定savepoint;
- releaseSavePoint:釋放savepoint。
TransactionStatus事務(wù)對象可被傳入到commit方法或rollback方法中,完成事務(wù)的提交或回滾。
下面我們通過一個具體實現(xiàn)來理解TransactionStatus的作用。以commit方法為例,如何通過TransactionStatus完成事務(wù)的提交。AbstractPlatformTransactionManager是PlatformTransactionManager接口的的實現(xiàn),作為模板類,其commit實現(xiàn)如下:
public final void commit(TransactionStatus status) throws TransactionException { // 1.檢查事務(wù)是否已完成 if (status.isCompleted()) { throw new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction"); } // 2.檢查事務(wù)是否需要回滾(局部事務(wù)回滾) DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; if (defStatus.isLocalRollbackOnly()) { if (defStatus.isDebug()) { logger.debug("Transactional code has requested rollback"); } processRollback(defStatus, false); return; } // 3.檢查事務(wù)是否需要回滾(全局事務(wù)回滾) if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { if (defStatus.isDebug()) { logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); } processRollback(defStatus, true); return; } // 4.提交事務(wù) processCommit(defStatus);}在commit模板方法中定義了事務(wù)提交的基本邏輯,通過查看status的事務(wù)狀態(tài)來決定拋出異常還是回滾,或是提交。其中的processRollback和processCommit方法也是模板方法,進一步定義了回滾、提交的邏輯。以processCommit方法為例,具體的提交操作將由抽象方法doCommit完成。
protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;doCommit的實現(xiàn)取決于具體的數(shù)據(jù)訪問技術(shù)。我們看下JDBC相應(yīng)的具體實現(xiàn)類DataSourceTransactionManager中的doCommit實現(xiàn)。
protected void doCommit(DefaultTransactionStatus status) { // 獲取status中的事務(wù)對象 DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); // 通過事務(wù)對象獲得數(shù)據(jù)庫連接對象 Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Committing JDBC transaction on Connection [" + con + "]"); } try { // 執(zhí)行commit con.commit(); } catch (SQLException ex) { throw new TransactionSystemException("Could not commit JDBC transaction", ex); }}在commit和processCommit方法中我們根據(jù)入?yún)⒌腡ransactionStatus提供的事務(wù)狀態(tài)來決定事務(wù)行為,而在doCommit中需要執(zhí)行事務(wù)提交時將會通過TransactionStatus中的事務(wù)對象來獲得數(shù)據(jù)庫連接對象,再執(zhí)行最后的commit操作。通過這個示例我們可以理解TransactionStatus所提供的事務(wù)狀態(tài)和事務(wù)對象的作用。
下面是用Spring事務(wù)API改寫后的事務(wù)管理代碼:
// 獲得事務(wù)管理器PlatformTransactionManager txManager = getPlatformTransactionManager();DefaultTransactionDefinition def = new DefaultTransactionDefinition();// 指定事務(wù)元信息def.setName("SomeTxName");def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);// 獲得事務(wù)TransactionStatus status = txManager.getTransaction(def);try { // 業(yè)務(wù)處理}catch (MyException ex) { // 捕獲異常,回滾事務(wù) txManager.rollback(status); throw ex;}// 提交事務(wù)txManager.commit(status);無論是使用JDBC、Hibernate還是MyBatis,我們只需要傳給txManager相應(yīng)的具體實現(xiàn)就可以在多種數(shù)據(jù)訪問技術(shù)中切換。
小結(jié):Spring事務(wù)通過PlatformTransactionManager、TransactionDefinition和TransactionStatus接口統(tǒng)一事務(wù)管理API,并結(jié)合策略模式和模板方法決定具體實現(xiàn)。
Spring事務(wù)API代碼還有個特點有沒有發(fā)現(xiàn),SQLException不見了。下面來看Spring事務(wù)是如何解決大量的異常處理代碼。
2.2 大量的異常處理代碼
為什么使用JDBC的代碼中會需要寫這么多的異常處理代碼。這是因為Connection的每個方法都會拋出SQLException,而SQLException又是檢查異常,這就強制我們在使用其方法時必須進行異常處理。那Spring事務(wù)是如何解決該問題的。我們看下doCommit方法:
protected void doCommit(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Committing JDBC transaction on Connection [" + con + "]"); } try { con.commit(); } catch (SQLException ex) { // 異常轉(zhuǎn)換 throw new TransactionSystemException("Could not commit JDBC transaction", ex); }}Connection的commit方法會拋出檢查異常SQLException,在catch代碼塊中SQLException將被轉(zhuǎn)換成TransactionSystemException拋出,而TransactionSystemException是一個非檢查異常。通過將檢查異常轉(zhuǎn)換成非檢查異常,讓我們能夠自行決定是否捕獲異常,不強制進行異常處理。
Spring事務(wù)中幾乎為數(shù)據(jù)庫的所有錯誤都定義了相應(yīng)的異常,統(tǒng)一了JDBC、Hibernate、MyBatis等不同異常API。這有助于我們在處理異常時使用統(tǒng)一的異常API接口,無需關(guān)心具體的數(shù)據(jù)訪問技術(shù)。
小結(jié):Spring事務(wù)通過異常轉(zhuǎn)換避免強制異常處理。
2.3 業(yè)務(wù)處理代碼與事務(wù)管理代碼混雜
在2.1節(jié)中給出了使用Spring事務(wù)API的寫法,即編程式事務(wù)管理,但仍未解決“業(yè)務(wù)處理代碼與事務(wù)管理代碼混雜”的問題。這時候就可以利用Spring AOP將事務(wù)管理代碼這一橫切關(guān)注點從代碼中剝離出來,即聲明式事務(wù)管理。以注解方式為例,通過為方法標(biāo)注@Transaction注解,將為該方法提供事務(wù)管理。其原理如下圖所示:
Spring事務(wù)會為@Transaction標(biāo)注的方法的類生成AOP增強的動態(tài)代理類對象,并且在調(diào)用目標(biāo)方法的攔截鏈中加入TransactionInterceptor進行環(huán)繞增加,實現(xiàn)事務(wù)管理。
下面我們看下TransactionInterceptor中的具體實現(xiàn),其invoke方法中將調(diào)用invokeWithinTransaction方法進行事務(wù)管理,如下所示:
protected Object invokeWithinTransaction(Method method, Class> targetClass, final InvocationCallback invocation) throws Throwable { // 查詢目標(biāo)方法事務(wù)屬性、確定事務(wù)管理器、構(gòu)造連接點標(biāo)識(用于確認(rèn)事務(wù)名稱) final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // 創(chuàng)建事務(wù) TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try { // 通過回調(diào)執(zhí)行目標(biāo)方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // 目標(biāo)方法執(zhí)行拋出異常,根據(jù)異常類型執(zhí)行事務(wù)提交或者回滾操作 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // 清理當(dāng)前線程事務(wù)信息 cleanupTransactionInfo(txInfo); } // 目標(biāo)方法執(zhí)行成功,提交事務(wù) commitTransactionAfterReturning(txInfo); return retVal; } else { // 帶回調(diào)的事務(wù)執(zhí)行處理,一般用于編程式事務(wù) // ... }}在調(diào)用目標(biāo)方法前后加入了創(chuàng)建事務(wù)、處理異常、提交事務(wù)等操作。這讓我們不必編寫事務(wù)管理代碼,只需通過@Transaction的屬性指定事務(wù)相關(guān)元信息。
小結(jié):Spring事務(wù)通過AOP提供聲明式事務(wù)將業(yè)務(wù)處理代碼和事務(wù)管理代碼分離。
3. 存在什么問題
Spring事務(wù)為了我們解決了第一節(jié)中列出的三個問題,但同時也會帶來些新的問題。
3.1 非public方法失效
@Transactional只有標(biāo)注在public級別的方法上才能生效,對于非public方法將不會生效。這是由于Spring AOP不支持對private、protect方法進行攔截。從原理上來說,動態(tài)代理是通過接口實現(xiàn),所以自然不能支持private和protect方法的。而CGLIB是通過繼承實現(xiàn),其實是可以支持protect方法的攔截的,但Spring AOP中并不支持這樣使用,筆者猜測做此限制是出于代理方法應(yīng)是public的考慮,以及為了保持CGLIB和動態(tài)代理的一致。如果需要對protect或private方法攔截則建議使用AspectJ。
3.2 自調(diào)用失效
當(dāng)通過在Bean的內(nèi)部方法直接調(diào)用帶有@Transactional的方法時,@Transactional將失效,例如:
public void saveAB(A a, B b){ saveA(a); saveB(b);}@Transactionalpublic void saveA(A a){ dao.saveA(a);}@Transactionalpublic void saveB(B b){ dao.saveB(b);}在saveAB中調(diào)用saveA和saveB方法,兩者的@Transactional都將失效。這是因為Spring事務(wù)的實現(xiàn)基于代理類,當(dāng)在內(nèi)部直接調(diào)用方法時,將不會經(jīng)過代理對象,而是直接調(diào)用目標(biāo)對象的方法,無法被TransactionInterceptor攔截處理。解決辦法:
(1)ApplicationContextAware
通過ApplicationContextAware注入的上下文獲得代理對象。
public void saveAB(A a, B b){ Test self = (Test) applicationContext.getBean("Test"); self.saveA(a); self.saveB(b);}(2)AopContext
通過AopContext獲得代理對象。
public void saveAB(A a, B b){ Test self = (Test)AopContext.currentProxy(); self.saveA(a); self.saveB(b);}(3)@Autowired
通過@Autowired注解注入代理對象。
@Componentpublic class Test { @Autowired Test self; public void saveAB(A a, B b) { self.saveA(a); self.saveB(b); } // ...}(4)拆分
將saveA、saveB方法拆分到另一個類中。
public void saveAB(A a, B b){ txOperate.saveA(a); txOperate.saveB(b);}上述兩個問題都是由于Spring事務(wù)的實現(xiàn)方式的限制導(dǎo)致的問題。下面再看兩個由于使用不當(dāng)容易犯錯的兩個問題。
3.3 檢查異常默認(rèn)不回滾
在默認(rèn)情況下,拋出非檢查異常會觸發(fā)回滾,而檢查異常不會。
根據(jù)invokeWithinTransaction方法,我們可以知道異常處理邏輯在completeTransactionAfterThrowing方法中,其實現(xiàn)如下:
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { if (txInfo != null && txInfo.getTransactionStatus() != null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex); } if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { try { // 異常類型為回滾異常,執(zhí)行事務(wù)回滾 txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by rollback exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by rollback exception", ex); throw ex2; } } else { try { // 異常類型為非回滾異常,仍然執(zhí)行事務(wù)提交 txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by commit exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by commit exception", ex); throw ex2; } } }}根據(jù)rollbackOn判斷異常是否為回滾異常。只有RuntimeException和Error的實例,即非檢查異常,或者在@Transaction中通過rollbackFor屬性指定的回滾異常類型,才會回滾事務(wù)。否則將繼續(xù)提交事務(wù)。所以如果需要對非檢查異常進行回滾,需要記得指定rollbackFor屬性,不然將回滾失效。
3.4 catch異常無法回滾
在3.3節(jié)中我們說到只有拋出非檢查異常或是rollbackFor中指定的異常才能觸發(fā)回滾。如果我們把異常catch住,而且沒拋出,則會導(dǎo)致無法觸發(fā)回滾,這也是開發(fā)中常犯的錯誤。例如:
@Transactionalpublic void insert(List users) { try { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); for (User user : users) { String insertUserSql = "insert into User (id, name) values (?,?)"; jdbcTemplate.update(insertUserSql, new Object[] { user.getId(), user.getName() }); } } catch (Exception e) { e.printStackTrace(); }}這里由于catch住了所有Exception,并且沒拋出。當(dāng)插入發(fā)生異常時,將不會觸發(fā)回滾。
但同時我們也可以利用這種機制,用try-catch包裹不用參與事務(wù)的數(shù)據(jù)操作,例如對于寫入一些不重要的日志,我們可將其用try-catch包裹,避免拋出異常,則能避免寫日志失敗而影響事務(wù)的提交。
作者:草捏子
來源:掘金
總結(jié)
以上是生活随笔為你收集整理的事务里面捕获异常_三问Spring事务:解决什么问题?如何解决?存在什么问题?...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python中的import详解_pyt
- 下一篇: lingo变量无限制版本_【运筹学】用L