解决springmvc web项目中 @Transactional事物注解失效问题 并深入源码分析问题

记录一次查找事务失效的过程。首先先描述一下问题,用测试用例测试事物方法,测试很成功,方法无异常,数据插入成功,方法中抛出错误,事物回滚,插入失败。后面用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,如果没有的话 ,再去自己所在的上下文中获取。
有什么问题,欢迎大家留言评论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值