转载

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

标签: spring 管理 应用 | 发表时间:2015-12-13 04:46 | 作者:m635674608

分享到:
出处:http://www.iteye.com

第 1 部分

http://www.ibm.com/search/csass/search/?q=%E4%BA%8B%E5%8A%A1&sn=dw&lang=zh&cc=CN&en=utf&hpp=20&dws=cndw&lo=zh

概述

Spring 最成功,最吸引人的地方莫过于轻量级的声明式事务管理,仅此一点,它就宣告了重量级 EJB 容器的覆灭。Spring 声明式事务管理将开发者从繁复的事务管理代码中解脱出来,专注于业务逻辑的开发上,这是一件可以被拿来顶礼膜拜的事情。但是,世界并未从此消停,开发人员需要面对的是层出不穷的应用场景,这些场景往往逾越了普通 Spring 技术书籍的理想界定。因此,随着应用开发的深入,在使用经过 Spring 层层封装的声明式事务时,开发人员越来越觉得自己坠入了迷雾,陷入了沼泽,体会不到外界所宣称的那种畅快淋漓。本系列文章的目标旨在整理并剖析实际应用中种种让我们迷茫的场景,让阳光照进云遮雾障的山头。

回页首

DAO 和事务管理的牵绊

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

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

下面,我们来看一段使用 Spring JDBC 进行数据访问的代码:

清单 1. UserJdbcWithoutTransManagerService.java

package user.withouttm;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.apache.commons.dbcp.BasicDataSource;  @Service("service1") public class UserJdbcWithoutTransManagerService {     @Autowired     private JdbcTemplate jdbcTemplate;      public void addScore(String userName,int toAdd){         String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";         jdbcTemplate.update(sql,toAdd,userName);     }      public static void main(String[] args) {         ApplicationContext ctx =          new ClassPathXmlApplicationContext("user/withouttm/jdbcWithoutTransManager.xml");         UserJdbcWithoutTransManagerService service =              (UserJdbcWithoutTransManagerService)ctx.getBean("service1");         JdbcTemplate jdbcTemplate = (JdbcTemplate)ctx.getBean("jdbcTemplate");         BasicDataSource basicDataSource = (BasicDataSource)jdbcTemplate.getDataSource();          //①.检查数据源autoCommit的设置         System.out.println("autoCommit:"+ basicDataSource.getDefaultAutoCommit());          //②.插入一条记录,初始分数为10         jdbcTemplate.execute(         "INSERT INTO t_user(user_name,password,score) VALUES('tom','123456',10)");          //③.调用工作在无事务环境下的服务类方法,将分数添加20分         service.addScore("tom",20);           //④.查看此时用户的分数         int score = jdbcTemplate.queryForInt(         "SELECT score FROM t_user WHERE user_name ='tom'");         System.out.println("score:"+score);         jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");     } }

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

清单 2. jdbcWithoutTransManager.xml

<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns:context="http://www.springframework.org/schema/context"        xmlns:p="http://www.springframework.org/schema/p"        xsi:schemaLocation="http://www.springframework.org/schema/beans          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd         http://www.springframework.org/schema/context          http://www.springframework.org/schema/context/spring-context-3.0.xsd">     <context:component-scan base-package="user.withouttm"/>      <!-- 数据源默认将autoCommit设置为true -->     <bean id="dataSource"         class="org.apache.commons.dbcp.BasicDataSource"         destroy-method="close"         p:driverClassName="oracle.jdbc.driver.OracleDriver"         p:url="jdbc:oracle:thin:@localhost:1521:orcl"         p:username="test"         p:password="test"/>      <bean id="jdbcTemplate"         class="org.springframework.jdbc.core.JdbcTemplate"         p:dataSource-ref="dataSource"/> </beans>

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

defaultAutoCommit:true  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

