Spring事务管理高级应用难点剖析
 

2010-03-30 作者:陈雄华 来源:IBM

 

Spring最成功,最吸引人的地方莫过于轻量级的声明式事务管理,仅此一点,它就宣告了重量级EJB容器的覆灭。Spring声明式事务管理将开发者从繁复的事务管理代码中解脱出来,专注于业务逻辑的开发上,这是一件可以被拿来顶礼膜拜的事情。

但是,世界并未从此消停,开发人员需要面对的是层出不穷的应用场景,这些场景往往逾越了普通Spring技术书籍的理想界定。因此,随着应用开发的深入,在使用经过Spring层层封装的声明式事务时,开发人员越来越觉得自己坠入了迷雾,陷入了沼泽,体会不到外界所宣称的那种畅快淋漓。本系列文章的目标旨在整理并剖析实际应用中种种让我们迷茫的场景,让阳光照进云遮雾障的山头。

很少有使用Spring但不使用Spring事务管理器的应用,因此常常有人会问:是否用了Spring,就一定要用Spring事务管理器,否则就无法进行数据的持久化操作呢?事务管理器和DAO是什么关系呢?

也许是DAO和事务管理如影随行的缘故吧,这个看似简单的问题实实在在地存在着,从初学者心中涌出,萦绕在开发老手的脑际。答案当然是否定的!我们都知道:Spring事务管理是保证数据操作的事务性(即原子性、一致性、隔离性、持久性,也即所谓的ACID),脱离了事务性,DAO照样可以顺利地进行数据的操作。下面,我们来看一段使用SpringJDBC进行数据访问的代码:

