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

联合军种做战的混乱java

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

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

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


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

混合数据访问技术 事务管理器
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 多是被使用得最多的组合,下面咱们经过实例观察事务管理的运做状况。apache


清单 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 的关键代码:session


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

				
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 外,其它数据访问技术数据链接泄漏的应对方案。

摘自:http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts2/?cmp=dwskl&cpb=dw&ct=dwcon&cr=cn_51CTO&ccy=cn

相关文章
相关标签/搜索