1. 程式人生 > >Spring事務異常rollback-only

Spring事務異常rollback-only

前言

在利用單元測試驗證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.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

這裡原因還是和上面一樣outermost transaction boundary執行commit,應該是rollback

定位720行程式碼

// Throw UnexpectedRollbackException only at outermost transaction boundary
            // or if explicitly asked to.
            if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
                throw new UnexpectedRollbackException(
                        "Transaction rolled back because it has been marked as rollback-only");
}

這裡是出錯的位置,我們一層一層定位上去,找到了下面的程式碼

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            // Standard transaction demarcation with getTransaction and commit/rollback calls.
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
            Object retVal = null;
            try {
                // This is an around advice: Invoke the next interceptor in the chain.
                // This will normally result in a target object being invoked.
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // target invocation exception
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }
            commitTransactionAfterReturning(txInfo);
            return retVal;
}

這裡執行了commitTransactionAfterReturning而不是completeTransactionAfterThrowing(txInfo, ex); 這很明顯,是因為沒有捕獲異常,導致的原因是我try-catch掉了。沒辦法,去掉try-catch,或者拋異常的logonService傳播機制改為propagation=Propagation.REQUIRES_NEW,讓它自己獨自提交回滾,別再設定rollback-only這種全域性的標識來噁心。

看看spring事務能什麼樣的異常能捕獲並回滾,什麼異常不捕獲,直接提交。上面的程式碼completeTransactionAfterThrowing,進去以後會發現有一個if else邏輯,其中條件判斷為

txInfo.transactionAttribute.rollbackOn(ex)

進去以後我找到了下面的程式碼

public boolean rollbackOn(Throwable ex) {
        return (ex instanceof RuntimeException || ex instanceof Error);
}

看樣子,spring預設只對RuntimeException和Error做捕捉,並回滾,其他的異常,直接提交

最後談談自己讀原始碼的一些經驗。
1、最好還是從異常報錯資訊中一步一步定位去了解為什麼出現這樣的錯誤
2、實在處於興趣想讀原始碼,那麼使用eclipse提供的工具如call Hierarchy,點選某個方法,直接右鍵,可以找到,或者使用預設快捷鍵ctrl+alt+h。這個工具提供了方法呼叫、與被呼叫的樹層次結構,在上面點點,一步一步下去,可以點某個方法立即定位之前的程式碼
3、debug,這個必須要用吧,不然,那麼複雜的類層次結構,沒有指南針怎麼行
4、主要還是理解裡面處理的思想,實現的話還是不要太過於糾結,先理清思路,明白具體做什麼的。在慢慢深入,有值得借鑑的地方,去模仿
5、花時間、慢慢啃,每次總會有收穫的