清单1.UserJdbcWithoutTransManagerService.java

  1. packageuser.withouttm;  
  2.  
  3. importorg.springframework.beans.factory.annotation.Autowired;  
  4. importorg.springframework.jdbc.core.JdbcTemplate;  
  5. importorg.springframework.stereotype.Service;  
  6. importorg.springframework.context.ApplicationContext;  
  7. importorg.springframework.context.support.ClassPathXmlApplicationContext;  
  8. importorg.apache.commons.dbcp.BasicDataSource;  
  9.  
  10. @Service("service1")  
  11. publicclassUserJdbcWithoutTransManagerService{  
  12. @Autowired  
  13. privateJdbcTemplatejdbcTemplate;  
  14.  
  15. publicvoidaddScore(StringuserName,inttoAdd){  
  16. Stringsql="UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?";  
  17. jdbcTemplate.update(sql,toAdd,userName);  
  18. }  
  19.  
  20. publicstaticvoidmain(String[]args){  
  21. ApplicationContextctx=  
  22. newClassPathXmlApplicationContext("user/withouttm/jdbcWithoutTransManager.xml");  
  23. UserJdbcWithoutTransManagerServiceservice=  
  24. (UserJdbcWithoutTransManagerService)ctx.getBean("service1");  
  25. JdbcTemplatejdbcTemplate=(JdbcTemplate)ctx.getBean("jdbcTemplate");  
  26. BasicDataSourcebasicDataSource=(BasicDataSource)jdbcTemplate.getDataSource();  
  27.  
  28. //①.检查数据源autoCommit的设置  
  29. System.out.println("autoCommit:"+basicDataSource.getDefaultAutoCommit());  
  30.  
  31. //②.插入一条记录,初始分数为10  
  32. jdbcTemplate.execute(  
  33. "INSERTINTOt_user(user_name,password,score)VALUES('tom','123456',10)");  
  34.  
  35. //③.调用工作在无事务环境下的服务类方法,将分数添加20分  
  36. service.addScore("tom",20);  
  37.  
  38. //④.查看此时用户的分数  
  39. intscore=jdbcTemplate.queryForInt(  
  40. "SELECTscoreFROMt_userWHEREuser_name='tom'");  
  41. System.out.println("score:"+score);  
  42. jdbcTemplate.execute("DELETEFROMt_userWHEREuser_name='tom'");  
  43. }  

jdbcWithoutTransManager.xml的配置文件如下所示:

清单2.jdbcWithoutTransManager.xml

  1. <?xmlversionxmlversion="1.0"encoding="UTF-8"?> 
  2. <beansxmlnsbeansxmlns="http://www.springframework.org/schema/beans" 
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  4. xmlns:context="http://www.springframework.org/schema/context" 
  5. xmlns:p="http://www.springframework.org/schema/p" 
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7. http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
  8. http://www.springframework.org/schema/context  
  9. http://www.springframework.org/schema/context/spring-context-3.0.xsd"> 
  10. <context:component-scanbase-packagecontext:component-scanbase-package="user.withouttm"/> 
  11. <!--数据源默认将autoCommit设置为true--> 
  12. <beanidbeanid="dataSource" 
  13. class="org.apache.commons.dbcp.BasicDataSource" 
  14. destroy-method="close" 
  15. p:driverClassName="oracle.jdbc.driver.OracleDriver" 
  16. p:url="jdbc:oracle:thin:@localhost:1521:orcl" 
  17. p:username="test" 
  18. p:password="test"/> 
  19. <beanidbeanid="jdbcTemplate" 
  20. class="org.springframework.jdbc.core.JdbcTemplate" 
  21. p:dataSource-ref="dataSource"/> 
  22. </beans> 

运行UserJdbcWithoutTransManagerService,在控制台上打出如下的结果:

  1. defaultAutoCommit:true  
  2. score:30 

在jdbcWithoutTransManager.xml中,没有配置任何事务管理器,但是数据已经成功持久化到数据库中。在默认情况下,dataSource数据源的autoCommit被设置为true――这也意谓着所有通过JdbcTemplate执行的语句马上提交,没有事务。如果将dataSource的defaultAutoCommit设置为false,再次运行UserJdbcWithoutTransManagerService,将抛出错误,原因是新增及更改数据的操作都没有提交到数据库,所以④处的语句因无法从数据库中查询到匹配的记录而引发异常。

对于强调读速度的应用,数据库本身可能就不支持事务,如使用MyISAM引擎的MySQL数据库。这时,无须在Spring应用中配置事务管理器,因为即使配置了,也是没有实际用处的。

不过,对于Hibernate来说,情况就有点复杂了。因为Hibernate的事务管理拥有其自身的意义,它和Hibernate一级缓存有密切的关系:当我们调用Session的save、update等方法时,Hibernate并不直接向数据库发送SQL语句,而是在提交事务(commit)或flush一级缓存时才真正向数据库发送SQL。所以,即使底层数据库不支持事务,Hibernate的事务管理也是有一定好处的,不会对数据操作的效率造成负面影响。所以,如果是使用Hibernate数据访问技术,没有理由不配置HibernateTransactionManager事务管理器。但是,不使用Hibernate事务管理器,在Spring中,Hibernate照样也可以工作,来看下面的例子:

清单3.UserHibernateWithoutTransManagerService.java

  1. packageuser.withouttm;  
  2.  
  3. importorg.springframework.beans.factory.annotation.Autowired;  
  4. importorg.springframework.jdbc.core.JdbcTemplate;  
  5. importorg.springframework.stereotype.Service;  
  6. importorg.springframework.context.ApplicationContext;  
  7. importorg.springframework.context.support.ClassPathXmlApplicationContext;  
  8. importorg.springframework.orm.hibernate3.HibernateTemplate;  
  9. importorg.apache.commons.dbcp.BasicDataSource;  
  10. importuser.User;  
  11.  
  12. @Service("service2")  
  13. publicclassUserHibernateWithoutTransManagerService{  
  14. @Autowired  
  15. privateHibernateTemplatehibernateTemplate;  
  16.  
  17. publicvoidaddScore(StringuserName,inttoAdd){  
  18. Useruser=(User)hibernateTemplate.get(User.class,userName);  
  19. user.setScore(user.getScore()+toAdd);  
  20. hibernateTemplate.update(user);  
  21. }  
  22.  
  23. publicstaticvoidmain(String[]args){  
  24. //参考UserJdbcWithoutTransManagerService相应代码  
  25. …  
  26. }  

此时,采用hiberWithoutTransManager.xml的配置文件,其配置内容如下:

清单4.hiberWithoutTransManager.xml

  1. <?xmlversionxmlversion="1.0"encoding="UTF-8"?> 
  2. <beansxmlnsbeansxmlns="http://www.springframework.org/schema/beans" 
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  4. xmlns:context="http://www.springframework.org/schema/context" 
  5. xmlns:p="http://www.springframework.org/schema/p" 
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7. http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
  8. http://www.springframework.org/schema/context  
  9. http://www.springframework.org/schema/context/spring-context-3.0.xsd"> 
  10. <!--省略掉包扫描,数据源,JdbcTemplate配置部分,参见jdbcWithoutTransManager.xml--> 
  11. …  
  12. <beanidbeanid="sessionFactory" 
  13. class=  
  14. "org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" 
  15. p:dataSource-ref="dataSource"> 
  16. <propertynamepropertyname="annotatedClasses"> 
  17. <list> 
  18. <value>user.User</value> 
  19. </list> 
  20. </property> 
  21. <propertynamepropertyname="hibernateProperties"> 
  22. <props> 
  23. <propkeypropkey="hibernate.dialect"> 
  24. org.hibernate.dialect.Oracle10gDialect  
  25. </prop> 
  26. <propkeypropkey="hibernate.show_sql">true</prop> 
  27. </props> 
  28. </property> 
  29. </bean> 
  30.  
  31. <beanidbeanid="hibernateTemplate" 
  32. class="org.springframework.orm.hibernate3.HibernateTemplate" 
  33. p:sessionFactory-ref="sessionFactory"/> 
  34. </beans> 

运行UserHibernateWithoutTransManagerService,程序正确执行,并得到类似于UserJdbcWithoutTransManagerService的执行结果,这说明Hibernate在Spring中,在没有事务管理器的情况下,依然可以正常地进行数据的访问。

应用分层的迷惑

Web、Service及DAO三层划分就像西方国家的立法、行政、司法三权分立一样被奉为金科玉律,甚至有开发人员认为如果要使用Spring事务管理就一定先要进行三层的划分。这个看似荒唐的论调在开发人员中颇有市场。更有甚者,认为每层必须先定义一个接口,然后再定义一个实现类。其结果是:一个很简单的功能,也至少需要3个接口,3个类,再加上视图层的JSP和JS等,打牌都可以转上两桌了,这种误解贻害不浅。

对将“面向接口编程”奉为圭臬,认为放之四海而皆准的论调,笔者深不以为然。是的,“面向接口编程”是MartinFowler,RodJohnson这些大师提倡的行事原则。如果拿这条原则去开发架构,开发产品,怎么强调都不为过。但是,对于我们一般的开发人员来说,做的最多的是普通工程项目,往往最多的只是一些对数据库增、删、查、改的功能。此时,“面向接口编程”除了带来更多的类文件外,看不到更多其它的好处。

Spring框架提供的所有附加的好处(AOP、注解增强、注解MVC等)唯一的前提就是让POJO的类变成一个受Spring容器管理的Bean,除此以外没有其它任何的要求。下面的实例用一个POJO完成所有的功能,既是Controller,又是Service,还是DAO:

清单5.MixLayerUserService.java

  1. packageuser.mixlayer;  
  2. importorg.springframework.beans.factory.annotation.Autowired;  
  3. importorg.springframework.jdbc.core.JdbcTemplate;  
  4. importorg.springframework.stereotype.Controller;  
  5. importorg.springframework.web.bind.annotation.RequestMapping;  
  6. //①.将POJO类通过注解变成SpringMVC的Controller  
  7. @Controller  
  8. publicclassMixLayerUserService{  
  9.  
  10. //②.自动注入JdbcTemplate  
  11. @Autowired  
  12. privateJdbcTemplatejdbcTemplate;  
  13.  
  14. //③.通过SpringMVC注解映URL请求  
  15. @RequestMapping("/logon.do")  
  16. publicStringlogon(StringuserName,Stringpassword){  
  17. if(isRightUser(userName,password)){  
  18. Stringsql="UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?";  
  19. jdbcTemplate.update(sql,20,userName);  
  20. return"success";  
  21. }else{  
  22. return"fail";  
  23. }  
  24. }  
  25. privatebooleanisRightUser(StringuserName,Stringpassword){  
  26. //dosth...  
  27. returntrue;  
  28. }  

通过@Controller注解将MixLayerUserService变成Web层的Controller,同时也是Service层的服务类。此外,由于直接使用JdbcTemplate访问数据,所以MixLayerUserService还是一个DAO。来看一下对应的Spring配置文件:

清单6.applicationContext.xml

  1. <?xmlversionxmlversion="1.0"encoding="UTF-8"?> 
  2. <beansxmlnsbeansxmlns="http://www.springframework.org/schema/beans" 
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  4. xmlns:context="http://www.springframework.org/schema/context" 
  5. xmlns:p="http://www.springframework.org/schema/p" 
  6. xmlns:aop="http://www.springframework.org/schema/aop" 
  7. xmlns:tx="http://www.springframework.org/schema/tx" 
  8. xsi:schemaLocation="http://www.springframework.org/schema/beans  
  9. http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
  10. http://www.springframework.org/schema/context  
  11.  http://www.springframework.org/schema/context/spring-context-3.0.xsd  
  12.  http://www.springframework.org/schema/aop  
  13.  http://www.springframework.org/schema/aop/spring-aop-3.0.xsd  
  14.  http://www.springframework.org/schema/tx  
  15. http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> 
  16. <!--扫描Web类包,通过注释生成Bean--> 
  17. <context:component-scanbase-packagecontext:component-scanbase-package="user.mixlayer"/> 
  18. <!--①.启动SpringMVC的注解功能,完成请求和注解POJO的映射--> 
  19. <beanclassbeanclass="org.springframework.web.servlet.mvc.annotation  
  20.  .AnnotationMethodHandlerAdapter"/> 
  21.  
  22. <!--模型视图名称的解析,即在模型视图名称添加前后缀--> 
  23. <beanclassbeanclass="org.springframework.web.servlet.view  
  24.  .InternalResourceViewResolver"  
  25. pp:prefix="/WEB-INF/jsp/"p:suffix=".jsp"/> 
  26.  
  27. <!--普通数据源 --> 
  28. <beanidbeanid="dataSource" 
  29. class="org.apache.commons.dbcp.BasicDataSource" 
  30. destroy-method="close" 
  31. p:driverClassName="oracle.jdbc.driver.OracleDriver" 
  32. p:url="jdbc:oracle:thin:@localhost:1521:orcl" 
  33. p:username="test" 
  34. p:password="test"/> 
  35.  
  36. <beanidbeanid="jdbcTemplate" 
  37. class="org.springframework.jdbc.core.JdbcTemplate" 
  38. p:dataSource-ref="dataSource"/> 
  39.  
  40. <!--事务管理器--> 
  41. <beanidbeanid="jdbcManager" 
  42. class="org.springframework.jdbc.datasource.DataSourceTransactionManager" 
  43. p:dataSource-ref="dataSource"/> 
  44.  
  45. <!--②使用aop和tx命名空间语法为MixLayerUserService所有公用方法添加事务增强--> 
  46. <aop:configproxy-target-classaop:configproxy-target-class="true"> 
  47. <aop:pointcutidaop:pointcutid="serviceJdbcMethod" 
  48. expression="execution(public*user.mixlayer.MixLayerUserService.*(..))"/> 
  49. <aop:advisorpointcut-refaop:advisorpointcut-ref="serviceJdbcMethod" 
  50. advice-ref="jdbcAdvice"order="0"/> 
  51. </aop:config> 
  52. <tx:adviceidtx:adviceid="jdbcAdvice"transaction-manager="jdbcManager"> 
  53. <tx:attributes> 
  54. <tx:methodnametx:methodname="*"/> 
  55. </tx:attributes> 
  56. </tx:advice> 
  57. </beans> 

在①处,我们定义配置了AnnotationMethodHandlerAdapter,以便启用SpringMVC的注解驱动功能。而②和③处通过Spring的aop及tx命名空间,以及Aspject的切点表达式语法进行事务增强的定义,对MixLayerUserService的所有公有方法进行事务增强。要使程序能够运行起来还必须进行web.xml的相关配置:

清单7.web.xml

  1. <?xmlversionxmlversion="1.0"encoding="GB2312"?> 
  2. <web-appversionweb-appversion="2.4"xmlns="http://java.sun.com/xml/ns/j2ee"  
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  4. xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee  
  5. http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> 
  6. <context-param> 
  7. <param-name>contextConfigLocation</param-name> 
  8. <param-value>classpath*:user/mixlayer/applicationContext.xml</param-value> 
  9. </context-param> 
  10. <context-param> 
  11. <param-name>log4jConfigLocation</param-name> 
  12. <param-value>/WEB-INF/classes/log4j.properties</param-value> 
  13. </context-param> 
  14.  
  15. <listener> 
  16. <listener-class> 
  17. org.springframework.web.util.Log4jConfigListener  
  18. </listener-class> 
  19. </listener> 
  20. <listener> 
  21. <listener-class> 
  22. org.springframework.web.context.ContextLoaderListener  
  23. </listener-class> 
  24. </listener> 
  25.  
  26. <servlet> 
  27. <servlet-name>user</servlet-name> 
  28. <servlet-class> 
  29. org.springframework.web.servlet.DispatcherServlet  
  30. </servlet-class> 
  31. <!--①通过contextConfigLocation参数指定Spring配置文件的位置--> 
  32. <init-param> 
  33. <param-name>contextConfigLocation</param-name> 
  34. <param-value>classpath:user/mixlayer/applicationContext.xml</param-value> 
  35. </init-param> 
  36. <load-on-startup>1</load-on-startup> 
  37. </servlet> 
  38. <servlet-mapping> 
  39. <servlet-name>user</servlet-name> 
  40. <url-pattern>*.do</url-pattern> 
  41. </servlet-mapping> 
  42. </web-app> 

这个配置文件很简单,唯一需要注意的是DispatcherServlet的配置。默认情况下SpringMVC根据Servlet的名字查找WEB-INF下的<servletName>-servlet.xml作为SpringMVC的配置文件,在此,我们通过contextConfigLocation参数显式指定SpringMVC配置文件的确切位置。

将org.springframework.jdbc及org.springframework.transaction的日志级别设置为DEBUG,启动项目,并访问http://localhost:8088/logon.do?userName=tom应用,MixLayerUserService#logon方法将作出响应,查看后台输出日志:

清单8执行日志

  1. 13:24:22,625DEBUG(AbstractPlatformTransactionManager.java:365)-  
  2. Creatingnewtransactionwithname  
  3.  [user.mixlayer.MixLayerUserService.logon]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT  
  4. 13:24:22,906DEBUG(DataSourceTransactionManager.java:205)-  
  5. AcquiredConnection[org.apache.commons.dbcp.PoolableConnection@6e1cbf]  
  6.  forJDBCtransaction  
  7. 13:24:22,921DEBUG(DataSourceTransactionManager.java:222)-  
  8. SwitchingJDBCConnection  
  9.  [org.apache.commons.dbcp.PoolableConnection@6e1cbf]tomanualcommit  
  10. 13:24:22,921DEBUG(JdbcTemplate.java:785)-  
  11. ExecutingpreparedSQLupdate  
  12. 13:24:22,921DEBUG(JdbcTemplate.java:569)-  
  13. ExecutingpreparedSQLstatement  
  14.  [UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?]  
  15. 13:24:23,140DEBUG(JdbcTemplate.java:794)-  
  16. SQLupdateaffected0rows  
  17. 13:24:23,140DEBUG(AbstractPlatformTransactionManager.java:752)-  
  18. Initiatingtransactioncommit  
  19. 13:24:23,140DEBUG(DataSourceTransactionManager.java:265)-  
  20. CommittingJDBCtransactiononConnection  
  21.  [org.apache.commons.dbcp.PoolableConnection@6e1cbf]  
  22. 13:24:23,140DEBUG(DataSourceTransactionManager.java:323)-  
  23. ReleasingJDBCConnection[org.apache.commons.dbcp.PoolableConnection@6e1cbf]  
  24.  aftertransaction  
  25. 13:24:23,156DEBUG(DataSourceUtils.java:312)-  
  26. ReturningJDBCConnectiontoDataSource 

日志中粗体部分说明了MixLayerUserService#logon方法已经正确运行在事务上下文中。Spring框架本身不应该是复杂化代码的理由,使用Spring的开发者应该是无拘无束的:从实际应用出发,去除掉那些所谓原则性的接口,去除掉强制分层的束缚,简单才是硬道理。

事务方法嵌套调用的迷茫

Spring事务一个被讹传很广说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。其实这种是不认识Spring事务传播机制而造成的误解,Spring对事务控制的支持统一在TransactionDefinition类中描述,该类有以下几个重要的接口方法:

◆intgetPropagationBehavior():事务的传播行为;

◆intgetIsolationLevel():事务的隔离级别;

◆intgetTimeout():事务的过期时间;

◆booleanisReadOnly():事务的读写特性。

很明显,除了事务的传播行为外,事务的其它特性Spring是借助底层资源的功能来完成的,Spring无非只充当个代理的角色。但是事务的传播行为却是Spring凭借自身的框架提供的功能,是Spring提供给开发者最珍贵的礼物,讹传的说法玷污了Spring事务框架最美丽的光环。所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持7种事务传播行为:

◆PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

◆PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。

◆PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。

◆PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。

◆PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

◆PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。

◆PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

Spring默认的事务传播行为是PROPAGATION_REQUIRED,它适合于绝大多数的情况。假设ServiveX#methodX()都工作在事务环境下(即都被Spring事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中。

下面,我们来看一下实例,UserService#logon()方法内部调用了UserService#updateLastLogonTime()和ScoreService#addScore()方法,这两个类都继承于BaseService。它们之间的类结构说明如下:

图1.UserService和ScoreService

具体的代码如下所示:

清单9UserService.java

  1. @Service("userService")  
  2. publicclassUserServiceextendsBaseService{  
  3. @Autowired  
  4. privateJdbcTemplatejdbcTemplate;  
  5. @Autowired  
  6. privateScoreServicescoreService;  
  7.  
  8. publicvoidlogon(StringuserName){  
  9. updateLastLogonTime(userName);  
  10. scoreService.addScore(userName,20);  
  11. }  
  12.  
  13. publicvoidupdateLastLogonTime(StringuserName){  
  14. Stringsql="UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?";  
  15. jdbcTemplate.update(sql,System.currentTimeMillis(),userName);  
  16. }  

UserService中注入了ScoreService的Bean,ScoreService的代码如下所示:

清单10ScoreService.java

  1. @Service("scoreUserService")  
  2. publicclassScoreServiceextendsBaseService{  
  3. @Autowired  
  4. privateJdbcTemplatejdbcTemplate;  
  5. publicvoidaddScore(StringuserName,inttoAdd){  
  6. Stringsql="UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?";  
  7. jdbcTemplate.update(sql,toAdd,userName);  
  8. }  

通过Spring的事务配置为ScoreService及UserService中所有公有方法都添加事务增强,让这些方法都工作于事务环境下。下面是关键的配置代码:

清单11事务增强配置

  1. <!--添加Spring事务增强--> 
  2. <aop:configproxy-target-classaop:configproxy-target-class="true"> 
  3. <aop:pointcutidaop:pointcutid="serviceJdbcMethod" 
  4. <!--所有继承于BaseService类的子孙类的public方法都进行事务增强--> 
  5. expression="within(user.nestcall.BaseService+)"/> 
  6. <aop:advisorpointcut-refaop:advisorpointcut-ref="serviceJdbcMethod" 
  7. advice-ref="jdbcAdvice"order="0"/> 
  8. </aop:config> 
  9. <tx:adviceidtx:adviceid="jdbcAdvice"transaction-manager="jdbcManager"> 
  10. <tx:attributes> 
  11. <tx:methodnametx:methodname="*"/> 
  12. </tx:attributes> 
  13. </tx:advice> 

将日志级别设置为DEBUG,启动Spring容器并执行UserService#logon()的方法,仔细观察如下的输出日志:

清单12执行日志

  1. 16:25:04,765DEBUG(AbstractPlatformTransactionManager.java:365)-  
  2. Creatingnewtransactionwithname[user.nestcall.UserService.logon]:  
  3. PROPAGATION_REQUIRED,ISOLATION_DEFAULT①为UserService#logon方法启动一个事务  
  4. 16:25:04,765DEBUG(DataSourceTransactionManager.java:205)-  
  5. AcquiredConnection[org.apache.commons.dbcp.PoolableConnection@32bd65]  
  6. forJDBCtransaction  
  7. logonmethod...  
  8. updateLastLogonTime...②直接执行updateLastLogonTime方法  
  9. 16:25:04,781DEBUG(JdbcTemplate.java:785)-ExecutingpreparedSQLupdate  
  10. 16:25:04,781DEBUG(JdbcTemplate.java:569)-ExecutingpreparedSQLstatement  
  11. [UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?]  
  12. 16:25:04,828DEBUG(JdbcTemplate.java:794)-SQLupdateaffected0rows  
  13. 16:25:04,828DEBUG(AbstractPlatformTransactionManager.java:470)-Participating  
  14. inexistingtransaction③ScoreService#addScore方法加入到UserService#logon的事务中  
  15. addScore...  
  16. 16:25:04,828DEBUG(JdbcTemplate.java:785)-ExecutingpreparedSQLupdate  
  17. 16:25:04,828DEBUG(JdbcTemplate.java:569)-ExecutingpreparedSQLstatement  
  18. [UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?]  
  19. 16:25:04,828DEBUG(JdbcTemplate.java:794)-SQLupdateaffected0rows  
  20. 16:25:04,828DEBUG(AbstractPlatformTransactionManager.java:752)-  
  21. Initiatingtransactioncommit  
  22. 16:25:04,828DEBUG(DataSourceTransactionManager.java:265)-CommittingJDBCtransaction  
  23. onConnection[org.apache.commons.dbcp.PoolableConnection@32bd65]  
  24. 16:25:04,828DEBUG(DataSourceTransactionManager.java:323)-ReleasingJDBCConnection  
  25. [org.apache.commons.dbcp.PoolableConnection@32bd65]aftertransaction  
  26. 16:25:04,828DEBUG(DataSourceUtils.java:312)-ReturningJDBCConnectiontoDataSource  

从上面的输入日志中,可以清楚地看到Spring为UserService#logon()方法启动了一个新的事务,而UserSerive#updateLastLogonTime()和UserService#logon()是在相同的类中,没有观察到有事务传播行为的发生,其代码块好像“直接合并”到UserService#logon()中。接着,当执行到ScoreService#addScore()方法时,我们就观察到了发生了事务传播的行为:Participatinginexistingtransaction,这说明ScoreService#addScore()添加到UserService#logon()的事务上下文中,两者共享同一个事务。所以最终的结果是UserService的logon(),updateLastLogonTime()以及ScoreService的addScore都工作于同一事务中。

多线程的困惑

由于Spring事务管理器是通过线程相关的ThreadLocal来保存数据访问基础设施,再结合IOC和AOP实现高级声明式事务的功能,所以Spring的事务天然地和线程有着千丝万缕的联系。

我们知道Web容器本身就是多线程的,Web容器为一个Http请求创建一个独立的线程,所以由此请求所牵涉到的Spring容器中的Bean也是运行于多线程的环境下。在绝大多数情况下,Spring的Bean都是单实例的(singleton),单实例Bean的最大的好处是线程无关性,不存在多线程并发访问的问题,也即是线程安全的。一个类能够以单实例的方式运行的前提是“无状态”:即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO必须执有一个Connection,而Connection即是状态化的对象。所以传统的DAO不能做成单实例的,每次要用时都必须new一个新的实例。传统的Service由于将有状态的DAO作为成员变量,所以传统的Service本身也是有状态的。

但是在Spring中,DAO和Service都以单实例的方式存在。Spring是通过ThreadLocal将有状态的变量(如Connection等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring不遗余力地将状态化的对象无状态化,就是要达到单实例化Bean的目的。由于Spring已经通过ThreadLocal的设施将Bean无状态化,所以Spring中单实例Bean对线程安全问题拥有了一种天生的免疫能力。不但单实例的Service可以成功运行于多线程环境中,Service本身还可以自由地启动独立线程以执行其它的Service。下面,通过一个实例对此进行描述:

清单13UserService.java在事务方法中启动独立线程运行另一个事务方法

  1. @Service("userService")  
  2. publicclassUserServiceextendsBaseService{  
  3. @Autowired  
  4. privateJdbcTemplatejdbcTemplate;  
  5.  
  6. @Autowired  
  7. privateScoreServicescoreService;  
  8. //①在logon方法体中启动一个独立的线程,在该独立的线程中执行ScoreService#addScore()方法  
  9. publicvoidlogon(StringuserName){  
  10. System.out.println("logonmethod...");  
  11. updateLastLogonTime(userName);  
  12. ThreadmyThread=newMyThread(this.scoreService,userName,20);  
  13. myThread.start();  
  14. }  
  15.  
  16. publicvoidupdateLastLogonTime(StringuserName){  
  17. System.out.println("updateLastLogonTime...");  
  18. Stringsql="UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?";  
  19. jdbcTemplate.update(sql,System.currentTimeMillis(),userName);  
  20. }  
  21. //②封装ScoreService#addScore()的线程  
  22. privateclassMyThreadextendsThread{  
  23. privateScoreServicescoreService;  
  24. privateStringuserName;  
  25. privateinttoAdd;  
  26. privateMyThread(ScoreServicescoreService,StringuserName,inttoAdd){  
  27. this.scoreService=scoreService;  
  28. this.userName=userName;  
  29. this.toAdd=toAdd;  
  30. }  
  31. publicvoidrun(){  
  32. scoreService.addScore(userName,toAdd);  
  33. }  
  34. }  

将日志级别设置为DEBUG,执行UserService#logon()方法,观察以下输出的日志:

清单14执行日志

  1. [main](AbstractPlatformTransactionManager.java:365)-Creatingnewtransactionwithname  
  2. [user.multithread.UserService.logon]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT①  
  3.  
  4. [main](DataSourceTransactionManager.java:205)-AcquiredConnection  
  5. [org.apache.commons.dbcp.PoolableConnection@1353249]forJDBCtransaction  
  6.  
  7. logonmethod...  
  8.  
  9. updateLastLogonTime...  
  10.  
  11. [main](JdbcTemplate.java:785)-ExecutingpreparedSQLupdate  
  12. [main](JdbcTemplate.java:569)-ExecutingpreparedSQLstatement  
  13. [UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?]  
  14. [main](JdbcTemplate.java:794)-SQLupdateaffected0rows  
  15. [main](AbstractPlatformTransactionManager.java:752)-Initiatingtransactioncommit  
  16.  
  17. [Thread-2](AbstractPlatformTransactionManager.java:365)-  
  18. Creatingnewtransactionwithname[user.multithread.ScoreService.addScore]:  
  19. PROPAGATION_REQUIRED,ISOLATION_DEFAULT②  
  20. [main](DataSourceTransactionManager.java:265)-CommittingJDBCtransaction  
  21. onConnection[org.apache.commons.dbcp.PoolableConnection@1353249]③  
  22.  
  23. [main](DataSourceTransactionManager.java:323)-ReleasingJDBCConnection  
  24. [org.apache.commons.dbcp.PoolableConnection@1353249]aftertransaction  
  25. [main](DataSourceUtils.java:312)-ReturningJDBCConnectiontoDataSource  
  26.  
  27. [Thread-2](DataSourceTransactionManager.java:205)-AcquiredConnection  
  28. [org.apache.commons.dbcp.PoolableConnection@10dc656]forJDBCtransaction  
  29.  
  30. addScore...  
  31.  
  32. [main](JdbcTemplate.java:416)-ExecutingSQLstatement  
  33. [DELETEFROMt_userWHEREuser_name='tom']  
  34. [main](DataSourceUtils.java:112)-FetchingJDBCConnectionfromDataSource  
  35. [Thread-2](JdbcTemplate.java:785)-ExecutingpreparedSQLupdate  
  36. [Thread-2](JdbcTemplate.java:569)-ExecutingpreparedSQLstatement  
  37. [UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?]  
  38. [main](DataSourceUtils.java:312)-ReturningJDBCConnectiontoDataSource  
  39. [Thread-2](JdbcTemplate.java:794)-SQLupdateaffected0rows  
  40. [Thread-2](AbstractPlatformTransactionManager.java:752)-Initiatingtransactioncommit  
  41. [Thread-2](DataSourceTransactionManager.java:265)-CommittingJDBCtransaction  
  42. onConnection[org.apache.commons.dbcp.PoolableConnection@10dc656]④  
  43. [Thread-2](DataSourceTransactionManager.java:323)-ReleasingJDBCConnection  
  44. [org.apache.commons.dbcp.PoolableConnection@10dc656]aftertransaction 

在①处,在主线程(main)执行的UserService#logon()方法的事务启动,在③处,其对应的事务提交,而在子线程(Thread-2)执行的ScoreService#addScore()方法的事务在②处启动,在④处对应的事务提交。

所以,我们可以得出这样的结论:在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,不同线程下的事务方法工作在独立的事务中。

小结

Spring声明式事务是Spring最核心,最常用的功能。由于Spring通过IOC和AOP的功能非常透明地实现了声明式事务的功能,一般的开发者基本上无须了解Spring声明式事务的内部细节,仅需要懂得如何配置就可以了。

但是在实际应用开发过程中,Spring的这种透明的高阶封装在带来便利的同时,也给我们带来了迷惑。就像通过流言传播的消息,最终听众已经不清楚事情的真相了,而这对于应用开发来说是很危险的。本系列文章通过剖析实际应用中给开发者造成迷惑的各种难点,通过分析Spring事务管理的内部运作机制将真相还原出来。在本文中,我们通过剖析了解到以下的真相:

◆在没有事务管理的情况下,DAO照样可以顺利进行数据操作;

◆将应用分成Web,Service及DAO层只是一种参考的开发模式,并非是事务管理工作的前提条件;

◆Spring通过事务传播机制可以很好地应对事务方法嵌套调用的情况,开发者无须为了事务管理而刻意改变服务方法的设计;

◆由于单实例的对象不存在线程安全问题,所以进行事务管理增强的Bean可以很好地工作在多线程环境下。

在下一篇文章中,笔者将继续分析Spring事务管理的以下难点:

◆混合使用多种数据访问技术(如SpringJDBC+Hibernate)的事务管理问题;

◆在通过Bean的方法通过SpringAOP增强存在哪些特殊的情况。



Java 中的中文编码问题
Java基础知识的三十个经典问答
玩转 Java Web 应用开发
使用Spring更好地处理Struts
用Eclipse开发iPhone Web应用
插件系统框架分析
更多...   


Struts+Spring+Hibernate
基于J2EE的Web 2.0应用开发
J2EE设计模式和性能调优
Java EE 5企业级架构设计
Java单元测试方法与技术
Java编程方法与技术


Struts+Spring+Hibernate/EJB+性能优化
华夏基金 ActiveMQ 原理与管理
某民航公司 Java基础编程到应用开发
某风电公司 Java 应用开发平台与迁移
日照港 J2EE应用开发技术框架与实践
某跨国公司 工作流管理JBPM
东方航空公司 高级J2EE及其前沿技术
更多...   
 
 
 
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号