Spring 事務管理高級應用難點剖析2

Spring 事務管理高級應用難點剖析: 第 2 部分

陳 雄華, 系統架構師
陳雄華,2002 年畢業於廈門大學計算機與信息工程學院,獲碩士學位。擁有 10 多年的 Java 開發、設計、架構的經驗。技術研發之餘,常將經驗所得行諸於文字,作者是國內多個著名技術網站的專欄作者,在各大技術網站、報刊雜誌發表過數十篇技術文章,廣受讀者好評。於 2005 年出版《精通 JBuilder 2005》,於 2007 年出版《精通 Spring 2.x -- 企業應用開發詳解》。

簡介: 本文是“Spring 事務管理高級應用難點剖析”系列文章的第 2 部分,作者將繼續深入剖析在實際 Spring 事務管理應用中容易遇見的一些難點,包括混合使用多種數據訪問技術(如 Spring JDBC + Hibernate)的事務管理問題,以及通過 Spring AOP 增強的 Bean 存在的一些比較特殊的情況。

查看本系列更多內容

發佈日期: 2010 年 3 月 25 日
級別: 中級
訪問情況 : 15432 次瀏覽
評論: 0 (查看 | 添加評論 - 登錄)

平均分 4 星 共 41 個評分 平均分 (41個評分)
爲本文評分

聯合軍種作戰的混亂

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 外,其它數據訪問技術數據連接泄漏的應對方案。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章