近期遇到一个spring注解事务失效的问题,先上代码(以下代码经过简化,只保留关键点)。
没耐心的朋友可以直接看最下面结论。
第一部分:具体问题
接下来上一下代码,大致介绍一下流程。
Controller
ServiceImpl的属性
重点看serviceImpl的方法(经过简化,只保留了关键点)
最下面的抛出异常是为了测试事务。
其中红框里面的super.addNew方法上有@Transactional(rollbackFor = Exception.class)注解。所以看上去并没有什么问题,但是每次抛出异常,数据库的操作都没有回滚。
问题大致就是这样,本来以为事情会很容易被解决,因为在网上看过很多事务失效的例子,各种原因被各种大神列举了很多次了。
第二部分:常规问题解决
普通事务失效原因:
1.如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB
2. 如果使用了spring+mvc,则context:component-scan重复扫描问题可能会引起事务失败。 (即父子容器)
3. @Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。
4. @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。
5. Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
以上五点摘抄自:Spring事务失效的原因
看到常规的原因,首先的解决思路如下
1.首先,数据库使用的是oracle,所以排除1
2.项目确实使用的是spring+mvc不过已经分开扫描了,mvc只扫描Controller和RestController,spring扫描排除这两个注解这样可以排除2,
3.事务管理器配置确实是放在spring容器里,用Listener加载的
4.方法是public
5.是在类的方法上使用的,非接口上
所以,常规的问题被我巧妙地全避开了。
第三部分:解决思路
这个事务失效问题是第一次出现,并没有广泛存在我们的系统中。于是和其他的事务方法对比,发现其他的事务方法都是通过Service.getInstance()方法获取到的实例,跟进去,发现getInstance方法是通过我们项目定义的工具类从xml里面getBean获取到的。看上去貌似没什么问题,getBean和使用@Autowired不都一样嘛,都能获取到bean。
打开日志debug级别,看能不能查到点什么细节。并且给方法入口和spring的事务管理器入口org.springframework.transaction.interceptor.TransactionInterceptor#invoke打上断点,然后运行到该事务方法。
首先在方法入口的断点处用evaluate expression执行AopUtils.isAopProxy这个方法来看一下注入的serviceImpl是否被代理了,结果是true。
继续执行,进入到spring事务管理器里面,然后一步步跟代码,获取TransactionInfo等,并没有发现什么可疑点,因为都已经进入到了spring事务管理器了,而spring几乎不会出bug,进入到方法第一行后,查看了下debug日志,发现有Creating new transaction with name [XXX]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '',-java.lang.Exception,继续执行,执行到super.addNew方法时,debug日志又出现了一行 Creating new transaction with name [XXX]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '',-java.lang.Exception,而且此XXX非刚才的XXX,意思是在第一个事务里面,又起了一个事务,但是我事务没有显示配置PROPAGATION,默认的是REQUIRED,为什么会又重新起一个事务呢,这里让我百思不得其解。于是我回过头又检查了一遍父子容器问题,是不是还是因为包扫描重复导致的呢,虽然如果是子容器(即MVC)的话,肯定不会进入到事务管理器,但我还是抱着侥幸心理检查了一遍,没有任何问题。
没找到问题所在,继续执行下去,发现跑出的异常被事务管理局接管了,debug日志也显示了transaction rollback。这就更奇怪了,既然事务都回滚了,数据是怎么插到数据库里的呢。我继续在插数据的那一行sql打上断点,重复执行,发现执行完这条sql的时候,数据就已经在数据库了,但是事务还没提交。。这又是怎么回事。这里我想到了会不会是数据库连接设置的autoCommit,于是重复执行,在spring的transaction源码里面打断点,发现事务管理器会自动把autoCommit关掉。
感觉这条追查的路已经堵死了。。反而产生了两个其他的问题,1.既然是PROPAGATION_REQUIRED,为什么会在事务里又起一个事务,2.既然事务都还没提交,你是怎么插入到数据库里的。。
想到最一开始观察到的,和其他事务起作用的方法差别在于,他们的成员变量是getInstance获得的,而这个有问题的是@Autowired注入的,那么是不是这里有问题呢,于是我把Autowired的全换成了getInstance获取在xml里面注册的bean,果然,事务起作用了。可是,这tm到底咋回事。。。Autowired和getBean不是一样么,为什么会出现两种截然不同的结果。刚好最近刚看完SpringIoc的源码,就决定追查到底。
第四部分:真相大白
理理思路,父容器先加载,子容器后加载,子容器在加载过程中会通过获取到父容器。
请求到达应用后,Controller会先被初始化,然后他的私有属性serviceImpl会被 inject,实质上是会被父容器inject,然后循环注入serviceImpl私有属性的私有属性。。一直循环直到所有的bean都被实例化。事务管理器也在父容器中定义,貌似没有问题,事实也证明了,事务管理器确实代理了目标方法。
然后深入看了下getInstance方法,这个方法是通过项目的一个工具类的getBean方法工作的,工具类的getBean方法实际上是new了一个ClassPathXmlApplicationContext,然后调用他的getBean方法,那么看到这里好像一切都明朗了。。
@Autowired自动装配的是web.xml里面定义的Listener加载的父容器里的bean,getInstance是自己定义的第二个容器获取到的bean,那么事务管理器也是一样,实际上有两个事物管理器,第一个是webxml加载父容器的,第二个是工具类加载的容器的。所以才会出现在一个事务中又起一个事务,因为容器2不知道方法已经在容器1起了一个事务,我所看到的事务没提交也是看到的容器1起的事务没提交。
如果我再仔细缕一遍全流程就会发现,dao里面还封装了一层mybatis的sqlsession,这里使用的是工具类的getBean方法。
如果再仔细看一下debug日志的话,就会发现启动应用的时候,每个xml都被加载了两次,一次是webxml的父容器,一次是工具类new加载的。
终章:结论
遇到事务失效的时候,先看一下是否被事务管理器接管了,如果有,那么多半是容器的问题,这里并不一定是父子容器,也有可能是父父容器。。
借用《黑客与画家》的一句话:如果自己就是潮水的一部分,又怎么能看见潮流的方向呢。
当遇到问题时,不要迷在某个局部,要跳出来,先把整个流程理清。
最后再说一句,开启了上帝视角之后,感觉这问题实在是很简单,不过解决过程中花费了我本人非常多时间(不少于十小时)。
希望这篇博客可以给有问题的朋友带来一点思路。