转自: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.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、花时间、慢慢啃,每次总会有收获的

Spring事务异常rollback-only的更多相关文章

  1. Spring事务异常rollback-only 笔记

    造成以上异常的原因情形: 在spring里面我们配置了事务的传播机制是REQUIRED,所以这两个事务最终会合并成一个事务.当a方法调用b方法时,程序中a方法中由于某某原因导致抛出异常(或者明确将该事 ...

  2. hibernate整合spring事务异常

    Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushM ...

  3. Spring事务异常回滚,捕获异常不抛出就不会回滚(转载) 解决了我一年前的问题

    最近遇到了事务不回滚的情况,我还考虑说JPA的事务有bug? 我想多了.......    为了打印清楚日志,很多方法我都加tyr catch,在catch中打印日志.但是这边情况来了,当这个方法异常 ...

  4. Spring事务异常回滚,捕获异常不抛出就不会回滚

    最近遇到了事务不回滚的情况,我还考虑说JPA的事务有bug? 我想多了.......    为了打印清楚日志,很多方法我都加tyr catch,在catch中打印日志.但是这边情况来了,当这个方法异常 ...

  5. 【转】Spring事务异常回滚,捕获异常不抛出就不会回滚

    最近遇到了事务不回滚的情况,我还考虑说JPA的事务有bug? 我想多了.......     为了打印清楚日志,很多方法我都加tyr catch,在catch中打印日志.但是这边情况来了,当这个方法异 ...

  6. Spring事务异常回滚

    最近遇到了事务不回滚的情况,我还考虑说JPA的事务有bug? 我想多了.......    为了打印清楚日志,很多方法我都加tyr catch,在catch中打印日志.但是这边情况来了,当这个方法异常 ...

  7. spring事务 异常回滚

    spring事务回滚 可抛出自定义继承自RuntimeException

  8. Spring AOP声明式事务异常回滚(转)

    转:http://hi.baidu.com/iduany/item/20f8f8ed24e1dec5bbf37df7 Spring AOP声明式事务异常回滚 近日测试用例,发现这样一个现象:在业务代码 ...

  9. Spring AOP声明式事务异常回滚

    近日测试用例,发现这样一个现象:在业务代码中,有如下两种情况,比如:throw new RuntimeException("xxxxxxxxxxxx"); 事物回滚throw ne ...

随机推荐

  1. [UE4]九宫格图片拉伸

    Draw As选择:Box Margin:边界尺寸.如果看不清楚,可以把“Draw As”选择“Border”:

  2. IDEA远程调试

    问题:         通常在java打成可运行的Jar项目时,运行jar时调试很不方便,特别是要在linux上面执行jar包的情况.此时需要将这个项目打成Jar包(如果是maven项目,直接使用pa ...

  3. 第11章 拾遗3:虚拟局域网(VLAN)

    1. 虚拟局域网(VLAN) (1)VLAN是建立在物理网络基础上的一种逻辑子网,它将把一个LAN划分成多个逻辑的局域网(VLAN),每个VLAN是一个广播域,VLAN内的主机间通信就和在一个LAN内 ...

  4. 本地IP,掩码,网关,DNS设置

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  5. Linux使用NFS服务实现远程共享

    首先安装 apt install -y nfs-kernel-server nfs-common 编辑配置文件 vim /etc/exports 添加内容: /root/test *(rw,sync, ...

  6. JavaWeb项目中web.xml有关servlet的基本配置

    JavaWeb项目中web.xml有关servlet的基本配置: 我们注意到,tomcat下的conf中也有一个web.xml文件,没错的,所有的JavaWeb项目中web.xml都继承自服务器下的w ...

  7. ThinkPHP5调用PHPExcel类实现导入导出

    注意:extend是放置第三方类的地方,不要乱配置命名空间那些,引起不必要的类错误 代码如下 <?php namespace app\index\controller; use think\Co ...

  8. 503 Error: need EHLO and AUTH first

    设置OUTLOOK2013使用QQ邮箱,按照QQ邮箱的配置介绍设置好后,收邮件的服务可以了,但是发送邮件的服务失败,报错:503 Error: need EHLO and AUTH first,经查, ...

  9. 数组转换成json key-value形式

    eg1(数组中包含的是数组): var jsonData = {}; var arr = [[1, 'boy', 'dabing'], [2, 'girl', 'dabing']]; for (var ...

  10. Java中的Map List Set等集合类

    一.概述 二 set map list的区别 三. Collections类和Collection接口 四. List接口,有序可重复的集合 五. Set接口,代表无序,不可重复的集合 六. Map接 ...