1. 程式人生 > >Spring 事務管理高階應用難點剖析--轉

Spring 事務管理高階應用難點剖析--轉

第 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 [[email protected]] 
	for JDBC transaction
13:24:22,921 DEBUG (DataSourceTransactionManager.java:222) - 
    Switching JDBC Connection 
	[[email protected]] 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 
	[[email protected]]
13:24:23,140 DEBUG (DataSourceTransactionManager.java:323) - 
    Releasing JDBC Connection [[email protected]] 
	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

圖 1. UserService 和 ScoreService

具體的程式碼如下所示:

清單 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 [[email protected]] 
	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 [[email protected]]

16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection 
    [[email protected]] 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 
    [[email protected]] 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 [[email protected]] ③

[main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection 
    [[email protected]] after transaction

[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

[Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection 
    [[email protected]] 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 [[email protected]] ④

[Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection 
    [[email protected]] 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 
    [[email protected]] for Hibernate transaction ①

12:38:57,093  (HibernateTransactionManager.java:504) - Preparing JDBC Connection 
    of Hibernate Session [[email protected]]

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 
	[[email protected]] ④

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 [[email protected]] 
	for JDBC transaction

…

17:24:11,109  (DataSourceTransactionManager.java:265) 
    - Committing JDBC transaction on Connection 
	[[email protected]]

17:24:11,125  (DataSourceTransactionManager.java:323) 
    - Releasing JDBC Connection [[email protected]] 
	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 方法是可以被事務增強的,所以基本上沒有什麼問