最新换了框架,新项目用SpringMVC + spring JdbcTemplate。搭框架时,发现了一个事务无法正常回滚的问题,记录如下:
首先展示问题:
Spring applicationContext.xml配置:
< bean id = "dataSource" class = "org.springframework.jndi.JndiObjectFactoryBean" > < property name = "jndiName" > < value > java:comp/env/jdbc/will </ value > </ property > </ bean > < bean id = "jdbcTemplate" class = "org.springframework.jdbc.core.JdbcTemplate" > < property name = "dataSource" ref = "dataSource" /> </ bean > < bean id = "txManager" class = "org.springframework.jdbc.datasource.DataSourceTransactionManager" > < property name = "dataSource" ref = "dataSource" /> </ bean > < tx:annotation-driven transaction-manager = "txManager" />
Spring mvc.dispatcher.xml配置:
< context:component-scan base-package = "com.will" > </ context:component-scan > < mvc:annotation-driven /> < mvc:default-servlet-handler /> <!-- 拦截器 < mvc:interceptors > < bean class = "com.will.mvc.MyInteceptor" /> </ mvc:interceptors > --> < bean id = "viewResolver" class = "org.springframework.web.servlet.view.UrlBasedViewResolver" > < property name = "viewClass" value = "org.springframework.web.servlet.view.JstlView" /> < property name = "prefix" value = "/WEB-INF/pages/" /> < property name = "suffix" value = ".jsp" /> </ bean >
然后在Service层模拟了一个事务回滚的method case:
@Transactional public boolean save(Person person) { for ( int id: new int []{ 2 , 3 }) { personDao.del(id); int j = 1 / 0 ; } return false ; }
本以为大功告成,在运行save方法时,由于1/0 抛出 java.lang.ArithmeticException: / by zero RuntimeException,导致事务回归。However,no way! So crazy~
查了下,发现Spring MVC对于事务配置比较讲究,需要额外的配置。解决办法如下:
需要在 applicationContext.xml增加:
< context:component-scan base-package = "com.will" > </ context:component-scan >
在 Spring mvc.dispatcher.xml增加:
< context:component-scan base-package = "com.will" > < context:exclude-filter type = "annotation" expression = "org.springframework.stereotype.Service" /> </ context:component-scan >
由于web.xml中配置:
< context-param > < param-name > contextConfigLocation </ param-name > < param-value > classpath:applicationContext.xml </ param-value > </ context-param > < listener > < listener-class > org.springframework.web.context.ContextLoaderListener </ listener-class > </ listener > < servlet > < servlet-name > dispatcher </ servlet-name > < servlet-class > org.springframework.web.servlet.DispatcherServlet </ servlet-class > < init-param > < param-name > contextConfigLocation </ param-name > < param-value > classpath*:/mvc_dispatcher_servlet.xml </ param-value > </ init-param > < load-on-startup > 1 </ load-on-startup > </ servlet > < servlet-mapping > < servlet-name > dispatcher </ servlet-name > < url-pattern > *.do </ url-pattern > </ servlet-mapping >
Spring容器优先加载由ServletContextListener(对应applicationContext.xml)产生的父容器,而SpringMVC(对应mvc_dispatcher_servlet.xml)产生的是子容器。子容器Controller进行扫描装配时装配的@Service注解的实例是没有经过事务加强处理,即没有事务处理能力的Service,而父容器进行初始化的Service是保证事务的增强处理能力的。如果不在子容器中将Service exclude掉,此时得到的将是原样的无事务处理能力的Service,
因为在多上下文的情况下,如果同一个bean被定义 两次 ,后面一个优先 。
( update 2014.05.27 今天看见一种说法:key word =双亲上下文。 不使用ContextLoaderListener监听器来加载spring的配置,改用DispatcherServlet来加载spring的配置,不要双亲上下文,只使用一个DispatcherServlet就不会出现上述问题。笔者这里未测过这个办法,因为我自己的业务需要一个extends ContextLoaderListener的selfListener,有兴趣的朋友可以自己测试 下这个说法,并欢迎把测试的结果与我交流 :) )
( update 2015.03.20 对于因为双亲上下文引起初始化二次,关键字:springmvc 初始化两次 )
经过如上配置,可以发现事务控制部分的日志如下:
2013-09-25 09:53:13,031 [http-8080-2] DEBUG [org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] - Adding transactional method 'save' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '' 2013-09-25 09:53:13,037 [http-8080-2] DEBUG [org.springframework.beans.factory.support.DefaultListableBeanFactory] - Returning cached instance of singleton bean 'txManager' 2013-09-25 09:53:13,050 [http-8080-2] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Creating new transaction with name [com.will.service.impl.PersonServiceImpl.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '' 2013-09-25 09:53:13,313 [http-8080-2] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Acquired Connection [jdbc:mysql://localhost:3306/mvc?useUnicode = true & characterEncoding = UTF -8, UserName = root @localhost, MySQL-AB JDBC Driver] for JDBC transaction 2013-09-25 09:53:13,323 [http-8080-2] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Switching JDBC Connection [jdbc:mysql://localhost:3306/mvc?useUnicode = true & characterEncoding = UTF -8, UserName = root @localhost, MySQL-AB JDBC Driver] to manual commit 2013-09-25 09:53:13,327 [http-8080-2] DEBUG [org.springframework.jdbc.core.JdbcTemplate] - Executing prepared SQL update 2013-09-25 09:53:13,328 [http-8080-2] DEBUG [org.springframework.jdbc.core.JdbcTemplate] - Executing prepared SQL statement [delete from person where id =?] 2013-09-25 09:53:13,348 [http-8080-2] DEBUG [org.springframework.jdbc.core.JdbcTemplate] - SQL update affected 1 rows 2013-09-25 09:53:13,363 [http-8080-2] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Initiating transaction rollback 2013-09-25 09:53:13,364 [http-8080-2] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [jdbc:mysql://localhost:3306/mvc?useUnicode = true & characterEncoding = UTF -8, UserName = root @localhost, MySQL-AB JDBC Driver] 2013-09-25 09:53:13,377 [http-8080-2] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Releasing JDBC Connection [jdbc:mysql://localhost:3306/mvc?useUnicode = true & characterEncoding = UTF -8, UserName = root @localhost, MySQL-AB JDBC Driver] after transaction 2013-09-25 09:53:13,378 [http-8080-2] DEBUG [org.springframework.jdbc.datasource.DataSourceUtils] - Returning JDBC Connection to DataSource
在2013-09-25 09:53:13,363处进行了rollback。