轉自:https://blog.csdn.net/sgls652709/article/details/49472719
前言
在利用單元測試驗證spring事務傳播機制的時候出現了下面的異常:
Transaction rolled back because it has been marked as rollback-only。記錄問題解決的步驟
正文
代碼示例
代碼-測試單元
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:config/spring-config-test.xml") @TransactionConfiguration(transactionManager="transactionManager",defaultRollback=false) @Transactional public class RegisterServiceTest { @Resource(name="registerService") private IRegisterService service; @Test public void registerTest() { RegisterDTO dto = new RegisterDTO(); dto.setDisplayname("superman12345"); dto.setPassword("99999"); service.register(dto); } }
代碼-RegisterService
@Transactional @Service public class RegisterService implements IRegisterService { @Resource private ILogonService logonService; @Resource private IUserService userService; @Override @Transactional(propagation=Propagation.REQUIRED) public void register(RegisterDTO dto) { try{ logonService.addLogon(dto); }catch(Exception e) { } userService.addUser(dto); } }
代碼-LogonService
@Transactional @Service public class LogonService implements ILogonService { @Resource(name="logonDaoImpl") private LogonDAO logonDao; @Override @Transactional(propagation=Propagation.REQUIRED) public int addLogon(RegisterDTO dto) { //注冊登錄信息 logonDao.addLogon(dto); throw new RuntimeException(); } }
代碼-UserService
@Transactional @Service public class UserService implements IUserService { @Resource(name="userDaoImpl") private UserDAO userDao; @Override @Transactional(propagation=Propagation.REQUIRED) public int addUser(RegisterDTO dto) { // 是否存在用戶 if (userDao.findUser(dto) != null) { throw new RuntimeException("已經存在用戶"); } // 注冊用戶,使用jdbcTempalte插入用戶信息 int userid = userDao.addUser(dto); dto.setUserid(userid); return userid; } }
背景說明:
一、從上面的代碼看出,我是采用注解來定義與注入spring元數據的,spring在web.xml文件的監聽函數ContextLoaderListener,創建applicationContext,在AbstractApplicationContext的refresh中,加載元數據,裝配元數據以及初始化元數據,對於service層的類,符合事務切面中的切點的匹配,那么在初始化這些service對象的時候采用的是代理創建,所以在Ioc容器(BeanFactory提供緩存元數據信息的集合)中,我們緩存的這些service對象就是代理對象。執行logonService.addLogon,userService.addUser的時候,我們執行代理對象的方法,其中事務攔截器TransactionInterceptor便是tx:advice提供的增強,通過代理織入到我們的業務代碼中
二、事務傳播機制的實現原理,如果幾個不同的service都是共享同一個connect(也就是service對象嵌套傳播機制為Propagation.REQUIRED),jdbc的connect.commit、connect.rollback,一起提交,一起回滾。這里面共享conntion應該就是共享同一個事務了。不同的connect,來執行commit/rollback自然是獨立的。同一個connection,如果一個service已經提交了,在另外service中connect.rollback自然對第一個service提交的代碼回滾不了的。所以spring處理且套事務,就是在TransactionInterceptor方法中,根據一系列開關(Propagation枚舉中的屬性),來處理connetion事務是同一個還是重新獲取,如果是同一個connection,不同service的commit(注:①)與rollback(注:②)的時機
注①:執行某一個service的時候根據傳播機制例如REQUIRED,spring發現事務沒建立,建立事務,在status對象中標記newTransaction為true,嵌套事務還有一個service是REQUIRED,那么使用這個事務,它的status中newTransaction為false,如果newTransaction為false的時候,commit全部跳過,如果是true,那么說明這個service是事務outermost transaction boundary,開始提交
注②:如果newTransaction為false,那么標記為rollback-only,如果是true,那么執行rollback
代碼調試
執行的時候發現出現了下面的異常
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:720) at org.springframework.test.context.transaction.TransactionalTestExecutionListener$TransactionContext.endTransaction(TransactionalTestExecutionListener.java:597) at org.springframework.test.context.transaction.TransactionalTestExecutionListener.endTransaction(TransactionalTestExecutionListener.java:296) at org.springframework.test.context.transaction.TransactionalTestExecutionListener.afterTestMethod(TransactionalTestExecutionListener.java:189) at org.springframework.test.context.TestContextManager.afterTestMethod(TestContextManager.java:404) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:91) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:232) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:175) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
根據上面出錯異常定位到異常信息的720行,報錯代碼satus.isNewTransaction為true
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) { throw new UnexpectedRollbackException( "Transaction rolled back because it has been marked as rollback-only"); }
這段代碼的意思是:共享的事物中已經有service出錯了,已經標記成rollback-only了,這里isNewTransaction是true,那么說明你是到了事物最外層的service了,你就不應該commit,應該rollback的。但是我想知道為什么會執行commit而不是rollback
定位異常報錯第597行,下面的代碼是spring-test中的源碼
public void endTransaction(boolean rollback) { if (rollback) { this.transactionManager.rollback(this.transactionStatus); } else { this.transactionManager.commit(this.transactionStatus); } }
原來這里由rollback控制,我繼續向上定位,看rollback是如何獲取的
定位代碼296行
private void endTransaction(TestContext testContext, TransactionContext txContext) throws Exception { boolean rollback = isRollback(testContext); if (logger.isTraceEnabled()) { logger.trace(String.format( "Ending transaction for test context %s; transaction status [%s]; rollback [%s]", testContext, txContext.transactionStatus, rollback)); } txContext.endTransaction(rollback); if (logger.isInfoEnabled()) { logger.info((rollback ? "Rolled back" : "Committed") + " transaction after test execution for test context " + testContext); } }
在boolean rollback = isRollback(testContext);獲取rollback,進入代碼,最后發現由成員屬性defaultRollback來控制,這個defaultRollback就是上面我配置的
@TransactionConfiguration(transactionManager="transactionManager",defaultRollback=false)
這里我設置成了defaultRollback為false,說到這行代碼我單元測試也剛剛掌握點皮毛,我發現只要有@Transactional就可以自動回滾測試代碼,不論成功與否。好吧,看到上面代碼新奇,用上了,控制默認不會滾,碰到錯誤也強制提交,okey,碰到事務嵌套,如果共享事物中某個service出現錯誤(注:③),那么強制提交也錯了
注③:spring事務源碼,對runtimeException和error的異常會捕獲處理回滾,但是檢查異常代碼,不會捕獲,直接提交,這樣也會導致rollback-only這樣的異常,當然,像我上面代碼service層直接try catch掉嵌套事務中,某一個service異常,在共享事物的時候,外層捕獲不到異常,直接commit,也是會出現rollback-only這樣的異常的,這在下面我會分析
代碼修改
上面測試代碼defaultRollback設置成true。將共享事務最開始(newTransaction為true)設在RegisterService中,它的事務傳播機制改成
@Transactional(propagation=Propagation.REQUIRES_NEW) public void register(RegisterDTO dto) { try{ logonService.addLogon(dto); }catch(Exception e) { } userService.addUser(dto); }
分析一下這里執行的過程:單元測試創建了一個事務,調用register,發現傳播機制是REQUIRES_NEW,那么掛起原來的事物,重新新建事務,logonService方法與userService方法是Propagation.REQUIRED,所以會共享這個新建的事物,register這里是它們
代碼-異常信息
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:720) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:478) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:272) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207) at com.sun.proxy.$Proxy25.register(Unknown Source) at org.test.service.RegisterServiceTest.registerTest(RegisterServiceTest.java:28) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:232) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:175) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675) at org.eclipse.jdt.internal.junit