记录一次查找事务失效的过程。首先先描述一下问题,用测试用例测试事物方法,测试很成功,方法无异常,数据插入成功,方法中抛出错误,事物回滚,插入失败。后面用tomcat跑这个web项目,发现这个事物方法上的事物注解@Transactional失效了。下面还原这个过程,并在还原后,进行问题解决以及源码分析。
如不想看还原过程的朋友,可以直接跳到后面,继续观看。
下面贴出部分代码,供后续分析使用
public interface CostService {
void insert(Cost cost);//插入数据库
int sum();//求和
}
@Service
public class CostServiceImpl implements CostService {
@Resource
private CostMapper costMapper;
@Transactional(rollbackFor = Exception.class)
public void insert(Cost c) {
costMapper.insert(c);
int a = 1 / 0;
}
}
下面为测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:spring-mvc.xml",
"classpath:spring-mybatis.xml"
})
public class TestClass {
@Resource
CostService costService;
@Test
public void test1(){
System.out.println("before insert:"+costService.sum());
try {
Cost cost = new Cost();
cost.setMoney(100);
costService.insert(cost);
} catch (Exception e) {
System.out.println(e.toString());
}
System.out.println("after insert:"+costService.sum());
}
}
运行测试类,结果如下,和我们预想的一样,事务方法抛出错误,事务回滚,插入数据库失败
下面为控制器类
@RestController
@RequestMapping("/test")
public class TestAction {
@Resource
private CostService costService;
@RequestMapping(value="test1",method=RequestMethod.GET)
public void test1(){
System.out.println("before insert:"+costService.sum());
try {
Cost cost = new Cost();
cost.setMoney(100);
costService.insert(cost);
} catch (Exception e) {
System.out.println(e.toString());
}
System.out.println("after insert:"+costService.sum());
}
}
接下来,通过tomcat运行该spring web项目,访问/test/test1接口,结果如下
虽然事务方法抛出错误,但是数据还是插入数据库成功了,从中说明事务失效了。
接下来,就研究一下,为什么tomcat启动spring web项目,为什么事务失效了。
首先,看一下web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<display-name>ssm-example</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mybatis.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
接下来看一下spring-mvc.xml配置文件,很简单,绝大部分人可能会这样简单配置一下,包括本人,就配置了一个spring自动扫描注解。而本次的关键,恰恰在于spring扫描注解并注册bean。
<context:component-scan base-package="com.wbb"></context:component-scan>
<mvc:annotation-driven></mvc:annotation-driven>
然后关于spring-mybatis.xml的配置在这里就不一一展示了,里面就配置了一些跟数据库相关的配置和事务,这里就看一下事务。
<!-- 事务管理 -->
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
接下来,深入源码,看一下springmvc初始化ioc容器的过程。springmvc初始化ioc容器的过程中,会生成两个上下文,一个是ContextLoaderListener启动生成的上下文为项目的根上下文。在根上下文的基础上,还有一个与web mvc相关的上下文用来保存控制器DispatcherServlet需要的mvc对象,作为根上下文的子上下文。
这里主要看一下子上下文DispatcherServlet的生成。
上图中显示了DispatcherServlet的继承关系。DispatcherServlet继承FrameworkServlet,而FrameworkServlet继承HttpServletBean,而DispatcherServlet初始化子上下文,是从HttpServletBean的init()方法开始的。
这里主要看一下initServletBean()方法。
这是个保护方法,交由子类FrameworkServlet去实现。
这里主要看一下initWebApplicationContext()方法,初始化应用子上下文
到这一步,就不继续深入进去了。这里主要看一下rootContext里面的属性,在里面可以找到beanFactory
在beanFactory里面可以找到beanDefinitionMap(bean定义map)和singleObjects(单例bean对象map)这两个属性。从这两个属性中,我们只找到transactionManager这个bean定义和bean对象,找不到costServiceImpl。因为前面web.xml配置的ContextLoaderListener加载的属性只有spring-mybatis.xml。而spring自动扫描注解配置在spring-mvc.xml中,该配置在DispatcherServlet加载。下面我们看一下子上下文,即上上图中的wac。
这里直接跳到方法的最后,看一下这个wac属性里的值。
跟上面一样,找到beanFactory中的singleObjects属性,在该属性中找到costServiceImpl
从图中可以看出,这个costServiceImpl并没有被事务增强,不是动态代理生成的代理类,仅仅是spring扫描到的普通注解bean,所以调用该CostService的insert()方法时,事务失效了,因为该方法上根本没有事务,其实也就没有事务失效的说法了。
解决方案,以本文的例子为例:
spring.xml配置文件修改为如下,spring自动扫描的时候,只扫描的Controller注解
<context:component-scan base-package="com.wbb" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<mvc:annotation-driven></mvc:annotation-driven>
因为本文ContextLoaderListener加载的配置文件只有spring-mybatis.xml,所有将spring自动扫描配置在这里,扫描除@Controller注解以外的注解
<context:component-scan base-package="com.wbb">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<mvc:annotation-driven></mvc:annotation-driven>
重新运行Debug,重复上面的步骤,找到initWebApplicationContext()方法里面的rootContext,重新来看一下beanFactory里面的singleObjects属性。在里面可以找到
transactionManager和costServiceImpl这两个bean,这里主要看一下costServiceImpl这个bean。如下图所示,该bean被事务增强了,是由jdk动态代理生成的代理类,所以调用insert()方法的时候,事务就有了。
接下来,重新访问/test/test1/接口,结果如下,这次就跟预想的一样了,事务有效了
看到最后,可能有人会问,为什么不将这两个配置文件都放在DispatcherServlet中,这样加载的时候transactionManager和costServiceImpl就在一个上下文上,后者就能被事务增强了,也就没有事务失效的问题了。有这个问题的朋友,可以去看一下这篇博客https://blog.csdn.net/py_xin/article/details/52052627,里面讲的应该还是挺清楚的
最后总结一下,个人认为,bean要被增强的话,必须和增强bean在一个上下文上。这样上下文才能知道是否在初始化生成bean的时候对这个bean进行增强操作。还有就是bean的获取,通过getBean向IOC容器获取的时候,容器会先去根上下文去获取bean,如果没有的话 ,再去自己所在的上下文中获取。
有什么问题,欢迎大家留言评论。