package user.withouttm;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.orm.hibernate3.HibernateTemplate; import org.apache.commons.dbcp.BasicDataSource; import user.User;  @Service("service2") public class UserHibernateWithoutTransManagerService {     @Autowired     private HibernateTemplate hibernateTemplate;      public void addScore(String userName,int toAdd){         User user = (User)hibernateTemplate.get(User.class,userName);         user.setScore(user.getScore()+toAdd);         hibernateTemplate.update(user);     }      public static void main(String[] args) {         //参考UserJdbcWithoutTransManagerService相应代码         …     } }

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

清单 4.hiberWithoutTransManager.xml

<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xmlns:context="http://www.springframework.org/schema/context"     xmlns:p="http://www.springframework.org/schema/p"     xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd         http://www.springframework.org/schema/context   http://www.springframework.org/schema/context/spring-context-3.0.xsd">     <!--省略掉包扫描,数据源,JdbcTemplate配置部分,参见jdbcWithoutTransManager.xml -->     …      <bean id="sessionFactory"         class=             "org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"             p:dataSource-ref="dataSource">         <property name="annotatedClasses">             <list>                 <value>user.User</value>             </list>         </property>         <property name="hibernateProperties">             <props>                 <prop key="hibernate.dialect">                     org.hibernate.dialect.Oracle10gDialect                 </prop>                 <prop key="hibernate.show_sql">true</prop>             </props>         </property>     </bean>      <bean id="hibernateTemplate"           class="org.springframework.orm.hibernate3.HibernateTemplate"           p:sessionFactory-ref="sessionFactory"/> </beans>

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

回页首

应用分层的迷惑

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

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

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

清单 5. MixLayerUserService.java

package user.mixlayer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; //①.将POJO类通过注解变成Spring MVC的Controller @Controller public class MixLayerUserService {      //②.自动注入JdbcTemplate     @Autowired     private JdbcTemplate jdbcTemplate;          //③.通过Spring MVC注解映URL请求     @RequestMapping("/logon.do")         public String logon(String userName,String password){         if(isRightUser(userName,password)){             String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";             jdbcTemplate.update(sql,20,userName);             return "success";         }else{             return "fail";         }     }     private boolean isRightUser(String userName,String password){         //do sth...         return true;     } }

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

清单 6.applicationContext.xml

<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xmlns:context="http://www.springframework.org/schema/context"     xmlns:p="http://www.springframework.org/schema/p"      xmlns:aop="http://www.springframework.org/schema/aop"     xmlns:tx="http://www.springframework.org/schema/tx"     xsi:schemaLocation="http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/context   http://www.springframework.org/schema/context/spring-context-3.0.xsd   http://www.springframework.org/schema/aop   http://www.springframework.org/schema/aop/spring-aop-3.0.xsd   http://www.springframework.org/schema/tx      http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">     <!--扫描Web类包,通过注释生成Bean-->     <context:component-scan base-package="user.mixlayer"/>     <!--①.启动Spring MVC的注解功能,完成请求和注解POJO的映射-->     <bean class="org.springframework.web.servlet.mvc.annotation    .AnnotationMethodHandlerAdapter"/>      <!--模型视图名称的解析,即在模型视图名称添加前后缀 -->     <bean class="org.springframework.web.servlet.view    .InternalResourceViewResolver"          p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>      <!--普通数据源 -->     <bean id="dataSource"         class="org.apache.commons.dbcp.BasicDataSource"         destroy-method="close"         p:driverClassName="oracle.jdbc.driver.OracleDriver"         p:url="jdbc:oracle:thin:@localhost:1521:orcl"         p:username="test"         p:password="test"/>      <bean id="jdbcTemplate"           class="org.springframework.jdbc.core.JdbcTemplate"           p:dataSource-ref="dataSource"/>      <!--事务管理器 -->     <bean id="jdbcManager"         class="org.springframework.jdbc.datasource.DataSourceTransactionManager"         p:dataSource-ref="dataSource"/>          <!--②使用aop和tx命名空间语法为MixLayerUserService所有公用方法添加事务增强 -->     <aop:config proxy-target-class="true">         <aop:pointcut id="serviceJdbcMethod"             expression="execution(public * user.mixlayer.MixLayerUserService.*(..))"/>         <aop:advisor pointcut-ref="serviceJdbcMethod"              advice-ref="jdbcAdvice" order="0"/>     </aop:config>     <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">         <tx:attributes>             <tx:method name="*"/>         </tx:attributes>     </tx:advice> </beans>

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

清单 7.web.xml

<?xml version="1.0" encoding="GB2312"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee     http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">     <context-param>         <param-name>contextConfigLocation</param-name>         <param-value>classpath*:user/mixlayer/applicationContext.xml</param-value>     </context-param>     <context-param>         <param-name>log4jConfigLocation</param-name>         <param-value>/WEB-INF/classes/log4j.properties</param-value>     </context-param>      <listener>         <listener-class>             org.springframework.web.util.Log4jConfigListener         </listener-class>     </listener>     <listener>         <listener-class>             org.springframework.web.context.ContextLoaderListener         </listener-class>     </listener>      <servlet>         <servlet-name>user</servlet-name>         <servlet-class>             org.springframework.web.servlet.DispatcherServlet         </servlet-class>         <!--①通过contextConfigLocation参数指定Spring配置文件的位置 -->         <init-param>             <param-name>contextConfigLocation</param-name>             <param-value>classpath:user/mixlayer/applicationContext.xml</param-value>         </init-param>         <load-on-startup>1</load-on-startup>     </servlet>     <servlet-mapping>         <servlet-name>user</servlet-name>         <url-pattern>*.do</url-pattern>     </servlet-mapping> </web-app>

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

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

清单 8 执行日志

13:24:22,625 DEBUG (AbstractPlatformTransactionManager.java:365) -      Creating new transaction with name   [user.mixlayer.MixLayerUserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT 13:24:22,906 DEBUG (DataSourceTransactionManager.java:205) -      Acquired Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf]   for JDBC transaction 13:24:22,921 DEBUG (DataSourceTransactionManager.java:222) -      Switching JDBC Connection   [org.apache.commons.dbcp.PoolableConnection@6e1cbf] to manual commit 13:24:22,921 DEBUG (JdbcTemplate.java:785) -      Executing prepared SQL update 13:24:22,921 DEBUG (JdbcTemplate.java:569) -      Executing prepared SQL statement   [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?] 13:24:23,140 DEBUG (JdbcTemplate.java:794) -      SQL update affected 0 rows 13:24:23,140 DEBUG (AbstractPlatformTransactionManager.java:752) -      Initiating transaction commit 13:24:23,140 DEBUG (DataSourceTransactionManager.java:265) -      Committing JDBC transaction on Connection   [org.apache.commons.dbcp.PoolableConnection@6e1cbf] 13:24:23,140 DEBUG (DataSourceTransactionManager.java:323) -      Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf]   after transaction 13:24:23,156 DEBUG (DataSourceUtils.java:312) -      Returning JDBC Connection to DataSource

日志中粗体部分说明了 MixLayerUserService#logon 方法已经正确运行在事务上下文中。

Spring 框架本身不应该是复杂化代码的理由,使用 Spring 的开发者应该是无拘无束的:从实际应用出发,去除掉那些所谓原则性的接口,去除掉强制分层的束缚,简单才是硬道理。

回页首

事务方法嵌套调用的迷茫

Spring 事务一个被讹传很广说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。

其实这种是不认识 Spring 事务传播机制而造成的误解,Spring 对事务控制的支持统一在 TransactionDefinition 类中描述,该类有以下几个重要的接口方法:

  • int getPropagationBehavior():事务的传播行为
  • int getIsolationLevel():事务的隔离级别
  • int getTimeout():事务的过期时间
  • boolean isReadOnly():事务的读写特性。

很明显,除了事务的传播行为外,事务的其它特性 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

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

具体的代码如下所示:

清单 9 UserService.java

@Service("userService") public class UserService extends BaseService {     @Autowired     private JdbcTemplate jdbcTemplate;     @Autowired     private ScoreService scoreService;      public void logon(String userName) {         updateLastLogonTime(userName);         scoreService.addScore(userName, 20);     }      public void updateLastLogonTime(String userName) {         String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";         jdbcTemplate.update(sql, System.currentTimeMillis(), userName);     } }

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

清单 10 ScoreService.java

@Service("scoreUserService") public class ScoreService extends BaseService{     @Autowired     private JdbcTemplate jdbcTemplate;     public void addScore(String userName, int toAdd) {         String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";         jdbcTemplate.update(sql, toAdd, userName);     } }

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

清单 11 事务增强配置

<!-- 添加Spring事务增强 --> <aop:config proxy-target-class="true">     <aop:pointcut id="serviceJdbcMethod"         <!-- 所有继承于BaseService类的子孙类的public方法都进行事务增强-->         expression="within(user.nestcall.BaseService+)"/>     <aop:advisor pointcut-ref="serviceJdbcMethod"          advice-ref="jdbcAdvice" order="0"/> </aop:config> <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">     <tx:attributes>         <tx:method name="*"/>     </tx:attributes> </tx:advice>

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

清单 12 执行日志

16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) -      Creating new transaction with name [user.nestcall.UserService.logon]:   PROPAGATION_REQUIRED,ISOLATION_DEFAULT  ①为UserService#logon方法启动一个事务  16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) -      Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]   for JDBC transaction  logon method...  updateLastLogonTime... ②直接执行updateLastLogonTime方法  16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update  16:25:04,781 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement      [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]  16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows  16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:470) - Participating      in existing transaction   ③ScoreService#addScore方法加入到UserService#logon的事务中  addScore...  16:25:04,828 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update  16:25:04,828 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement      [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]  16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows  16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) -      Initiating transaction commit  ④提交事务  16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transaction     on Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]  16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection      [org.apache.commons.dbcp.PoolableConnection@32bd65] after transaction  16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

从上面的输入日志中,可以清楚地看到 Spring 为 UserService#logon() 方法启动了一个新的事务,而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的类中,没有观察到有事务传播行为的发生,其代码块好像“直接合并”到 UserService#logon() 中。接着,当执行到 ScoreService#addScore() 方法时,我们就观察到了发生了事务传播的行为:Participating in existing transaction,这说明 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。下面,通过一个实例对此进行描述:

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

@Service("userService") public class UserService extends BaseService {     @Autowired     private JdbcTemplate jdbcTemplate;      @Autowired     private ScoreService scoreService;     //① 在logon方法体中启动一个独立的线程,在该独立的线程中执行ScoreService#addScore()方法     public void logon(String userName) {         System.out.println("logon method...");         updateLastLogonTime(userName);         Thread myThread = new MyThread(this.scoreService,userName,20);         myThread.start();     }      public void updateLastLogonTime(String userName) {         System.out.println("updateLastLogonTime...");         String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";         jdbcTemplate.update(sql, System.currentTimeMillis(), userName);     }     //② 封装ScoreService#addScore()的线程     private class MyThread extends Thread{         private ScoreService scoreService;         private String userName;         private int toAdd;         private MyThread(ScoreService scoreService,String userName,int toAdd) {             this.scoreService = scoreService;             this.userName = userName;             this.toAdd = toAdd;         }         public void run() {             scoreService.addScore(userName,toAdd);         }     } }

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

清单 14 执行日志

[main] (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name     [user.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①  [main] (DataSourceTransactionManager.java:205) - Acquired Connection      [org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC transaction  logon method...  updateLastLogonTime...  [main] (JdbcTemplate.java:785) - Executing prepared SQL update  [main] (JdbcTemplate.java:569) - Executing prepared SQL statement      [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]  [main] (JdbcTemplate.java:794) - SQL update affected 0 rows  [main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit  [Thread-2](AbstractPlatformTransactionManager.java:365) -      Creating new transaction with name [user.multithread.ScoreService.addScore]:   PROPAGATION_REQUIRED,ISOLATION_DEFAULT ②  [main] (DataSourceTransactionManager.java:265) - Committing JDBC transaction     on Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③  [main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection      [org.apache.commons.dbcp.PoolableConnection@1353249] after transaction  [main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource  [Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection      [org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC transaction  addScore...  [main] (JdbcTemplate.java:416) - Executing SQL statement      [DELETE FROM t_user WHERE user_name='tom']  [main] (DataSourceUtils.java:112) - Fetching JDBC Connection from DataSource  [Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update  [Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement      [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]  [main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource  [Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows  [Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit  [Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC transaction      on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④  [Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection      [org.apache.commons.dbcp.PoolableConnection@10dc656] after transaction

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

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

回页首

小结

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

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

在本文中,我们通过剖析了解到以下的真相:

  • 在没有事务管理的情况下,DAO 照样可以顺利进行数据操作;
  • 将应用分成 Web,Service 及 DAO 层只是一种参考的开发模式,并非是事务管理工作的前提条件;
  • Spring 通过事务传播机制可以很好地应对事务方法嵌套调用的情况,开发者无须为了事务管理而刻意改变服务方法的设计;
  • 由于单实例的对象不存在线程安全问题,所以进行事务管理增强的 Bean 可以很好地工作在多线程环境下。

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

  • 混合使用多种数据访问技术(如 Spring JDBC + Hibernate)的事务管理问题;
  • 进行 Spring AOP 增强的 Bean 存在哪些特殊的情况。

第 2 部分

http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts2/

联合军种作战的混乱

Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色,各有千秋。像 Hibernate 是非常优秀的 ORM 实现方案,但对底层 SQL 的控制不太方便;而 iBatis 则通过模板化技术让您方便地控制 SQL,但没有 Hibernate 那样高的开发效率;自由度最高的当然是直接使用 Spring JDBC 莫属了,但是它也是最底层的,灵活的代价是代码的繁复。很难说哪种数据访问技术是最优秀的,只有在某种特定的场景下,才能给出答案。所以在一个应用中,往往采用多个数据访问技术:一般是两种,一种采用 ORM 技术框架,而另一种采用偏 JDBC 的底层技术,两者珠联璧合,形成联合军种,共同御敌。

但是,这种联合军种如何应对事务管理的问题呢?我们知道 Spring 为每种数据访问技术提供了相应的事务管理器,难道需要分别为它们配置对应的事务管理器吗?它们到底是如何协作,如何工作的呢?这些层出不穷的问题往往压制了开发人员使用联合军种的想法。

其实,在这个问题上,我们低估了 Spring 事务管理的能力。如果您采用了一个高端 ORM 技术(Hibernate,JPA,JDO),同时采用一个 JDBC 技术(Spring JDBC,iBatis),由于前者的会话(Session)是对后者连接(Connection)的封装,Spring 会“足够智能地”在同一个事务线程让前者的会话封装后者的连接。所以,我们只要直接采用前者的事务管理器就可以了。下表给出了混合数据访问技术所对应的事务管理器:

表 1. 混合数据访问技术的事务管理器

混合数据访问技术事务管理器
ORM 技术框架 JDBC 技术框架
Hibernate Spring JDBC 或 iBatis HibernateTransactionManager
JPA Spring JDBC 或 iBatis JpaTransactionManager
JDO Spring JDBC 或 iBatis JdoTransactionManager

由于一般不会出现同时使用多个 ORM 框架的情况(如 Hibernate + JPA),我们不拟对此命题展开论述,只重点研究 ORM 框架 + JDBC 框架的情况。Hibernate + Spring JDBC 可能是被使用得最多的组合,下面我们通过实例观察事务管理的运作情况。

清单 1.User.java:使用了注解声明的实体类

import javax.persistence.Entity;  import javax.persistence.Table;  import javax.persistence.Column;  import javax.persistence.Id;  import java.io.Serializable;   @Entity  @Table(name="T_USER")  public class User implements Serializable{      @Id     @Column(name = "USER_NAME")      private String userName;      private String password;      private int score;        @Column(name = "LAST_LOGON_TIME")     private long lastLogonTime = 0;   }

再来看下 UserService 的关键代码:

清单 2.UserService.java:使用 Hibernate 数据访问技术

package user.mixdao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.stereotype.Service; import org.springframework.orm.hibernate3.HibernateTemplate; import org.apache.commons.dbcp.BasicDataSource; import user.User;  @Service("userService") public class UserService extends BaseService {     @Autowired     private HibernateTemplate hibernateTemplate;      @Autowired     private ScoreService scoreService;      public void logon(String userName) {         System.out.println("logon method...");         updateLastLogonTime(userName); //①使用Hibernate数据访问技术         scoreService.addScore(userName, 20); //②使用Spring JDBC数据访问技术     }      public void updateLastLogonTime(String userName) {         System.out.println("updateLastLogonTime...");         User user = hibernateTemplate.get(User.class,userName);         user.setLastLogonTime(System.currentTimeMillis());         hibernateTemplate.flush(); //③请看下文的分析     } }

在①处,使用 Hibernate 操作数据,而在②处调用 ScoreService#addScore(),该方法内部使用 Spring JDBC 操作数据。

在③处,我们显式调用了 flush() 方法,将 Session 中的缓存同步到数据库中,这个操作将即时向数据库发送一条更新记录的 SQL 语句。之所以要在此显式执行 flush() 方法,原因是:默认情况下,Hibernate 要在事务提交时才将数据的更改同步到数据库中,而事务提交发生在 logon() 方法返回前。如果所有针对数据库的更改都使用 Hibernate,这种数据同步延迟的机制不会产生任何问题。但是,我们在 logon() 方法中同时采用了 Hibernate 和 Spring JDBC 混合数据访问技术。Spring JDBC 无法自动感知 Hibernate 一级缓存,所以如果不及时调用 flush() 方法将数据更改同步到数据库,则②处通过 Spring JDBC 进行数据更改的结果将被 Hibernate 一级缓存中的更改覆盖掉,因为,一级缓存在 logon() 方法返回前才同步到数据库!

ScoreService 使用 Spring JDBC 数据访问技术,其代码如下:

清单 3.ScoreService.java:使用 Spring JDBC 数据访问技术

package user.mixdao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.apache.commons.dbcp.BasicDataSource;  @Service("scoreUserService") public class ScoreService extends BaseService{     @Autowired     private JdbcTemplate jdbcTemplate;     public void addScore(String userName, int toAdd) {         System.out.println("addScore...");         String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";         jdbcTemplate.update(sql, toAdd, userName);         //① 查看此处数据库激活的连接数         BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();         System.out.println("激活连接数量:"+basicDataSource.getNumActive());     } }

Spring 关键的配置文件代码如下所示:

清单 4. applicationContext.xml 事务配置代码部分

<!-- 使用Hibernate事务管理器 --> <bean id="hiberManager"     class="org.springframework.orm.hibernate3.HibernateTransactionManager"     p:sessionFactory-ref="sessionFactory"/>      <!-- 对所有继承BaseService类的公用方法实施事务增强 --> <aop:config proxy-target-class="true">     <aop:pointcut id="serviceJdbcMethod"         expression="within(user.mixdao.BaseService+)"/>     <aop:advisor pointcut-ref="serviceJdbcMethod"         advice-ref="hiberAdvice"/> </aop:config>      <tx:advice id="hiberAdvice" transaction-manager="hiberManager">     <tx:attributes>         <tx:method name="*"/>     </tx:attributes> </tx:advice>

启动 Spring 容器,执行 UserService#logon() 方法,可以查看到如下的执行日志:

清单 5. 代码运行日志

12:38:57,062  (AbstractPlatformTransactionManager.java:365) - Creating new transaction      with name [user.mixdao.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT  12:38:57,093  (SessionImpl.java:220) - opened session at timestamp: 12666407370  12:38:57,093  (HibernateTransactionManager.java:493) - Opened new Session      [org.hibernate.impl.SessionImpl@83020] for Hibernate transaction ①  12:38:57,093  (HibernateTransactionManager.java:504) - Preparing JDBC Connection      of Hibernate Session [org.hibernate.impl.SessionImpl@83020]  12:38:57,109  (JDBCTransaction.java:54) - begin  …  logon method... updateLastLogonTime... …  12:38:57,109  (AbstractBatcher.java:401) - select user0_.USER_NAME as USER1_0_0_,      user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_,   user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=?      Hibernate: select user0_.USER_NAME as USER1_0_0_,   user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_,   user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=?  …  12:38:57,187  (HibernateTemplate.java:422) - Not closing pre-bound      Hibernate Session after HibernateTemplate  12:38:57,187  (HibernateTemplate.java:397) - Found thread-bound Session     for HibernateTemplate  Hibernate: update T_USER set LAST_LOGON_TIME=?, password=?, score=? where USER_NAME=?  …  2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:470)      - Participating in existing transaction ② addScore...  2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:785)      - Executing prepared SQL update  2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:569)     - Executing prepared SQL statement   [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]  2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:794)      - SQL update affected 1 rows  激活连接数量:1 ③ 2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:752)      - Initiating transaction commit 2010-02-20 12:38:57,203 DEBUG [main] (HibernateTransactionManager.java:652)      - Committing Hibernate transaction on Session   [org.hibernate.impl.SessionImpl@83020] ④  2010-02-20 12:38:57,203 DEBUG [main] (JDBCTransaction.java:103) - commit ⑤

仔细观察这段输出日志,在①处 UserService#logon() 开启一个新的事务,在②处 ScoreService#addScore() 方法加入到①处开启的事务上下文中。③处的输出是 ScoreService#addScore() 方法内部的输出,汇报此时数据源激活的连接数为 1,这清楚地告诉我们 Hibernate 和 JDBC 这两种数据访问技术在同一事务上下文中“ 共用” 一个连接。在④处,提交 Hibernate 事务,接着在⑤处触发调用底层的 Connection 提交事务。

从以上的运行结果,我们可以得出这样的结论:使用 Hibernate 事务管理器后,可以混合使用 Hibernate 和 Spring JDBC 数据访问技术,它们将工作于同一事务上下文中。但是使用 Spring JDBC 访问数据时,Hibernate 的一级或二级缓存得不到同步,此外,一级缓存延迟数据同步机制可能会覆盖 Spring JDBC 数据更改的结果。

由于混合数据访问技术的方案的事务同步而缓存不同步的情况,所以最好用 Hibernate 完成读写操作,而用 Spring JDBC 完成读的操作。如用 Spring JDBC 进行简要列表的查询,而用 Hibernate 对查询出的数据进行维护。如果确实要同时使用 Hibernate 和 Spring JDBC 读写数据,则必须充分考虑到 Hibernate 缓存机制引发的问题:必须充分分析数据维护逻辑,根据需要,及时调用 Hibernate 的 flush() 方法,以免覆盖 Spring JDBC 的更改,在 Spring JDBC 更改数据库时,维护 Hibernate 的缓存。

可以将以上结论推广到其它混合数据访问技术的方案中,如 Hibernate+iBatis,JPA+Spring JDBC,JDO+Spring JDBC 等。

回页首

特殊方法成漏网之鱼

由于 Spring 事务管理是基于接口代理或动态字节码技术,通过 AOP 实施事务增强的。虽然,Spring 还支持 AspectJ LTW 在类加载期实施增强,但这种方法很少使用,所以我们不予关注。

对于基于接口动态代理的 AOP 事务增强来说,由于接口的方法是 public 的,这就要求实现类的实现方法必须是 public 的(不能是 protected,private 等),同时不能使用 static 的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施 AOP 增强,也不能进行 Spring 事务增强了。

基于 CGLib 字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行 AOP 增强植入的。由于使用 final、static、private 修饰符的方法都不能被子类覆盖,相应的,这些方法将不能被实施 AOP 增强。所以,必须特别注意这些修饰符的使用,以免不小心成为事务管理的漏网之鱼。

下面通过具体的实例说明基于 CGLib 字节码动态代理无法享受 Spring AOP 事务增强的特殊方法。

清单 6.UserService.java:4 个不同修饰符的方法

package user.special; import org.springframework.stereotype.Service;  @Service("userService") public class UserService {       //① private方法因访问权限的限制,无法被子类覆盖     private void method1() {         System.out.println("method1");     }       //② final方法无法被子类覆盖     public final void method2() {         System.out.println("method2");     }      //③ static是类级别的方法,无法被子类覆盖     public static void method3() {         System.out.println("method3");     }       //④ public方法可以被子类覆盖,因此可以被动态字节码增强     public void method4() {         System.out.println("method4");     }  }

Spring 通过 CGLib 动态代理技术对 UserService Bean 实施 AOP 事务增强的配置如下所示:

清单 7.applicationContext.xml:对 UserService 用 CGLib 实施事务增强

<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xmlns:context="http://www.springframework.org/schema/context"     xmlns:p="http://www.springframework.org/schema/p"   xmlns:aop="http://www.springframework.org/schema/aop"     xmlns:tx="http://www.springframework.org/schema/tx"     xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd         http://www.springframework.org/schema/context       http://www.springframework.org/schema/context/spring-context-3.0.xsd    http://www.springframework.org/schema/aop    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd    http://www.springframework.org/schema/tx    http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">      <!-- 省略声明数据源及DataSourceTransactionManager事务管理器-->     …     <aop:config proxy-target-class="true">      <!-- ①显式使用CGLib动态代理 -->         <!-- ②希望对UserService所有方法实施事务增强 -->         <aop:pointcut id="serviceJdbcMethod"             expression="execution(* user.special.UserService.*(..))"/>         <aop:advisor pointcut-ref="serviceJdbcMethod"              advice-ref="jdbcAdvice" order="0"/>     </aop:config>     <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">         <tx:attributes>             <tx:method name="*"/>         </tx:attributes>     </tx:advice> </beans>

在 ① 处,我们通过 proxy-target-class="true"显式使用 CGLib 动态代理技术,在 ② 处通过 AspjectJ 切点表达式表达 UserService 所有的方法,希望对 UserService 所有方法都实施 Spring AOP 事务增强。

在 UserService 添加一个可执行的方法,如下所示:

清单 8.UserService.java 添加 main 方法

package user.special; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.stereotype.Service;  @Service("userService") public class UserService {     …     public static void main(String[] args) {         ApplicationContext ctx =              new ClassPathXmlApplicationContext("user/special/applicationContext.xml");            UserService service = (UserService) ctx.getBean("userService");          System.out.println("before method1");         service.method1();         System.out.println("after method1");          System.out.println("before method2");         service.method2();         System.out.println("after method2");          System.out.println("before method3");         service.method3();         System.out.println("after method3");          System.out.println("before method4");         service.method4();         System.out.println("after method4");      } }

在运行 UserService 之前,将 Log4J 日志级别设置为 DEBUG,运行以上代码查看输出日志,如下所示:

17:24:10,953  (AbstractBeanFactory.java:241)      - Returning cached instance of singleton bean 'userService'  before method1 method1 after method1 before method2 method2 after method2 before method3 method3 after method3 before method4  17:24:10,953  (AbstractPlatformTransactionManager.java:365)      - Creating new transaction with name [user.special.UserService.method4]:   PROPAGATION_REQUIRED,ISOLATION_DEFAULT  17:24:11,109  (DataSourceTransactionManager.java:205)      - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@165b7e]   for JDBC transaction  …  17:24:11,109  (DataSourceTransactionManager.java:265)      - Committing JDBC transaction on Connection   [org.apache.commons.dbcp.PoolableConnection@165b7e]  17:24:11,125  (DataSourceTransactionManager.java:323)      - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@165b7e]   after transaction  17:24:11,125  (DataSourceUtils.java:312)      - Returning JDBC Connection to DataSource  after method4

观察以上输出日志,很容易发现 method1~method3 这 3 个方法都没有被实施 Spring 的事务增强,只有 method4 被实施了事务增强。这个结果刚才验证了我们前面的论述。

我们通过下表描述哪些特殊方法将成为 Spring AOP 事务增强的漏网之鱼:

表 2. 不能被 Spring AOP 事务增强的方法

动态代理策略不能被事务增强的方法
基于接口的动态代理 除 public 外的其它所有的方法,此外 public static 也不能被增强
基于 CGLib 的动态代理 private、static、final 的方法

不过,需要特别指出的是,这些不能被 Spring 事务增强的特殊方法并非就不工作在事务环境下。只要它们被外层的事务方法调用了,由于 Spring 的事务管理的传播特殊,内部方法也可以工作在外部方法所启动的事务上下文中。我们说,这些方法不能被 Spring 进行 AOP 事务增强,是指这些方法不能启动事务,但是外层方法的事务上下文依就可以顺利地传播到这些方法中。

这些不能被 Spring 事务增强的方法和可被 Spring 事务增强的方法唯一的区别在 “是否可以主动启动一个新事务” :前者不能而后者可以。对于事务传播行为来说,二者是完全相同的,前者也和后者一样不会造成数据连接的泄漏问题。换句话说,如果这些“特殊方法”被无事务上下文的方法调用,则它们就工作在无事务上下文中;反之,如果被具有事务上下文的方法调用,则它们就工作在事务上下文中。

对于 private 的方法,由于最终都会被 public 方法封装后再开放给外部调用,而 public 方法是可以被事务增强的,所以基本上没有什么问题。在实际开发中,最容易造成隐患的是基于 CGLib 的动态代理时的“public static”和“public final”这两种特殊方法。原因是它们本身是 public 的,所以可以直接被外部类(如 Web 层的 Controller 类)调用,只要调用者没有事务上下文,这些特殊方法也就以无事务的方式运作。

回页首

小结

在本文中,我们通过剖析了解到以下的真相:

  • 混合使用多个数据访问技术框架的最佳组合是一个 ORM 技术框架(如 Hibernate 或 JPA 等)+ 一个 JDBC 技术框架(如 Spring JDBC 或 iBatis)。直接使用 ORM 技术框架对应的事务管理器就可以了,但必须考虑 ORM 缓存同步的问题;
  • Spring AOP 增强有两个方案:其一为基于接口的动态代理,其二为基于 CGLib 动态生成子类的代理。由于 Java 语法的特性,有些特殊方法不能被 Spring AOP 代理,所以也就无法享受 AOP 织入带来的事务增强。

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

  • 直接获取 Connection 时,哪些情况会造成数据连接的泄漏,以及如何应对;
  • 除 Spring JDBC 外,其它数据访问技术数据连接泄漏的应对方案。

第 3 部分

http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts3/

概述

对于应用开发者来说,数据连接泄漏无疑是一个可怕的梦魇。如果存在数据连接泄漏问题,应用程序将因数据连接资源的耗尽而崩溃,甚至还可能引起数据库的崩溃。数据连接泄漏像黑洞一样让开发者避之唯恐不及。

Spring DAO 对所有支持的数据访问技术框架都使用模板化技术进行了薄层的封装。只要您的程序都使用 Spring DAO 模板(如 JdbcTemplate、HibernateTemplate 等)进行数据访问,一定不会存在数据连接泄漏的问题 ―― 这是 Spring 给予我们郑重的承诺!因此,我们无需关注数据连接(Connection)及其衍生品(Hibernate 的 Session 等)的获取和释放的操作,模板类已经通过其内部流程替我们完成了,且对开发者是透明的。

但是由于集成第三方产品,整合遗产代码等原因,可能需要直接访问数据源或直接获取数据连接及其衍生品。这时,如果使用不当,就可能在无意中创造出一个魔鬼般的连接泄漏问题。

我们知道:当 Spring 事务方法运行时,就产生一个事务上下文,该上下文在本事务执行线程中针对同一个数据源绑定了一个唯一的数据连接(或其衍生品),所有被该事务上下文传播的方法都共享这个数据连接。这个数据连接从数据源获取及返回给数据源都在 Spring 掌控之中,不会发生问题。如果在需要数据连接时,能够获取这个被 Spring 管控的数据连接,则使用者可以放心使用,无需关注连接释放的问题。

那么,如何获取这些被 Spring 管控的数据连接呢? Spring 提供了两种方法:其一是使用数据资源获取工具类,其二是对数据源(或其衍生品如 Hibernate SessionFactory)进行代理。在具体介绍这些方法之前,让我们先来看一下各种引发数据连接泄漏的场景。

回页首

Spring JDBC 数据连接泄漏

如果直接从数据源获取连接,且在使用完成后不主动归还给数据源(调用 Connection#close()),则将造成数据连接泄漏的问题。

一个具体的实例

下面,来看一个具体的实例:

清单 1.JdbcUserService.java:主体代码

package user.connleak;  import org.apache.commons.dbcp.BasicDataSource;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.context.ApplicationContext;  import org.springframework.context.support.ClassPathXmlApplicationContext;  import org.springframework.jdbc.core.JdbcTemplate;  import org.springframework.stereotype.Service;  import java.sql.Connection;   @Service("jdbcUserService")  public class JdbcUserService {      @Autowired      private JdbcTemplate jdbcTemplate;       public void logon(String userName) {          try {              // ①直接从数据源获取连接,后续程序没有显式释放该连接             Connection conn = jdbcTemplate.getDataSource().getConnection();              String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";              jdbcTemplate.update(sql, System.currentTimeMillis(), userName);              Thread.sleep(1000);// ②模拟程序代码的执行时间         } catch (Exception e) {              e.printStackTrace();          }      }  }

JdbcUserService 通过 Spring AOP 事务增强的配置,让所有 public 方法都工作在事务环境中。即让 logon() 和 updateLastLogonTime() 方法拥有事务功能。在 logon() 方法内部,我们在①处通过调用 jdbcTemplate.getDataSource().getConnection() 显式获取一个连接,这个连接不是 logon() 方法事务上下文线程绑定的连接,所以如果开发者如果没有手工释放这连接(显式调用 Connection#close() 方法),则这个连接将永久被占用(处于 active 状态),造成连接泄漏!下面,我们编写模拟运行的代码,查看方法执行对数据连接的实际占用情况:

清单 2.JdbcUserService.java:模拟运行代码

… @Service("jdbcUserService") public class JdbcUserService {     …     //①以异步线程的方式执行JdbcUserService#logon()方法,以模拟多线程的环境     public static void asynchrLogon(JdbcUserService userService, String userName) {         UserServiceRunner runner = new UserServiceRunner(userService, userName);         runner.start();     }     private static class UserServiceRunner extends Thread {         private JdbcUserService userService;         private String userName;         public UserServiceRunner(JdbcUserService userService, String userName) {             this.userService = userService;             this.userName = userName;         }         public void run() {             userService.logon(userName);         }     }      //② 让主执行线程睡眠一段指定的时间     public static void sleep(long time) {         try {             Thread.sleep(time);         } catch (InterruptedException e) {             e.printStackTrace();         }     }       //③ 汇报数据源的连接占用情况     public static void reportConn(BasicDataSource basicDataSource) {         System.out.println("连接数[active:idle]-[" +             basicDataSource.getNumActive()+":"+basicDataSource.getNumIdle()+"]");     }      public static void main(String[] args) {         ApplicationContext ctx =              new ClassPathXmlApplicationContext("user/connleak/applicatonContext.xml");         JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService");          BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");            //④汇报数据源初始连接占用情况         JdbcUserService.reportConn(basicDataSource);          JdbcUserService.asynchrLogon(userService, "tom");         JdbcUserService.sleep(500);          //⑤此时线程A正在执行JdbcUserService#logon()方法         JdbcUserService.reportConn(basicDataSource);           JdbcUserService.sleep(2000);         //⑥此时线程A所执行的JdbcUserService#logon()方法已经执行完毕         JdbcUserService.reportConn(basicDataSource);          JdbcUserService.asynchrLogon(userService, "john");         JdbcUserService.sleep(500);            //⑦此时线程B正在执行JdbcUserService#logon()方法         JdbcUserService.reportConn(basicDataSource);                  JdbcUserService.sleep(2000);            //⑧此时线程A和B都已完成JdbcUserService#logon()方法的执行         JdbcUserService.reportConn(basicDataSource);     }

在 JdbcUserService 中添加一个可异步执行 logon() 方法的 asynchrLogon() 方法,我们通过异步执行 logon() 以及让主线程睡眠的方式模拟多线程环境下的执行场景。在不同的执行点,通过 reportConn() 方法汇报数据源连接的占用情况。

使用如下的 Spring 配置文件对 JdbcUserServie 的方法进行事务增强:

清单 3.applicationContext.xml

<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xmlns:context="http://www.springframework.org/schema/context"     xmlns:p="http://www.springframework.org/schema/p"  xmlns:aop="http://www.springframework.org/schema/aop"     xmlns:tx="http://www.springframework.org/schema/tx"     xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd         http://www.springframework.org/schema/context    http://www.springframework.org/schema/context/spring-context-3.0.xsd    http://www.springframework.org/schema/aop    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd    http://www.springframework.org/schema/tx    http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">     <context:component-scan base-package="user.connleak"/>     <bean id="dataSource"         class="org.apache.commons.dbcp.BasicDataSource"             destroy-method="close"             p:driverClassName="oracle.jdbc.driver.OracleDriver"             p:url="jdbc:oracle:thin:@localhost:1521:orcl"             p:username="test"             p:password="test"             p:defaultAutoCommit="false"/>      <bean id="jdbcTemplate"         class="org.springframework.jdbc.core.JdbcTemplate"         p:dataSource-ref="dataSource"/>      <bean id="jdbcManager"         class="org.springframework.jdbc.datasource.DataSourceTransactionManager"         p:dataSource-ref="dataSource"/>      <!-- 对JdbcUserService的所有方法实施事务增强 -->     <aop:config proxy-target-class="true">         <aop:pointcut id="serviceJdbcMethod"             expression="within(user.connleak.JdbcUserService+)"/>         <aop:advisor pointcut-ref="serviceJdbcMethod"        advice-ref="jdbcAdvice" order="0"/>     </aop:config>     <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">         <tx:attributes>             <tx:method name="*"/>         </tx:attributes>     </tx:advice> </beans>

保证 BasicDataSource 数据源的配置默认连接为 0,运行以上程序代码,在控制台中将输出以下的信息:

清单 4. 输出日志

连接数 [active:idle]-[0:0]  连接数 [active:idle]-[2:0]  连接数 [active:idle]-[1:1]  连接数 [active:idle]-[3:0]  连接数 [active:idle]-[2:1]

我们通过下表对数据源连接的占用和泄漏情况进行描述:

表 1. 执行过程数据源连接占用情况

时间执行线程 1执行线程 2数据源连接activeidleleak
T0 未启动 未启动 0 0 0
T1 正在执行方法 未启动 2 0 0
T2 执行完毕 未启动 1 1 1
T3 执行完毕 正式执行方法 3 0 1
T4 执行完毕 执行完毕 2 1 2

可见在执行线程 1 执行完毕后,只释放了一个数据连接,还有一个数据连处于 active 状态,说明泄漏了一个连接。相似的,执行线程 2 执行完毕后,也泄漏了一个连接:原因是直接通过数据源获取连接(jdbcTemplate.getDataSource().getConnection())而没有显式释放造成的。

通过 DataSourceUtils 获取数据连接

Spring 提供了一个能从当前事务上下文中获取绑定的数据连接的工具类,那就是 DataSourceUtils。Spring 强调必须使用 DataSourceUtils 工具类获取数据连接,Spring 的 JdbcTemplate 内部也是通过 DataSourceUtils 来获取连接的。DataSourceUtils 提供了若干获取和释放数据连接的静态方法,说明如下:

  • static Connection doGetConnection(DataSource dataSource) :首先尝试从事务上下文中获取连接,失败后再从数据源获取连接;
  • static Connection getConnection(DataSource dataSource) :和 doGetConnection 方法的功能一样,实际上,它内部就是调用 doGetConnection 方法获取连接的;
  • static void doReleaseConnection(Connection con, DataSource dataSource) :释放连接,放回到连接池中;
  • static void releaseConnection(Connection con, DataSource dataSource) :和 doReleaseConnection 方法的功能一样,实际上,它内部就是调用 doReleaseConnection 方法获取连接的;

来看一下 DataSourceUtils 从数据源获取连接的关键代码:

清单 5. DataSourceUtils.java 获取连接的工具类

public abstract class DataSourceUtils {     …     public static Connection doGetConnection(DataSource dataSource) throws SQLException {            Assert.notNull(dataSource, "No DataSource specified");          //①首先尝试从事务同步管理器中获取数据连接         ConnectionHolder conHolder =              (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);         if (conHolder != null && (conHolder.hasConnection() ||              conHolder.isSynchronizedWithTransaction())) {              conHolder.requested();             if (!conHolder.hasConnection()) {                 logger.debug(                     "Fetching resumed JDBC Connection from DataSource");                 conHolder.setConnection(dataSource.getConnection());             }    return conHolder.getConnection();   }            //②如果获取不到,则直接从数据源中获取连接         Connection con = dataSource.getConnection();          //③如果拥有事务上下文,则将连接绑定到事务上下文中         if (TransactionSynchronizationManager.isSynchronizationActive()) {             ConnectionHolder holderToUse = conHolder;             if (holderToUse == null) {     holderToUse = new ConnectionHolder(con);    }    else {holderToUse.setConnection(con);}             holderToUse.requested();    TransactionSynchronizationManager.registerSynchronization(                 new ConnectionSynchronization(holderToUse, dataSource));    holderToUse.setSynchronizedWithTransaction(true);    if (holderToUse != conHolder) {     TransactionSynchronizationManager.bindResource(                 dataSource, holderToUse);    }   }   return con;  }     … }

它首先查看当前是否存在事务管理上下文,并尝试从事务管理上下文获取连接,如果获取失败,直接从数据源中获取连接。在获取连接后,如果当前拥有事务上下文,则将连接绑定到事务上下文中。

我们在清单 1 的 JdbcUserService 中,使用 DataSourceUtils.getConnection() 替换直接从数据源中获取连接的代码:

清单 6. JdbcUserService.java:使用 DataSourceUtils 获取数据连接

public void logon(String userName) {     try {         //Connection conn = jdbcTemplate.getDataSource().getConnection();         //①使用DataSourceUtils获取数据连接         Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());         String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";         jdbcTemplate.update(sql, System.currentTimeMillis(), userName);         Thread.sleep(1000);      } catch (Exception e) {         e.printStackTrace();     } }

重新运行代码,得到如下的执行结果:

清单 7. 输出日志

连接数 [active:idle]-[0:0]  连接数 [active:idle]-[1:0]  连接数 [active:idle]-[0:1]  连接数 [active:idle]-[1:0]  连接数 [active:idle]-[0:1]

对照清单 4 的输出日志,我们可以看到已经没有连接泄漏的现象了。一个执行线程在运行 JdbcUserService#logon() 方法时,只占用一个连接,而且方法执行完毕后,该连接马上释放。这说明通过 DataSourceUtils.getConnection() 方法确实获取了方法所在事务上下文绑定的那个连接,而不是像原来那样从数据源中获取一个新的连接。

使用 DataSourceUtils 获取数据连接也可能造成泄漏!

是否使用 DataSourceUtils 获取数据连接就可以高枕无忧了呢?理想很美好,但现实很残酷:如果 DataSourceUtils 在没有事务上下文的方法中使用 getConnection() 获取连接,依然会造成数据连接泄漏!

保持代码清单 6 的代码不变,调整 Spring 配置文件,将清单 3 中 Spring AOP 事务增强配置的代码注释掉,重新运行清单 6 的代码,将得到如下的输出日志:

清单 8. 输出日志

连接数 [active:idle]-[0:0]  连接数 [active:idle]-[1:1]  连接数 [active:idle]-[1:1]  连接数 [active:idle]-[2:1]  连接数 [active:idle]-[2:1]

我们通过下表对数据源连接的占用和泄漏情况进行描述:

表 2. 执行过程数据源连接占用情况

时间执行线程 1执行线程 2数据源连接activeidleleak
T0 未启动 未启动 0 0 0
T1 正在执行方法 未启动 1 1 0
T2 执行完毕 未启动 1 1 1
T3 执行完毕 正式执行方法 2 1 1
T4 执行完毕 执行完毕 2 1 2

仔细对照表 1 的执行过程,我们发现在 T1 时,有事务上下文时的 active 为 2,idle 为 0,而此时由于没有事务管理,则 active 为 1 而 idle 也为 1。这说明有事务上下文时,需要等到整个事务方法(即 logon())返回后,事务上下文绑定的连接才释放。但在没有事务上下文时,logon() 调用 JdbcTemplate 执行完数据操作后,马上就释放连接。

在 T2 执行线程完成 logon() 方法的执行后,有一个连接没有被释放(active),所以发生了连接泄漏。到 T4 时,两个执行线程都完成了 logon() 方法的调用,但是出现了两个未释放的连接。

要堵上这个连接泄漏的漏洞,需要对 logon() 方法进行如下的改造:

清单 9.JdbcUserService.java:手工释放获取的连接

public void logon(String userName) {     Connection conn = null;     try {         conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());         String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";         jdbcTemplate.update(sql, System.currentTimeMillis(), userName);         Thread.sleep(1000);         // ①     } catch (Exception e) {         e.printStackTrace();     }finally {         // ②显式使用DataSourceUtils释放连接         DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource());     } }

在 ② 处显式调用 DataSourceUtils.releaseConnection() 方法释放获取的连接。特别需要指出的是:一定不能在 ① 处释放连接!因为如果 logon() 在获取连接后,① 处代码前这段代码执行时发生异常,则①处释放连接的动作将得不到执行。这将是一个非常具有隐蔽性的连接泄漏的隐患点。

JdbcTemplate 如何做到对连接泄漏的免疫

分析 JdbcTemplate 的代码,我们可以清楚地看到它开放的每个数据操作方法,首先都使用 DataSourceUtils 获取连接,在方法返回之前使用 DataSourceUtils 释放连接。

来看一下 JdbcTemplate 最核心的一个数据操作方法 execute():

清单 10.JdbcTemplate#execute()

public <T> T execute(StatementCallback<T> action) throws DataAccessException {     //① 首先根据DataSourceUtils获取数据连接     Connection con = DataSourceUtils.getConnection(getDataSource());     Statement stmt = null;     try {         Connection conToUse = con;         …         handleWarnings(stmt);         return result;     }     catch (SQLException ex) {         JdbcUtils.closeStatement(stmt);         stmt = null;         DataSourceUtils.releaseConnection(con, getDataSource());         con = null;         throw getExceptionTranslator().translate(             "StatementCallback", getSql(action), ex);     }     finally {         JdbcUtils.closeStatement(stmt);         //② 最后根据DataSourceUtils释放数据连接         DataSourceUtils.releaseConnection(con, getDataSource());     } }

在 ① 处通过 DataSourceUtils.getConnection() 获取连接,在 ② 处通过 DataSourceUtils.releaseConnection() 释放连接。所有 JdbcTemplate 开放的数据访问方法最终都是通过 execute(StatementCallback<T> action) 执行数据访问操作的,因此这个方法代表了 JdbcTemplate 数据操作的最终实现方式。

正是因为 JdbcTemplate 严谨的获取连接,释放连接的模式化流程保证了 JdbcTemplate 对数据连接泄漏问题的免疫性。所以,如有可能尽量使用 JdbcTemplate,HibernateTemplate 等这些模板进行数据访问操作,避免直接获取数据连接的操作。

使用 TransactionAwareDataSourceProxy

如果不得已要显式获取数据连接,除了使用 DataSourceUtils 获取事务上下文绑定的连接外,还可以通过 TransactionAwareDataSourceProxy 对数据源进行代理。数据源对象被代理后就具有了事务上下文感知的能力,通过代理数据源的 getConnection() 方法获取的连接和使用 DataSourceUtils.getConnection() 获取连接的效果是一样的。

下面是使用 TransactionAwareDataSourceProxy 对数据源进行代理的配置:

清单 11.applicationContext.xml:对数据源进行代理

<bean id="dataSource"     class="org.apache.commons.dbcp.BasicDataSource"     destroy-method="close"     p:driverClassName="oracle.jdbc.driver.OracleDriver"     p:url="jdbc:oracle:thin:@localhost:1521:orcl"     p:username="test"     p:password="test"     p:defaultAutoCommit="false"/>      <!-- ①对数据源进行代理--> <bean id="dataSourceProxy"      class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy"     p:targetDataSource-ref="dataSource"/>      <!-- ②直接使用数据源的代理对象--> <bean id="jdbcTemplate"     class="org.springframework.jdbc.core.JdbcTemplate"     p:dataSource-ref="dataSourceProxy"/>      <!-- ③直接使用数据源的代理对象--> <bean id="jdbcManager"     class="org.springframework.jdbc.datasource.DataSourceTransactionManager"     p:dataSource-ref="dataSourceProxy"/>

对数据源进行代理后,我们就可以通过数据源代理对象的 getConnection() 获取事务上下文中绑定的数据连接了。

因此,如果数据源已经进行了 TransactionAwareDataSourceProxy 的代理,而且方法存在事务上下文,那么清单 1 的代码也不会生产连接泄漏的问题。

回页首

其它数据访问技术的等价类

理解了 Spring JDBC 的数据连接泄漏问题,其中的道理可以平滑地推广到其它框架中去。Spring 为每个数据访问技术框架都提供了一个获取事务上下文绑定的数据连接(或其衍生品)的工具类和数据源(或其衍生品)的代理类。

DataSourceUtils 的等价类

下表列出了不同数据访问技术对应 DataSourceUtils 的等价类:

表 3. 不同数据访问框架 DataSourceUtils 的等价类

数据访问技术框架连接 ( 或衍生品 ) 获取工具类
Spring JDBC org.springframework.jdbc.datasource.DataSourceUtils
Hibernate org.springframework.orm.hibernate3.SessionFactoryUtils
iBatis org.springframework.jdbc.datasource.DataSourceUtils
JPA org.springframework.orm.jpa.EntityManagerFactoryUtils
JDO org.springframework.orm.jdo.PersistenceManagerFactoryUtils

TransactionAwareDataSourceProxy 的等价类

下表列出了不同数据访问技术框架下 TransactionAwareDataSourceProxy 的等价类:

表 4. 不同数据访问框架 TransactionAwareDataSourceProxy 的等价类

数据访问技术框架连接 ( 或衍生品 ) 获取工具类
Spring JDBC org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
Hibernate org.springframework.orm.hibernate3.LocalSessionFactoryBean
iBatis org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
JPA
JDO org.springframework.orm.jdo.TransactionAwarePersistenceManagerFactoryProxy

http://www.cnblogs.com/davidwang456/p/3832949.html

已有 0 人发表留言,猛击->> 这里 <<-参与讨论

ITeye推荐

  • —软件人才免语言低担保 赴美带薪读研!—
正文到此结束
Loading...