發現問題
最近,碰到一個問題,再用spring實現事務管理的時候,發現不起作用,在出異常時,並不會回滾數據庫操作。
我想實現的功能如下:
@Transactional(isolation=Isolation.DEFAULT,readOnly=false,propagation=Propagation.REQUIRED,rollbackFor=Exception.class) public boolean someService() { mapper.insert(); mapper.insert(); }
TransactionManager的配置如下:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>
但是在測試中,在service中手動拋出異常后,第一條insert語句並不會回滾。
之后研究了很久,在控制台打印的消息顯示,並沒有開啟事務。那么我分析應該有下面幾種可能:
- 配置錯誤
- JDBC的AutoCommit沒有設置為false
- 控制台信息顯示,在Service中,insertA在jdbt連接池中取出了一個連接,插入完后,便將連接返回給了連接池,所以在service中,有多個數據庫連接分別處理不同的insert語句。這也可能導致事務失敗。
下面來逐步排除:
AutoCommit=true,這個是有可能的,但是通過查看Spring的源碼,發現Spring在事務中,是會檢測當前連接的AutoCommit選項並把它設置為False,所以排除
最后,通過查看Spring關於Transaction的文檔,發現上面說了,如果使用的是DataSourceTransactionManager,那么只能在一個數據庫連接上開啟事務。那么這么說,應該是數據庫連接池的問題了?或者是Spring自己並沒有開啟事務管理,而是將事務管理委托給了Mysql去實現?這個是有可能的,所以后面再看。
解決問題
然后,我選擇使用了切面Aop來實現Spring的事務管理。步驟如下:
//注入transactionManager @Autowired() @Qualifier("transactionManager1") private PlatformTransactionManager transactionManager;
這一步是獲得自動注入的 transactionManager實例,之后使用TransactionTemplate
//編程式開啟事務 TransactionTemplate template = new TransactionTemplate(transactionManager);
是不是很像JDBCTemplate?那么一樣的使用內部類實現回調:
//在這邊也可以取消返回值,設為void boolean result = (Boolean) template.execute(new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus transactionStatus) { try { mapper.insertA(); throw new SQLException(); mapper.insertB(); } catch (Exception e) {mapper.insertB(); //出異常時,手動設置回滾 transactionStatus.setRollbackOnly(); e.printStackTrace(); return false; } return true; } });
這樣,就可以使用Aop手動開啟事務了。
經過測試,在兩個插入之間手動拋出了異常,insertA的插入被回滾了。從控制台輸出信息來看,insertA插入成功后,即把jdbc返回到連接池了,並且事務管理配置的依然是DataSourceTransactionManager,說明前面的數據庫連接池的問題被排除了。
分析原因
那么就剩下配置錯誤了。
實驗多次以后,我發現如下配置是可以讓聲明式事務管理起作用的:
Spring的配置中,取消注入Controller
<context:component-scan base-package="com.test"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan>
SpringMVC的配置,取消注入Service
<context:component-scan base-package="com.test"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" /> </context:component-scan>
配置事務的Service實現某個接口
@Service public class Test() { @Transactional(isolation=Isolation.DEFAULT,readOnly=false,propagation=Propagation.REQUIRED,rollbackFor=Exception.class) public boolean someService() { mapper.insert(); mapper.insert(); } }
這樣便可以開啟事務,並在遇到異常時回滾了。那為什么原來的配置就不行呢?
根本原因是因為spring的context是父子容器,由ServletContextListener 加載spring配置文件產生的是父容器,springMVC加載配置文件產生的是子容器,子容器對Controller進行掃描裝配時裝配了@Service注解的實例 (@Controller 實例依賴@Service實例),而該實例理應由父容器進行初始化以保證事務的增強處理,所以此時得到的將是原樣的Service,沒有經過事務加強處理,故而沒有事務處理能力
所以在spring的配置中我們要手動exclude controller的注入,不掃描帶有@Controller注解的類。因為這些類已經隨容器啟動時,在servlet-context中掃描過一遍了
在springMVC的配置中掃描業務組件,讓springMVC不掃描帶有@Service注解的類(留在spring中掃描@Service注解的類),防止事務失效。
在需要配置事務管理的Service類中,需要讓類繼承一個或幾個接口,是因為如果不繼承接口,那么會使用cglib動態代理去實現,那么事務管理也會失效。如果實現了接口,那么便會使用jdk自帶的動態代理去實現,這樣便可以成功的開啟事務了。