Spring中的事務控制學習中(轉)


Chapter 1. Spring中的事務控制(Transacion Management with Spring)

Table of Contents

1.1. 有關事務(Transaction)的楔子
1.1.1. 認識事務本身
1.1.2. 初識事務家族成員
1.2. 群雄逐鹿下的Java事務管理
1.2.1. Java平台的局部事務支持
1.2.2. Java平台的分布式事務支持
1.2.2.1. 基於JTA的分布式事務管理
1.2.2.1.1. JTA編程事務管理
1.2.2.1.2. JTA聲明性事務管理
1.2.2.2. 基於JCA的分布式事務管理
1.2.3. 繼續前行之前的反思
1.3. 一統河山后的Spring事務管理
1.3.1. spring事務王國的架構
1.3.1.1. 統一中原的過程
1.3.1.2. 和平年代
1.3.1.2.1. TransactionDefinition
1.3.1.2.1.1. TransactionDefinition簡介
1.3.1.2.1.2. TransactionDefinition相關實現
1.3.1.2.2. TransactionStatus
1.3.1.2.3. PlatformTransactionManager
1.3.1.2.3.1. PlatformTransactionManager實現類概覽
1.3.1.2.3.2. 窺一斑而知全豹
1.3.2. 使用spring進行事務管理
1.3.2.1. 編程式事務管理
1.3.2.1.1. 直接使用PlatformTransactionManager進行編程式事務管理
1.3.2.1.2. 使用TransactionTemplate進行編程式事務管理
1.3.2.1.3. 編程創建基於Savepoint的嵌套事務
1.3.2.2. 聲明式事務管理
1.3.2.2.1. 引子
1.3.2.2.2. XML元數據驅動的聲明式事務
1.3.2.2.2.1. 使用ProxyFactory(ProxyFactoryBean)+TransactionInterceptor
1.3.2.2.2.2. 使用“一站式”的TransactionProxyFactoryBean
1.3.2.2.2.3. 使用BeanNameAutoProxyCreator
1.3.2.2.2.4. 使用Spring2.x的聲明事務配置方式
1.3.2.2.3. Annotation元數據驅動的聲明式事務
1.4. spring事務管理之擴展篇
1.4.1. 理解並活用ThreadLocal
1.4.1.1. 理解ThreadLocal的存在背景
1.4.1.2. 理解ThreadLocal的實現
1.4.1.3. ThreadLocal的應用場景
1.4.1.4. 使用ThreadLocal管理多數據源切換的條件
1.4.2. 談Strategy模式在開發過程中的應用
1.4.3. Spring與JTA背后的奧秘(Magic behind Spring and JTA)

事務管理(Transaction Management)是一個很深的研究方向,而本章最終目的是為了闡述spring的事務管理抽象的理念以及相關內容,所以,你大可放心,我不會為你准備一本類似於“磚頭”的書籍, 但為了能夠在整個講解的過程中始終有一個平滑的過渡,有關事務(Transaction)的一些基本概念還是有必要簡單介紹一下的。

1.1. 有關事務(Transaction)的楔子

1.1.1. 認識事務本身

為了說明“什么是事務”,我覺得先從事務所針對的目的說起,會比較容易切入。

對於一個軟件系統來說,需要相應的數據資源(比如,數據庫,文件系統等)來保存系統狀態,在對系統狀態所依托的數據資源進行訪問的時候,為了保證系統始終處於一個“正確”的狀態[1], 我們就必須對這些訪問操作進行一些必要的限定,以此來保證系統狀態的完整性。

事務就是以可控的方式對數據資源進行訪問的一組操作,為了保證事務執行前后數據資源所承載的系統狀態始終處於“正確”狀態,事務本身持有四個限定屬性, 即原子性(Atomicity),一致性(Consistency),隔離性(Isolation)以及持久性(Durability),也就是常說的事務的ACID屬性:

事務的原子性(Atomicity)

原子性要求事務所包含的全部操作是一個不可分割的整體,這些操作要么全部提交成功,要么只要其中一個操作失敗,就全部“成仁”(“一顆老鼠屎攪壞一鍋湯”好像形容這種情況比較貼切哦)。 如果把整個事務的操作比做“鋼七連”,那我們的口號就得從“不拋棄,不放棄”改成“要么不拋棄,要么就全部放棄”了。

事務的一致性(Consistency)

一致性要求事務所包含的操作不能違反數據資源的一致性檢查,數據資源在事務執行之前處於一個數據的一致性狀態,那么,事務執行之后也需要依然保持數據間的一致性狀態。 對於一個證券系統來說,如果顧客銀行賬戶和證券賬戶資金總和為10萬的話(銀行賬戶初始8萬,證券賬戶初始2萬),從銀行賬戶的8萬轉賬5萬到證券賬戶的事務操作結束之后, 銀行賬戶會剩余3萬,證券賬戶為7萬,兩個賬戶的總和依然是10萬,如果事務操作結束后,整個數據狀態不是這個樣子,那么就說系統處於不一致狀態,而使用事務其中一個目的就是為了避免這種不一致性狀態的產生。

事務的隔離性(Isolation)[2]

事務的隔離性主要規定了各個事務之間相互影響的程度。隔離性概念主要面向對數據資源的並發訪問(Concurrency),並兼顧影響事務的一致性。當兩個事務或者更多事務同時訪問同一數據資源的時候, 不同的隔離級別決定了各個事務對該數據資源訪問的不同行為。

不出意外的話,我們可以為事務指定四種類型的隔離級別,隔離程度按照從弱到強分別為“Read Uncommitted”,“Read Committed”,“Repeatable Read”和“Serializable”:

  • Read Uncommitted.  最低的隔離級別,Read Uncommitted最直接的效果就是一個事務可以讀取另一個事務並未提交的更新結果。

    Read Uncommitted是以較低的隔離度來尋求較高的性能,其本身無法避免以下幾個問題:

    • 臟讀(Dirty Read). 如果一個事務中對數據進行了更新,但事務還沒有提交,另一個事務可以“看到”該事務沒有提交的更新結果,這樣造成的問題就是,如果第一個事務回滾,那么,第二個事務在此之前所“看到”的數據就是一筆臟數據。

    • 不可重復讀取(Non-Repeatable Read).  不可重復讀取是指同一個事務在整個事務過程中對同一筆數據進行讀取,每次讀取結果都不同。如果事務1在事務2的更新操作之前讀取一次數據,在事務2的更新操作之后再讀取同一筆數據一次,兩次結果是不同的,所以,Read Uncommitted也無法避免不可重復讀取的問題。

    • 幻讀(Phantom Read)[3] 幻讀是指同樣一筆查詢在整個事務過程中多次執行后,查詢所得的結果集是不一樣的。幻讀針對的是多筆記錄。在Read Uncommitted隔離級別下, 不管事務2的插入操作是否提交,事務1在插入操作之前和之后執行相同的查詢,取得的結果集是不同的,所以,Read Uncommitted同樣無法避免幻讀的問題。

     

  • Read Committed.  Read Committed通常是大部分數據庫采用的默認隔離級別,它在Read Uncommitted隔離級別基礎上所做的限定更進一步, 在該隔離級別下,一個事務的更新操作結果只有在該事務提交之后,另一個事務才可能讀取到同一筆數據更新后的結果。 所以,Read Committed可以避免Read Uncommitted隔離級別下存在的臟讀問題, 但,無法避免不可重復讀取和幻讀的問題。

  • Repeatable Read.  Repeatable Read隔離級別可以保證在整個事務的過程中,對同一筆數據的讀取結果是相同的,不管其他事務是否同時在對同一筆數據進行更新,也不管其他事務對同一筆數據的更新提交與否。 Repeatable Read隔離級別避免了臟讀和不可重復讀取的問題,但無法避免幻讀。

  • Serializable.  最為嚴格的隔離級別,所有的事務操作都必須依次順序執行,可以避免其他隔離級別遇到的所有問題,是最為安全的隔離級別, 但同時也是性能最差的隔離級別,因為所有的事務在該隔離級別下都需要依次順序執行,所以,並發度下降,吞吐量上不去,性能自然就下來了。 因為該隔離級別極大的影響系統性能,所以,很少場景會使用它。通常情況下,我們會使用其他隔離級別加上相應的並發鎖的機制來控制對數據的訪問,這樣既保證了系統性能不會損失太大,也能夠一定程度上保證數據的一致性。

對於數據庫來說,通常都有一個默認的隔離級別,大多數情況下都是ReadCommitted,只有Hsqldb使用Read UnCommited作為默認隔離級別。 而且,並非所有的數據庫都支持這四種隔離級別,比如Oracle只支持Read Committed和Serializable,如果你指定的隔離級別當前數據庫不支持的話, 數據庫會采用默認的隔離級別代替你指定的隔離級別。EJB,Spring,JDBC等數據訪問方式都允許我們為事務指定以上提到的四種隔離級別,但最終事務是否以指定的隔離級別執行,則由底層的數據資源來決定。

 

不同的隔離級別設置會對系統的並發性以及數據一致性造成不同的影響,總的來說,隔離級別與系統並發性成反比,與數據一致性成正比。 也就是說,事務隔離度越高,系統並發性越差,進而造成系統性能就越差,不過,隔離度的增高,卻可以更好地保證數據的一致性。隔離程度與並發性和一致性的關系如下圖:

Figure 1.1. Isolation與Concurrency,Consistency之間的關系

Isolation與Concurrency,Consistency之間的關系

在具體的實踐過程中,我們需要根據系統的具體情況來調整隔離度以保證系統性能與數據一致性之間有一個良好的平衡,但總的來說,保證數據的一致性的考慮應該優於對系統性能的考慮。

 

事務的持久性(Durability)

事務的持久性是指一旦整個事務操作成功提交完成,對數據所做的變更將被記載並不可逆轉,多少有點兒“生米煮成熟飯”的意思。 即使發生某些系統災難或者什么天災人禍之類的事情,之前事務所做的變更也可以找回並恢復。縱使海枯石爛,我(數據庫等資源管理系統)對你(事務)的真心永不變! 通常情況下,數據庫等數據資源管理系統會通過冗余存儲或者多數據網絡備份等方式來保證事務的持久性。

至此,對事務自身的認識就算告一段落了,接着,讓我們進一步認識一下與事務有關的“ 家族成員”吧!

 

1.1.2. 初識事務家族成員

在一個典型的事務處理場景中,有以下幾個參與者:

Resource Manager(RM)

ResourceManager簡稱RM,它負責存儲並管理系統數據資源的狀態,比如數據庫服務器,JMS消息服務器等都是相應的Resource Manager。

Transaction Processing Monitor(TP Monitor)

Transaction Processing Monitor簡稱TPM或者TP Monitor,它的職責是在分布式事務場景中協調包含多個RM的事務處理。TP Monitor通常對應特定的軟件中間件(Middleware), 隨着軟件開發技術的進步,TP Monitor的實現也由原來基於過程式的設計與實現轉向面向對象的更趨模塊化的設計和實現。J2EE規范[4]中的應用服務器(Application Server)通常擔當的就是TP Monitor的角色。

Transaction Manager(TM)

Transaction Manager簡稱為TM,它可以認為是TP Monitor中的核心模塊,直接負責多RM之間的事務處理的協調工作,並且提供事務界定(Transaction Demarcation)[5], 事務上下文傳播(transaction context propagation)[6]等功能接口。

Application

以獨立形式存在的或者運行於容器中的應用程序,可以認為是事務邊界的觸發點。

實際上,並非每一個事務的場景中都會出現以上提到的所有參與者,如果我們根據整個事務中牽扯的RM的多寡來區分事務類型的話,可以將事務分為兩類,即全局事務(Global Transaction)和局部事務(Local Transaction),在這兩類事務中,具體的事務參與者是不同的:
  • 全局事務(Global Transaction).  如果整個事務處理過程中有多個Resource Manager的參與,那么就需要引入TP Monitor來協調多個RM之間的事務處理,TP Monitor將采用“兩階段提交(2 Phase Commit)”協議來保證整個事務的ACID屬性, 這種場景下的事務我們就稱其為全局事務(Global Transaction)或者分布式事務(Distributed Transaction)。全局事務中各個參與者之間的關系如下圖所示:

    Figure 1.2. 全局事務示意圖

    全局事務示意圖

    所有應用程序提交的事務請求需要通過TP Monitor的調配之后,直接由TM統一協調,TM將使用“兩階段提交(two-phase commit)”協議來協調處理多RM之間的事務處理。 針對“兩階段提交”的描述,最經典的比喻就是西方婚禮的過程,婚禮的牧師或者主持是TM,他會首先詢問兩位新人(兩個RM),是否願意娶對方為妻(嫁給對方), 如果雙方的反饋都是“I do”的時候,牧師將宣布二者結為夫妻(即整個事務提交成功)。如果雙方任何一方有疑議,那不好意思,婚禮無法繼續進行,整個事務提交失敗,雙方都要回滾(rollback)到之前的單身狀態。

     

  • 局部事務(Local Transaction).  如果當前事務只有一個Resource Manager參與其中的話,我們就可以稱當前事務為局部事務(Local Transaction)。 比如,你在當前事務中只對一個數據庫進行更新,或者只向一個消息隊列中發送消息的情況,都屬於局部事務。

    Figure 1.3. 局部事務示意圖

    局部事務示意圖

    因為局部事務只包含一個Resource manager,所以,也就沒有必要引入相應的TP Monitor來幫助協調管理多個Resource Manager之間的事務, 應用程序可以直接與RM打交道。通常情況下,相應的RM都有內置的事務支持,所以,在局部事務中,我們更傾向於直接使用RM的內置事務支持, 這樣不僅可以極大的減少事務處理的復雜度,也避免了引入TP Monitor來協調多個Resource Manager之間事務的性能負擔。

     

Caution

局部事務與全局事務的主要區分在於“事務”中牽扯多少RM,而不是“系統”中實際有多少RM,這是需要我們注意的地方。 即使你系統中存在多個數據庫(即RM),只要你當前事務只更新一個數據庫的數據,那當前事務就依然應該算作局部事務,而不是全局事務(雖然這種情況下,你也可以啟用全局事務)。

實際上,針對單一事務資源的事務管理,你可以在局部事務中直接使用RM內置的事務支持來進行,你也可以引入TP Monitor在分布式事務場景中進行,通常情況下, 各TP Monitor在實現的時候會檢測參與事務的RM數目,如果只有單一的RM參與,TP Monitor會做一定的優化,避免采用“兩階段提交”協議的負擔, 但即使如此,針對單一事務資源參與的事務,直接采用局部事務中RM內置的事務支持,無論是從復雜度,還是從效率上來看,都要更勝一籌。

 

到此為止,我們所闡述的都是概念層面的東西,要真正的在系統開發中使用事務,我們需要相應的產品和API支持。 為了讓事務“走進現實”,不同的組織或者不同的技術平台會有各自不同的API設計和實現, 但既然Spring(不是Spring .Net)歸屬於Java一族,其他的平台和組織的解決方案暫且放於一邊,我們專門來看Java平台的事務解決方案如何?

1.2. 群雄逐鹿下的Java事務管理

對於應用程序的開發人員來說,更多時候,我們是通過相應產品提供的API接口來訪問事務資源,考慮如何在應用的業務邏輯中界定事務邊界,而對於各提供商如何在產品中實現事務支持, 則通常不是我們需要關心的問題。所以,以下內容將更多的圍繞着各事務處理場景下我們可以通過哪些產品提供的事務處理接口或者標准的事務處理接口來進行事務控制為主線進行。 當然,期間我們也可能提及相應場景下比較受歡迎的幾款事務處理的產品實現。

下面我們將按照從局部事務場景到全局事務場景的順序來看一下,各場景中Java平台為我們都准備了哪些可用的事務處理API。

1.2.1. Java平台的局部事務支持

在Java的局部事務場景中,系統中事務管理的具體處理方式會隨着所使用的數據訪問技術的不同而各異,我們不是使用專用的事務API來管理事務, 而是通過當前使用的數據訪問技術所提供的基於“connection” [7] 的API來管理事務。

  • 數據庫資源的局部事務管理.  要在對數據庫的訪問過程中進行事務管理,每一種數據訪問技術都提供了特定於它自身的事務管理API,比如JDBC是Java平台訪問關系數據庫最基礎的API,如果直接使用JDBC進行數據訪問的話,我們可以將數據庫連接(java.sql.Connection)的自動提交(AutoCommit)功能設置為false,改為手動提交來控制整個事務的提交或者回滾:

    Connection connection = null;
        boolean rollback = false;
        try
        {
        connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        // do data access with JDBC
        connection.commit();
        }
        catch(SQLException e)
        {
        e.printStackTrace(); // don't do this
        rollback = true;
        }
        finally
        {
        if(connection != null)
        {
        if(rollback)
        {
        try {
        connection.rollback();
        } catch (SQLException e) {
        e.printStackTrace(); // don't do this
        }
        }
        else
        {
        try {
        connection.close();
        } catch (SQLException e) {
        e.printStackTrace(); // don't do this
        }
        }
        }
        }
        
    而如果我們使用Hibernate進行數據訪問,我們就得使用Hibernate的Session進行數據訪問期間的事務管理[8]
    Session session = null;
        Transaction transaction = null;
        try
        {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();
        // do data access with Hibernate
        session.flush();
        transaction.commit();
        }
        catch(HibernateException e)
        {
        transaction.rollback();
        }
        finally
        {
        session.close();
        }
        
    同樣的,如果我們使用JDO,TopLink甚至JPA進行數據訪問的話,這些數據訪問技術也都在他們數據訪問API之上提供了相應的事務管理支持。

     

  • 消息服務資源的局部事務管理.  在使用JMS進行消息處理的過程中,我們可以通過JMS的javax.jms.Session來控制整個處理過程的事務:

    boolean rollback = false;
        Connection  con = null;
        Session session = null;
        try
        {
        con = cf.createConnection();
        session = con.createSession(true, Session.AUTO_ACKNOWLEDGE);
        // process Messages with JMS API
        session.commit();
        }
        catch (JMSException e) {
        e.printStackTrace();// don't do this
        rollback = true;
        }
        finally
        {
        if(con != null)
        {
        if(rollback)
        {
        try {
        session.rollback();
        } catch (JMSException e1) {
        e1.printStackTrace(); // don't do this
        }
        }
        else
        {
        try
        {
        con.close();
        } catch (JMSException e) {
        e.printStackTrace(); // don't do this
        }
        }
        }
        }
        
    我們在通過javax.jms.Connection的createSession方法創建javax.jms.Session的時候, 將該方法的第一個參數指定為true要求創建一個事務型的javax.jms.Session實例,然后就可以根據情況提交和回滾(rollback)事務了。

     

以上兩種情況之外的時候,我們可能需要借助於Java Connector Architecture(JCA)來管理局部事務,JCA允許通過javax.resource.spi.LocalTransaction接口暴露局部事務控制接口。 但是,JCA只能與JavaEE的應用服務器集成,所以,通過JCA訪問事務資源的應用程序需要綁定到相應的JavaEE服務器。

 

1.2.2. Java平台的分布式事務支持

Java平台上的分布式事務管理主要是通過Java Transaction API(JTA)或者Java Connector Architecture(JCA)提供支持的。

1.2.2.1. 基於JTA的分布式事務管理

JTA是Sun提出的標准化分布式事務訪問的Java接口規范。不過,JTA規范定義的只是一套Java接口定義,具體的實現留給了相應的提供商去實現,各JavaEE應用服務器需要提供對JTA的支持, 另外,除了可以使用綁定到各JavaEE應用服務器的JTA實現之外,Java平台上也存在幾個獨立的並且比較成熟的JTA實現產品,這包括:

 

使用JTA進行分布式事務管理通常有兩種方式,直接使用JTA接口的編程事務管理以及基於應用服務器的聲明性事務管理。

1.2.2.1.1. JTA編程事務管理

使用JTA進行分布式事務的編程式事務管理通常使用javax.transaction.UserTransaction接口進行,各應用服務器都提供了針對它的JNDI查找服務。

下面是典型的使用UserTransaction進行事務管理的代碼片斷:

try
{
UserTransaction ut = (UserTransaction)ctx.lookup("javax.transaction.UserTransaction");
ut.begin();
// 事務操作
ut.commit();
} catch (NamingException e) {
e.printStackTrace();
} catch (NotSupportedException e) {
e.printStackTrace();
} catch (SystemException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (RollbackException e) {
e.printStackTrace();
} catch (HeuristicMixedException e) {
e.printStackTrace();
} catch (HeuristicRollbackException e) {
e.printStackTrace();
}
如果是在EJB中使用UserTransaction的話,你可以直接通過javax.ejb.EJBContext獲得UserTransaction的引用:
UserTransaction ut = ctx.getUserTransaction();
...
在EJB中直接編程使用UserTransaction主要是BMT(Bean Managed Transaction)的情況下, 不過,在EJB中使用JTA進行分布式管理有比直接使用編程方式更吸引人的方式,那就是在CMT(Container Managed Transacton)情況下,EJB容器提供的聲明性的事務管理。

 

1.2.2.1.2. JTA聲明性事務管理

如果使用EJB進行聲明性的分布式事務管理的話(限於CMT的情況),JTA的使用則只限於EJB容器的內部,對於應用程序來說則完全就是透明的, 現在唯一需要做的工作實際上就是在相應的部署描述符中指定相應的事務屬性即可:

<enterprise-beans>
<session>
<display-name>Your Enterprise Java Bean</display-name>
<ejb-name>YourBean</ejb-name>
<home>...</home>
<remote>...</remote>
<ejb-class>...YourBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>YourBean</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
</assembly-descriptor>
<transaction-type>指定了我們讓EJB容器來管理的事務,並且,<trans-attribute>規定所有方法執行都需要相應事務, 現在,應用程序內部再也不用充斥着各種事務管理的代碼了。

 

1.2.2.2. 基於JCA的分布式事務管理

JCA規范主要面向Enterprise Information System(EIS)的集成,通過為遺留的EIS系統和JavaEE應用服務器指定統一的通信標准, 二者就可以實現各種服務上的互通。

當應用服務器通過JCA將一些EIS系統集成進來之后,我們就可以讓EIS系統中的事務資源也加入到JavaEE應用的全局事務中來。 實際上,要在應用程序中控制這種跨越多個系統的分布式事務,我們最終還是通過JTA來進行的,JCA更多的是提供資源的集成, 所以,從這一點兒來說,在Java平台上管理分布式事務,JTA是唯一標准接口。

1.2.3. 繼續前行之前的反思

你也看到了,Java平台提供的事務管理API足夠豐富,可謂高中低檔一應俱全,這當然有助於我們根據合適場景選用合適的事務管理API, 但,在實際使用過程中,過多的事務管理策略的選擇也會造成一些問題:

局部事務的管理綁定到了具體的數據訪問方式

使用JDBC進行數據訪問,你需要通過java.sql.Connection來控制局部事務; 使用Hibernate進行數據訪問,你需要通過org.hibernate.Session和org.hibernate.Transaction來管理局部事務, 至於其他的數據訪問方式,自然也是要使用它們特定的數據訪問API來控制局部事務。

這樣直接導致的問題就是事務管理代碼與數據訪問代碼甚至業務邏輯代碼混雜,因為局部事務場景下,我們使用的是數據訪問API進行事務控制。 如果實際使用中不能通過合適的方式對事物管理的代碼與數據訪問代碼或者業務邏輯代碼進行邏輯上的隔離, 將直接導致數據訪問代碼和業務邏輯代碼的可重用性降低,甚至事務管理代碼在數據訪問層和業務服務層的到處散落。

當前的情況是,各種數據訪問方式只提供了簡單的事務API,但沒有更高層次的抽象來幫助我們隔離事務與數據訪問兩個方面的過緊耦合。

事務的異常處理

事務處理過程中出現的異常應該都是不可恢復的,所以,應該拋出“unchecked exception”,並且有一個統一的父類,便於客戶端處理。 但是現在的情況是:

  • 沒有一個統一的事務相關異常體系,使用特定API管理事務的時候,你就需要捕捉這些 API特定的異常並處理;

  • 許多事務管理代碼在使用過程中拋出的依然還是“checked exception”強制客戶端代碼來捕捉並處理, 從UserTransaction的使用上你就可以看出來, 從JNDI查找到事務的開始和結束等操作,簡單的事務界定操作,卻引出七八個異常需要處理,任何一個人在使用UserTransaction進行編程式事務管理的時候也不會認為這樣的API設計很好用吧?!

 

事務處理API的多樣性

對於開發人員來說,所謂對事務的管理,最多也就是界定一下事務的邊界,規定事務什么地方開始,什么地方結束,可是,要達到這一個目的,你卻要在各種數據訪問API或者JTA之間徘徊。

各種事務管理API的存在給了我們更多選擇,但沒有一個統一的方式來抽象單一的事務管理需求,反而讓這種多種選擇的優勢變得繁雜而不易管理。

CMT聲明式事務的局限

EJB容器提供的CMT特性是比較受歡迎的事務界定管理方式,因為業務對象中不需要混雜任何事務管理代碼,所有的事務管理都通過一些簡單的配置交由容器來管理。 CMT提供的聲明式的事務管理很好的分離了事務管理與具體的數據資源訪問之間的耦合,使得開發人員能夠分別專心於業務邏輯和數據訪問邏輯的開發。

但CMT形式的聲明式事務管理有一個令人惋惜的限制,那就是,你必須借助於EJB容器才能得到這種聲明式事務的好處。 如果你的應用程序想要使用聲明式的事務管理,卻不得不花費不少銀子來購買應用服務器廠商的授權,或者,即使是使用開源的應用服務器, 但你的應用程序在此之前並沒有特別強烈的必須應用服務器的需求,這些情況下,引入應用服務器的做法應該不太容易令人接受吧?

總之,要使用CMT的聲明式事務管理,強制要求引入EJB容器的支持(當然,你也得以EJB的形式提供組件實現),某些情況下是不合適的。

鑒於這些問題,我們應該考慮對目前的狀況進行改進,以便簡化開發過程,提高整個過程中的開發效率:
  • 我們能否對眾多的基於數據訪問API的局部事務操作進行一個合理的抽象,以便隔離事務管理與數據資源訪問之間的過分耦合?

  • 能否在合適的地方將各種場景下事務處理拋出的“checked exception”進行一個合適的轉譯,以屏蔽事務處理過程中因使用不同的事務API所造成的差異性?

  • 既然對於我們來說,事務管理的需求很簡單,基本上就是事物界定的工作,那我們能否對事務的界定操作進行統一抽象,以屏蔽各種事務管理API的差異性,使得事務管理能夠以統一的編程模型來進行?

  • 既然聲明式的事務管理如此誘人,那么能否突破必須依賴EJB容器的限制,尋求一種能夠為普通的Java對象(POJO)提供聲明式事務的方式那?

如果這些問題也是你在考慮的話,那么,恭喜你,你不用從頭去探索這些問題的解決方法了,因為spring的事務抽象框架正是我們所要尋找的解決方案。

 

1.3. 一統河山后的Spring事務管理

spring的事務框架將開發過程中事務管理相關的關注點進行適當的分離,並對這些關注點進行合理的抽象,最終打造了一套使用方便卻功能強大的事務管理“利器”。 通過spring的事務框架,我們可以按照統一的編程模型來進行事務編程,卻不用關心所使用的數據訪問技術以及具體要訪問什么類型的事務資源; 並且,spring的事務框架與spring提供的數據訪問支持可以緊密結合,更是讓你在事務管理與數據訪問之間游刃有余,而最主要的,結合spring的AOP框架, spring的事務框架為我們帶來了只有CMT才有的使用聲明式事務管理的待遇,卻無需綁定到任何的應用服務器上。

其他溢美之詞咱就先放一邊,還是趕快進入正題吧!

1.3.1. spring事務王國的架構

spring的事務框架設計理念的基本原則在於:讓事務管理的關注點與數據訪問關注點相分離:

  • 當你在業務層使用事務的抽象API進行事務界定的時候,你不需要關心事務將要加諸於上的事務資源是什么,對不同的事務資源的管理將由相應的框架實現類來操心;

  • 當你在數據訪問層對可能參與事務的數據資源進行訪問的時候,只需要使用相應的數據訪問API進行數據訪問,卻不需要關心當前的事務資源如何參與事務或者是否需要參與事務,這同樣將由事務框架類來打理;

當以上兩個關注點被清晰的分離出來之后,對於我們開發人員來說,唯一需要關心的,就只是通過抽象后的事務管理API對當前事務進行界定而已:
public class FooService
{
private PlatformTransactionManager transactionManager;
public void serviceMethod()
{
TransactionDefinition definition = ...;
TransactionStatus txStatus = getTransactionManager().getTransaction(definition);
try
{
// dao1.doDataAccess();
// dao2.doDataAccess();
// ...
}
catch(DataAccessException e)
{
getTransactionManager().rollback(txStatus);
throw e;
}
catch(OtherNecessaryException e)
{
getTransactionManager().rollback(txStatus);
throw e;
}
getTransactionManager().commit(txStatus);
}
public PlatformTransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
}
縱使你數據訪問方式如何變換,我事務管理實現可以巋然不動。只要有了這樣一個統一的事務管理編程模型,剩下的聲明式事務管理自然就是錦上添花之作啦! 從此之后,事務管理就是事務管理,數據訪問只關心數據訪問,再也不用因為他們之間的糾纏而煩惱。

 

1.3.1.1. 統一中原的過程

org.springframework.transaction.PlatformTransactionManager是spring事務抽象架構的核心接口,他的主要作用在於為應用程序提供事務界定的統一方式。 既然事務界定的需要很簡單,那么PlatformTransactionManager的定義看起來也不會過於復雜:

public interface PlatformTransactionManager
{
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
PlatformTransactionManager是整個事務抽象策略的頂層接口,它就好象我們的戰略藍圖,而戰略的具體實施則將由相應的PlatformTransactionManager實現類來執行。

 

spring的事務框架針對不同的數據訪問方式以及全局事務場景,提供了相應的PlatformTransactionManager實現類,當每一個實現類的職責完成之后,spring事務框架的“統一大業”就算完成了。 在深入了解各個PlatformTransactionManager實現類的奧秘之前,我們不妨先考慮一下,如果讓我們來實現一個PlatformTransactionManager,要如何去做那?

不妨先以針對JDBC數據訪問方式的局部事務管理為例。對於層次划分清晰的應用來說,我們通常都是將事務管理放在Service層,而將數據訪問邏輯放在DAO層,這樣做的目的在於可以不用因為將事務管理代碼放在DAO層而降低數據訪問邏輯的重用性, 也可以在Service層根據相應邏輯來決定提交(commit)或者回滾(rollback)事務。一般的Service對象可能需要在同一個業務方法中調用多個數據訪問對象的方法,類似於這樣的情況:

Figure 1.4. 普通情況下的事務管理代碼

普通情況下的事務管理代碼

因為JDBC的局部事務控制是由同一個java.sql.Connection來控制的,所以,要保證兩個DAO的數據訪問方法處於一個事務中,我們就得保證他們使用的是同一個java.sql.Connection,要做到這一點, 通常采用稱之為“ connection passing”的方式,即為同一個事務中的各個dao的數據訪問方法傳遞當前事務對應的同一個java.sql.Connection,這樣,我們的業務方法以及數據訪問方法都得做一定的修改:

Figure 1.5. connection-passing方式的事務管理代碼

connection-passing方式的事務管理代碼

我們只要把java.sql.Connection的獲取並設置autoCommit狀態的代碼以及使用java.sql.Connection提交事務的代碼重構到原來的開啟事務以及提交事務的方法中,針對JDBC的局部事務管理的整合看起來離成功也就是咫尺之遙了。 不過,這看起來的咫尺之遙,實際上卻依然遙遠。

 

使用這種方式,最致命的一個問題就在於,不但事務管理代碼無法擺脫java.sql.Connection的糾纏,而且數據訪問對象的定義要綁定到具體的數據訪問技術上來。 現在是使用JDBC進行數據訪問,你要在數據訪問方法中聲明對java.sql.Connection的依賴,那要是使用HIbernate的話,是不是要聲明對Session的依賴那?顯然,這樣的做法是不可行的。 不過好消息是,傳遞Connection的理念是對的,只不過,我們具體實施過程中所采用的方法不對頭。

要傳遞java.sql.Connection,我們可以將整個事務對應的java.sql.Connection實例放到統一的一個地方去,無論是誰,要使用該資源,都從這一個地方來獲取,這樣就解除了事務管理代碼和數據訪問代碼之間通過java.sql.Connection的“直接”耦合。 具體一點兒說就是,我們在事務開始之前取得一個java.sql.Connection,然后將這個Connection綁定到當前的調用線程,之后,數據訪問對象在使用Connection進行數據訪問的時候,就可以從當前線程上去獲得這個事務開始的時候綁定的Connection實例, 當所有的數據訪問對象全部使用這個綁定到當前線程的Connection完成了數據訪問工作,我們就使用這個Connection實例提交或者回滾事務,然后解除它到當前線程的綁定。

Figure 1.6. java.sql.Connection綁定到線程示意圖

java.sql.Connection綁定到線程示意圖

這個時候的java.sql.Connection就像那大河上的一條船,從啟航(事務開始)到航程結束(事務完成),在這整個期間,大河沿岸都可以與該船打交道,而至於說你是發“ 木船”>還是發“ 鐵輪”, 那由你來決定了,發JDBC的船,那就是Connection,發Hibernate的船,那就是Session...

 

假設TransactionResourceManager就是我們存放java.sql.Connection(或者其他事務資源)的地方,那么,它看起來可能的樣子如下(過多的邏輯檢驗代碼略去):

public class TransactionResourceManager
{
private static ThreadLocal resources = new ThreadLocal();
public static Object getResource()
{
return resources.get();
}
public static void bindResource(Object resource)
{
resources.set(resource);
}
public static Object unbindResource()
{
Object res = getResource();
resources.set(null);
return res;
}
}
對於我們要實現的針對JDBC的PlatformTransactionManager,只需要在事務開始將java.sql.Connection通過我們的TransactionResourceManager綁定,然后在事務結束解除綁定即可:
public class JdbcTransactionManager implements PlatformTransactionManager
{
private DataSource dataSource;
public JdbcTransactionManager(DataSource dataSource)
{
this.dataSource = dataSource;
}
public TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException {
Connection connection;
try
{
connection = dataSource.getConnection();
TransactionResourceManager.bindResource(connection);
return new DefaultTransactionStatus(connection,true,true,false,true,null);
}
catch (SQLException e)
{
throw new CannotCreateTransactionException("can't get connection for tx",e);
}
}
public void rollback(TransactionStatus txStatus) throws TransactionException {
Connection connection = (Connection)TransactionResourceManager.unbindResource();
try
{
connection.rollback();
}
catch (SQLException e)
{
throw new UnexpectedRollbackException("rollback failed with SQLException",e);
}
finally
{
try {
connection.close();
} catch (SQLException e) {
// log information but can do nothing
}
}
}
public void commit(TransactionStatus txStatus) throws TransactionException {
Connection connection = (Connection)TransactionResourceManager.unbindResource();
try
{
connection.commit();
}
catch (SQLException e)
{
throw new TransactionSystemException("commit failed with SQLException",e);
}
finally
{
try {
connection.close();
} catch (SQLException e) {
// log information but can do nothing
}
}
}
}
因為Connection在事務開始和結束期間都可以通過我們的TransactionResourceManager獲得,所以,所有的DAO層數據訪問對象在使用JDBC進行數據訪問的時候,就可以直接從TransactionResourceManager來獲得數據庫連接並進行數據訪問,這樣就可以保證在整個事務期間,所有的數據訪問對象對應的同一個Connection。
public class FooJdbcDao implements IDao
{
public void doDataAccess()
{
Connection con = (Connection)TransactionResourceManager.getResource();
// ...
}
}
至此,我們完成了PlatformTransactionManager具體實現類並解除了它與相應數據訪問對象之間通過java.sql.Connection的直接耦合,進行事務控制的時候,我們只需要為Service對象提供相應的PlatformTransactionManager實現類, Service對象中的事務管理功能就算大功告成了,而不需要關心到底對應的是什么樣的事務資源,甚至什么樣的數據訪問方式。

 

當然,為了便於讀者理解spring抽象層的實現原理,以上的代碼實例都是簡化后的模型,所以,不要試圖將他們應用於生產環境。 原型代碼永遠都是原型代碼,要做的事情還有許多,比如:

  • 如何保證PlatformTransactionManager的相應方法被以正確的順序調用,如果哪一個方法沒有被正確調用,也會造成資源泄漏以及事務管理代碼混亂的問題。

    在稍后為讀者介紹使用spring進行編程事務管理的時候,你將看到spring是如何解決這個問題的。

  • 數據訪問對象的接口定義不會因為最初的‘connection passing’方式而改變契約了,但是,現在卻要強制使用每個數據訪問對象使用TransactionResourceManager來獲取數據資源接口, 另外,如果當前數據訪問對象對應的數據方法不想參與跨越多個數據操作的事務的時候,甚至於不想(或不能)使用類似的事務管理支持,是否就意味着無法獲得connection進行數據訪問了那? 

    不知道你是否還記得我們在介紹spring的數據訪問一章內容的時候,曾經提到的org.springframework.jdbc.datasource.DataSourceUtils工具類, 當時我們只是強調了DataSourceUtils提供的異常轉譯能力,實際上,DataSourceUtils最主要工作卻在於對connection的管理,DataSourceUtils會從類似TransactionResourceManager的類(spring中對應org.springframework.transaction.support.TransactionSynchronizationManager)那里 獲取Connection資源,如果當前線程之前沒有綁定任何connection,他就通過數據訪問對象的DataSource引用獲取新的connection,否則就使用綁定的那個connection。 這就是為什么要強調,當我們要使用spring提供的事務支持的時候,必須通過DataSourceUtils來獲取連接的原因,因為它提供了spring事務管理框架在數據訪問層需要提供的基礎設施中不可或缺的一部分,而JdbcTemplate等類內部已經使用DataSourceUtils來管理連接了,所以,我們不用操心這些細節。 從這里,你也應可以看出,spring的事務管理與它的數據訪問框架是緊密結合的。

    Note

    對應Hibernate的SessionFactoryUtils,對應JDO的PersistenceManagerFactoryUtils以及對應其他數據訪問技術的Utils類, 他們的作用與DataSourceUtils是相似的,除了提供異常轉譯功能,他們更多的用於數據訪問資源的管理工作,以配合對應的PlatformTransactionManager實現類進行事務管理。

     

實際上,spring在實現針對各種數據訪問技術的PlatformTransactionManager的時候要考慮很多的東西,不像原型以及提出的幾個問題所展示的那么簡單, 不過,各個實現類的基本思路與原型所展示的是基本吻合的,當我們了解了針對Jdbc的PlatformTransactionManager是如何實現的時候,其他的實現類基本上就是平推了。

 

1.3.1.2. 和平年代

spring的事務抽象包括三個主要接口,即PlatformTransactionManager,TransactionDefinition以及TransactionStatus,他們之間的關系如下:

Figure 1.7. spring事務抽象接口關系圖

spring事務抽象接口關系圖

三接口以org.springframework.transaction.PlatformTransactionManager為中心,互為犄角,多少有點兒“ 晉西北鐵三角”的味道。 org.springframework.transaction.PlatformTransactionManager負責界定事務邊界,org.springframework.transaction.TransactionDefinition負責定義事務相關屬性,包括隔離級別,傳播行為等, org.springframework.transaction.PlatformTransactionManager將參照org.springframework.transaction.TransactionDefinition的屬性定義來開啟相關事務,事務開啟之后到事務結束期間的事務狀態由org.springframework.transaction.TransactionStatus負責, 我們也可以通過org.springframework.transaction.TransactionStatus對事務進行有限的控制。

 

1.3.1.2.1. TransactionDefinition
1.3.1.2.1.1. TransactionDefinition簡介

org.springframework.transaction.TransactionDefinition主要定義了有哪些事務屬性可以指定,這包括:

  • 事務的隔離級別(Isolation)

  • 事務的傳播行為(Propagation Behavisor)

  • 事務的超時時間(Timeout)

  • 是否為只讀事務(ReadOnly)

 

TransactionDefinition內定義了五個常量用於標志可供選擇的隔離級別:

  • ISOLATION_DEFAULT. 如果指定隔離級別為ISOLATION_DEFAULT,則表示使用數據庫默認的隔離級別,通常情況下是“read committed”;

  • ISOLATION_READ_UNCOMMITTED. 對應“Read Uncommitted”隔離級別,無法避免臟讀,不可重復讀和幻讀;

  • ISOLATION_READ_COMMITTED. 對應“Read Committed”隔離級別,可以避免臟讀,但無法避免不可重復讀和幻讀;

  • ISOLATION_REPEATABLE_READ. 對應“Repeatable read”隔離級別,可以避免臟讀和不可重復讀,但不能避免幻讀;

  • ISOLATION_SERIALIZABLE. 對應“Serializable”隔離級別,可以避免所有的臟讀,不可重復讀以及幻讀,但並發性效率最低;

事務的傳播行為(Propagation Behavisor)我們之前沒有提到,不過,如果你接觸過EJB的CMT的話,對它應該也不會陌生。 事務的傳播行為(Propagation Behavisor)表示整個事務處理過程所跨越的業務對象將以什么樣的行為參與事務(我們將在聲明式事務中更多的依賴於該屬性)。 比如,當有FoobarService調用FooService和BarService兩個方法的時候,FooService的業務方法和BarService的業務方法可以指定它們各自的事務傳播行為:

Figure 1.8. 業務方法的傳播行為

業務方法的傳播行為

FooService的業務方法的傳播行為被我們指定為Required,表示如果當前存在事務的話,則加入當前事務,因為FoobarService在調用FooService的業務方法的時候已經啟動了一個事務,所以,FooSerivce的業務方法會直接加入FoobarService啟動的“ 事務1”中; BarService的業務方法的傳播行為被指定為Required New,表示無論當前是否存在事務,都需要為其重新啟動一個事務,所以,它使用的是自己啟動的“ 事務2”。

 

TransactionDefinition針對事務的傳播行為提供了以下幾種選擇,除了PROPAGATION_NESTED是spring特有的外,其他的傳播行為的語義與CMT基本相同:

  • PROPAGATION_REQUIRED.  如果當前存在一個事務,則加入當前事務;如果不存在任何事務,則創建一個新的事務。總之,要至少保證在一個事務中運行。PROPAGATION_REQUIRED通常作為默認的事務傳播行為。

  • PROPAGATION_SUPPORTS.  如果當前存在一個事務,則加入當前事務;如果當前不存在事務,則直接執行。 對於一些查詢方法來說,PROPAGATION_SUPPORTS通常是比較合適的傳播行為選擇。 如果當前方法直接執行,那么不需要事務的支持;如果當前方法被其他方法調用,而其他方法啟動了一個事務的時候,使用PROPAGATION_SUPPORTS可以保證當前方法能夠加入當前事務並洞察當前事務對數據資源所做的更新。 比如說,A.service()會首先更新數據庫,然后調用B.service()進行查詢,那么,B.service()如果是PROPAGATION_SUPPORTS的傳播行為, 就可以讀取A.service()之前所做的最新更新結果,而如果使用稍后所提到的PROPAGATION_NOT_SUPPORTED,則B.service()將無法讀取最新的更新結果,因為A.service()的事務在這個時候還沒有提交(除非隔離級別是read uncommitted):

    Figure 1.9. PROPAGATION_SUPPORTS可能場景

    PROPAGATION_SUPPORTS可能場景

     

  • PROPAGATION_MANDATORY.  PROPAGATION_MANDATORY強制要求當前存在一個事務,如果不存在,則拋出異常。 如果某個方法需要事務支持,但自身又不管理事務提交或者回滾的時候,比較適合使用PROPAGATION_MANDATORY。 你可以參照《JAVA TRANSACTION DESIGN STRATEGIES》一書中對REQUIRED和MANDATORY兩種傳播行為的比較來更深入的了解PROPAGATION_MANDATORY的可能應用場景。

  • PROPAGATION_REQUIRES_NEW.  不管當前是否存在事務,都會創建新的事務。如果當前存在事務的話,會將當前的事務掛起(suspend)。 如果某個業務對象所做的事情不想影響到外層事務的話,PROPAGATION_REQUIRES_NEW應該是合適的選擇,比如,假設當前的業務方法需要向數據庫中更新某些日志信息, 但即使這些日志信息更新失敗,我們也不想因為該業務方法的事務回滾而影響到外層事務的成功提交,因為這種情況下,當前業務方法的事務成功與否對外層事務來說是無關緊要的。

  • PROPAGATION_NOT_SUPPORTED.  不支持當前事務,而是在沒有事務的情況下執行。如果當前存在事務的話,當前事務原則上將被掛起(suspend),但要依賴於對應的PlatformTransactionManager實現類是否支持事務的掛起(suspend),更多情況請參照TransactionDefinition的javadoc文檔。 PROPAGATION_NOT_SUPPORTED與PROPAGATION_SUPPORTS之間的區別,可以參照PROPAGATION_SUPPORTS部分的實例內容。

  • PROPAGATION_NEVER.  永遠不需要當前存在事務,如果存在當前事務,則拋出異常。

  • PROPAGATION_NESTED.  如果存在當前事務,則在當前事務的一個嵌套事務中執行,否則與PROPAGATION_REQUIRED的行為類似,即創建新的事務,在新創建的事務中執行。 PROPAGATION_NESTED粗看起來好像與PROPAGATION_REQUIRES_NEW的行為類似,實際上二者是有差別的。 PROPAGATION_REQUIRES_NEW創建的新事務與外層事務屬於同一個“檔次”,即二者的地位是相同的,當新創建的事務運行的時候,外層事務將被暫時掛起(suspend); 而PROPAGATION_NESTED創建的嵌套事務則不然,它是寄生於當前外層事務的,它的地位比當前外層事務的地位要小一號,當內部嵌套事務運行的時候,外層事務也是出於active狀態:

    Figure 1.10. PROPAGATION_REQUIRES_NEW與PROPAGATION_NESTED創建的事務的區別

    PROPAGATION_REQUIRES_NEW與PROPAGATION_NESTED創建的事務的區別

    也就是說,PROPAGATION_REQUIRES_NEW新創建的事務雖然是在當前外層事務內執行,但新創建的事務是獨立於當前外層事務而存在的,二者擁有各自獨立的狀態而互不干擾; 而PROPAGATION_NESTED創建的事務屬於當前外層事務的內部子事務(sub-transaction),內部子事務的處理內容屬於當前外層事務的一部分,而不能獨立於外層事務而存在,並且與外層事務共有事務狀態,我想這也就是為什么稱其為內部嵌套事務的原因。

     

    PROPAGATION_NESTED可能的應用場景在於,你可以將一個大的事務划分為多個小的事務來處理,並且外層事務可以根據各個內部嵌套事務的執行結果來選擇不同的執行流程。 比如,某個業務對象的業務方法A.service()可能調用其他業務方法B.service()向數據庫中插入一批業務數據,但當插入數據的業務方法出現錯誤的時候(比如主鍵沖突),我們可以在當前事務中捕捉前一個方法拋出的異常,然后選擇另一個更新數據的業務方法C.service()來執行, 這個時候,我們就可以把B.service()和C.serivce()方法的傳播行為指定為PROPAGATION_NESTED[9],如果用偽代碼演示的話,看起來如下:

    /**
        * PROPAGATION_REQUIRED
        */
        A.service()
        {
        try
        {
        // PROPAGATION_NESTED
        B.service();
        }
        catch(Exception e)
        {
        // PROPAGATION_NESTED
        C.service();
        }
        }
        
    不過,並非所有的PlatformTransactionManager實現都支持PROPAGATION_NESTED類型的傳播行為,現在只有org.springframework.jdbc.datasource.DataSourceTransactionManager在使用JDBC3.0數據庫驅動的情況下才支持(當然,數據庫和相應的驅動程序也需要提供支持),另外, 某些JtaTransactionManager也可能提供支持,但JTA規范並沒有要求提供對嵌套事務的支持。

     

對TransactionDefinition所提供的這幾個傳播行為選項的使用,最好是建立在充分理解的基礎之上,當然,除非特殊的場景,通常情況下,PROPAGATION_REQUIRED將依然是我們最多的選擇。

 

TransactionDefinition提供了TIMEOUT_DEFAULT常量定義,用來指定事務的超時時間,TIMEOUT_DEFAULT默認值為-1,這會采用當前事務系統默認的超時時間,你將可以通過TransactionDefinition的具體實現類提供自定義的事務超時時間。

TransactionDefinition提供的最后一個重要信息就是將要創建的是否是一個只讀(ReadOnly)的事務,如果你需要創建一個只讀的事務的話,可以通過TransactionDefinition的相關實現類進行設置。 只讀的事務僅僅是給相應的ResourceManager提供一種優化的提示,但最終是否提供優化,則由最終的ResourceManager決定。對於一些查詢來說,我們通常會希望它們采用只讀事務。

1.3.1.2.1.2. TransactionDefinition相關實現

TransactionDefinition僅僅是一個接口定義,要為PlatformTransactionManager創建事務提供信息,需要有相應的實現類提供支持。 TransactionDefinition的相關實現類雖然不多,但為了便於理解,我們依然將他們划分為“兩派”:

Figure 1.11. TransactionDefinition繼承層次圖

TransactionDefinition繼承層次圖

將TransactionDefinition的相關實現類按照“ 編程式事務場景”和“ 聲明式事務場景”划分為兩個分支, 僅僅是出於每個類在相應場景中出現的頻率方面考慮的,而不是說“ 聲明式事務場景”的實現類不能在“ 編程式事務場景”使用。

 

org.springframework.transaction.support.DefaultTransactionDefinition是TransactionDefinition接口的默認實現類, 他提供了各事務屬性的默認值,並且通過它的setter方法,你可以更改這些默認設置。這些默認值包括:

  • propagationBehavior = PROPAGATION_REQUIRED

  • isolationLevel = ISOLATION_DEFAULT

  • timeout = TIMEOUT_DEFAULT

  • readOnly = false

org.springframework.transaction.support.TransactionTemplate是spring提供的進行編程式事務管理的模板方法類(我們將稍后提到該類的使用), 通過直接繼承DefaultTransactionDefinition,我們在使用TransactionTemplate的時候就可以直接通過TransactionTemplate本身提供事務控制屬性。

 

org.springframework.transaction.interceptor.TransactionAttribute是繼承自TransactionDefinition的接口定義, 主要面向使用Spring AOP進行聲明式事務管理的場合,它在TransactionDefinition定義的基礎上添加了一個rollbackOn方法:

boolean rollbackOn(Throwable ex);
這樣,我們可以通過聲明的方式指定業務方法在拋出哪些的異常的情況下可以回滾(rollback)事務。

 

TransactionAttribute的默認實現類是DefaultTransactionAttribute,他同時繼承了DefaultTransactionDefinition, 在DefaultTransactionDefinition的基礎上追加了rollbackOn的實現,DefaultTransactionAttribute的實現指定當異常類型為“unchecked exception”的情況下將回滾(rollback)事務。

DefaultTransactionAttribute之下有兩個實現類,即RuleBasedTransactionAttribute以及DelegatingTransactionAttribute。 RuleBasedTransactionAttribute允許我們同時指定多個回滾規則,這些規則以包含org.springframework.transaction.interceptor.RollbackRuleAttribute或者org.springframework.transaction.interceptor.NoRollbackRuleAttribute的List形式提供, RuleBasedTransactionAttribute的rollbackOn將使用傳入的異常類型與這些回滾規則進行匹配,然后再決定是否要回滾事務。

DelegatingTransactionAttribute是抽象類,它存在的目的就是被子類化,DelegatingTransactionAttribute會將所有方法調用委派給另一個具體的TransactionAttribute實現類, 比如DefaultTransactionAttribute或者RuleBasedTransactionAttribute,不過,除非不是簡單的直接委派(什么附加邏輯都不添加),否則,實現一個DelegatingTransactionAttribute是沒有任何意義的。

1.3.1.2.2. TransactionStatus

org.springframework.transaction.TransactionStatus接口定義表示整個事務處理過程中的事務狀態, 我們將更多時候在編程式事務中使用該接口。

在事務處理過程中,我們可以使用TransactionStatus進行如下工作:

  • 使用TransactionStatus提供的相應方法查詢事務狀態;

  • 通過setRollbackOnly()方法標記當前事務以回滾(rollback);

  • 如果相應的PlatformTransactionManager支持Savepoint,可以通過TransactionStatus在當前事務中創建內部嵌套事務;

在稍后為讀者介紹如何使用spring進行編程式事務管理部分,可以更直觀的了解這些。

 

TransactionStatus的實現層次比較簡單,見下圖:

Figure 1.12. TransactionStatus繼承層次

TransactionStatus繼承層次

org.springframework.transaction.SavepointManager是在JDBC3.0的基礎上對Savepoint的支持提供的抽象, 通過繼承SavepointManager,TransactionStatus獲得可以管理Savepoint的能力,從而支持創建內部嵌套事務。

 

org.springframework.transaction.support.AbstractTransactionStatus為TransactionStatus的抽象類實現, 主要為其他實現子類提供一些“公共設施”,它下面主要有兩個子類, DefaultTransactionStatus和SimpleTransactionStatus, 其中,DefaultTransactionStatus是spring事務框架內部使用的主要TransactionStatus實現類,spring事務框架內的各個TransactionManager的實現大都借助於DefaultTransactionStatus來記載事務狀態信息。 SimpleTransactionStatus在spring框架內部的實現中沒有使用到,目前來看,主要用於測試目的。

1.3.1.2.3. PlatformTransactionManager

PlatformTransactionManager是spring事務抽象框架的核心組件,關於它的定義以及作用我們之前已經提過了,所以,這部分我們不妨更多的關注一下PlatformTransactionManager整個的層次體系以及針對不同數據訪問技術的實現類。

PlatformTransactionManager整個的抽象體系基於Strategy模式,由PlatformTransactionManager對事務界定進行統一抽象,而具體的界定策略的實現則交由具體的實現類。 下面我們先來看一下有哪些實現類可供我們使用...

1.3.1.2.3.1. PlatformTransactionManager實現類概覽

PlatformTransactionManager的實現類可以划分到面向局部事務和面向全局事務兩個分支:

  • 面向局部事務的PlatformTransactionManager實現類.  spring為各種數據訪問技術提供了現成的PlatformTransactionManager實現支持,以下列表給出了各種數據訪問技術與它們對應的實現類的關系:

    Table 1.1. 數據訪問技術與PlatformTransactionManager實現類對應關系

    數據訪問技術 PlatformTransactionManager實現類
    JDBC/iBatis DataSourceTransactionManager
    Hibernate HibernateTransactionManager
    JDO JdoTransactionManager
    JPA(Java Persistence API) JpaTransactionManager
    TopLink TopLinkTransactionManager
    JMS JmsTransactionManager
    JCA Local Transaction CciLocalTransactionManager

    在這些實現類當中,CciLocalTransactionManager可能是比較少見的實現,CCI的意思是Common Client Interface, CciLocalTransactionManager主要是面向JCA的局部事務(Local Transaction),本書不打算對JCA的集成做過多的闡述,讀者如果在實際項目中需要使用到JCA進行EIS(Enterprise Information System)系統集成,你可以從spring的參考文檔獲得使用spring提供的JCA集成支持的足夠信息。

     

    有了這些實現類,我們在使用spring的事務抽象框架進行事務管理的時候,只需要根據當前使用的數據訪問技術選擇對應的PlatformTransactionManager實現類即可。

    Tip

    如果你的應用程序需要同時使用Hibernate以及JDBC(或者iBatis)進行數據訪問,那么你可以使用HibernateTransactionManager對基於Hibernate和JDBC(或者iBatis)的事務進行統一管理, 只要Hibernate的SessionFactory和JDBC(或者iBatis)引用的是同一個DataSource就行。你能猜到為什么嗎?

  • 面向全局事務的PlatformTransactionManager實現類. org.springframework.transaction.jta.JtaTransactionManager是spring提供的支持分布式事務的PlatformTransactionManager實現。 直接使用JTA規范接口進行分布式事務管理有以下幾個問題:

    • UserTransaction接口使用復雜不說(一長串的異常處理我們之前也見過了),它所暴露的事務管理能力有限,對於事務的掛起(suspend)以及恢復(resume)操作,只有JTA的TransactionManager才支持;

    • JTA規范並沒有明確要求對TransactionManager的支持,這就造成當下各個JTA提供商雖然提供了TransactionManager的實現,但在應用服務器中暴露的位置各有差異,為了進一步支持REQUIRES_NEW和NOT_SUPPORTED之類需要事務掛起以及恢復操作的事務傳播行為, 我們需要通過不同的方式來獲取不同JTA提供商暴露的TransactionManager實現;

    鑒於此,JtaTransactionManager對各種JTA實現提供的分布式事務支持進行了統一封裝,只不過它的所有的事務管理操作最終都會委派給具體的JTA實現來完成。

     

    對於典型的基於JTA的分布式事務管理,我們直接使用JtaTransactionManager就可以了,但某些時候,需要更多使用到各JTA產品的TransactionManager特性的時候, 這個時候,我們就可以為JtaTransactionManager注入這些JTA產品的javax.transaction.TransactionManager的實現, 而至於說你是通過應用服務器獲取該TransactionManager,還是直接使用本地定義的TransactionManager(比如JOTM或者Atomikos等獨立JTA實現產品的TransactionManager), 則完全由你根據當時的場景來決定了。 能夠為JtaTransactionManager提供具體的TransactionManager實現為我們擴展JtaTransactionManager提供了很好的一個切入點。

    JtaTransactionManager有兩個子類OC4JJtaTransactionManager和WebLogicJtaTransactionManager,分別面向基於Oracle OC4J和Weglogic的JTA分布式事務管理, 在這些情況下,使用具體的子類來代替通常的JtaTransactionManager。不過,大多數情況下,使用spring提供的FactoryBean機制來獲取不同JTA提供商提供的TransactionManager實現,然后注入JtaTransactionManager使用,是比較好的做法, org.springframework.transaction.jta包下,spring提供了面向JOTM,Weblogic和Websphere的TransactionManager查找FactoryBean實現,如果需要,你也可以根據情況實現其他的TransactionManager實現的查找FactoryBean。

 

有了spring的事務抽象框架,事務管理策略的轉換也變得很簡單,通常也只是簡單的配置文件變更而已。 如果我們最初只需要處理單一資源的事務管理,那么,局部場景中的面向不同數據訪問技術的PlatformTransactionManager實現將是我們的最佳選擇,即使后來需要引入分布式資源的事務管理, 對於我們來說,也僅僅是從局部事務場景中的某個PlatformTransactionManager實現轉向JtaTransactionManager的變動而已,無論是編程式注入還是通過spring的IoC容器注入,對於應用程序來說都不會造成很大的變動。
1.3.1.2.3.2. 窺一斑而知全豹

PlatformTransactionManager的各個子類在實現的時候基本上遵循統一的結構和理念,所以,我們不妨選擇以DataSourceTransactionManager這一實現類作為切入點, 以管中窺豹之勢,一探spring的抽象事務框架中各個PlatformTransactionManager實現類的奧秘之所在。

不過,在開始之前,我們有必要先了解幾個概念:

transaction object

transaction object承載了當前事務的必要信息,PlatformTransactionManager實現類可以根據transaction object所提供的信息來決定如何處理當前事務。 transaction object的概念類似於JTA規范中的javax.transaction.Transaction定義。

TransactionSynchronization

TransactionSynchronization是可以注冊到事務處理過程中的回調(callback)接口,它就像是事務處理的事件監聽器, 當事務處理的某些規定時點發生的時候,會調用TransactionSynchronization上的一些方法來執行相應的回調邏輯,比如在事務完成后清理相應的系統資源等操作。

spring事務抽象框架所定義的TransactionSynchronization類似於JTA規范的javax.transaction.Synchronization,但比JTA的Synchronization提供了更多的回調方法, 允許我們對事務的處理添加更多的回調邏輯。

TransactionSynchronizationManager

類似於JTA規范中的javax.transaction.TransactionSynchronizationRegistry, 我們通過TransactionSynchronizationManager來管理TransactionSynchronization,當前事務狀態以及具體的事務資源。 在介紹spring事務框架實現原理的原型中,我們提到會將具體的事務資源,比如java.sql.Connection或者hibernate Session綁定到線程,而TransactionSynchronizationManager就是這些資源綁定的目的地,當然,從該類的名字也可以看出, 它更多關注與事務相關的Synchronization的管理。

之所以將他們與JTA規范中定義的接口相提並論是因為,這些概念只限於局部場景中對應的PlatformTransactionManager實現類使用,JtaTransactionManager直接就使用對應的JTA產品提供的對應設施了,JtaTransactionManager的最終工作都是委派給具體的JTA實現產品,記得嗎?!

 

OK,有了這些鋪墊,我們開始進入正題...

spring的事務抽象框架是以PlatformTransactionManager作為頂層抽象接口,具體的實現交給不同的實現類,使用對象可以根據當前場景選擇使用或者替換哪一個具體的實現類, 從這個層次看,整個框架的設計是以Strategy模式為基礎的。 不過,從各個實現類的繼承層次上來看,spring事務框架的實現則更多的依賴於模板方法模式:

Figure 1.13. DataSourceTransactionManager的實現層次

DataSourceTransactionManager的實現層次

org.springframework.transaction.support.AbstractPlatformTransactionManager作為DataSourceTransactionManager的父類, 以模板方法的形式封裝了固定的事務處理邏輯,而只將與事務資源相關的操作以protected或者abstract方法的形式下放給DataSourceTransactionManager來實現。 作為模板方法父類,AbstractPlatformTransactionManager替各子類實現了以下固定的事務內部處理邏輯:
  • 判定是否存在當前事務,然后根據判斷結果執行不同的處理邏輯;

  • 結合是否存在當前事務的情況,根據TransactionDefinition中指定的傳播行為(Propagation)的不同語義執行后繼邏輯;

  • 根據情況掛起(suspend)或者恢復(resume)事務;

  • 提交事務之前檢查readOnly字段是否被設置,如果是的話,以事務的回滾代替事務的提交;

  • 事務回滾的情況下清理並恢復事務狀態;

  • 如果事務的synchonization處於active狀態,在事務處理的規定時點觸發注冊的synchonization回調接口;

這些固定的事務內部處理邏輯大都體現在以下幾個主要的模板方法中:
  • public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException

  • public final void rollback(TransactionStatus status) throws TransactionException

  • public final void commit(TransactionStatus status) throws TransactionException

  • protected final SuspendedResourcesHolder suspend(Object transaction) throws TransactionException

  • protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) throws TransactionException

我們可不打算這幾個模板方法都捋一遍,畢竟只要了解了前面兩個或者三個模板方法的流程,整個事務處理的圖景基本上就可以展現在我們眼前了。

 

我們先從第一個模板方法getTransaction(TransactionDefinition)開始。 getTransaction(TransactionDefinition)的主要目的是開啟一個事務,但需要在此之前判斷一下之前是否存在一個事務,如果存在,則根據TransactionDefinition中的傳播行為決定是掛起當前事務還是拋出異常;同樣的,不存在事務的情況下,也需要根據傳播行為的具體語義來決定如何處理。 getTransaction(TransactionDefinition)方法的處理邏輯基本上按照下面的流程執行[10]

Procedure 1.1. getTransaction(TransactionDefinition)執行流程

  1. 獲取transaction object,以判斷是否存在當前事務

     

    Object transaction = doGetTransaction();
    這行代碼有兩點需要申明:
    1. 獲取的transaction object類型會因具體實現類的不同而各異,DataSourceTransactionManager會返回DataSourceTransactionManager.DataSourceTransactionObject類型實例, HibernateTransactionManager會返回HibernateTransactionManager.HibernateTransactionObject類型的實例,等等。 AbstractPlatformTransactionManager不需要知道具體實現類返回的transaction object具體類型是什么,因為最終對transaction object的依賴都將通過方法參數進行傳遞, 只要具體的實現類在取得transaction object參數后知道如何轉型就行,所以,這一步返回的transaction為Object類型;

    2. doGetTransaction()為getTransaction(TransactionDefinition)模板方法暴露給子類來實現的abstract類型方法,DataSourceTransactionManager在實現doGetTransaction()方法邏輯的時候, 會從TransactionSynchronizationManager獲取綁定的資源,然后添加到DataSourceTransactionObject之后返回。以此類推,其他AbstractPlatformTransactionManager子類都采用類似的邏輯實現了doGetTransaction()方法。

     

  2. 獲取Log類的Debug信息,避免之后的代碼重復

     

    boolean debugEnabled = logger.isDebugEnabled();
        if (debugEnabled) {
        logger.debug("Using transaction object [" + transaction + "]");
        }
        
    debugEnabled將以方法參數的形式在各方法調用間傳遞,以避免每次都調用logger.isDebugEnabled()獲取debug日志狀態。這一步與具體的事務處理流程關系不大。

     

  3. 檢查TransactionDefinition參數合法性

     

    if (definition == null) {
        // Use defaults if no transaction definition given.
        definition = new DefaultTransactionDefinition();
        }
        
    如果definition參數為空,則創建一個DefaultTransactionDefinition實例以提供默認的事務定義數據。

     

  4. 根據先前獲得transaction object判斷是否存在當前事務,根據判定結果采取不同的處理方式

     

    if (isExistingTransaction(transaction)) {
        // Existing transaction found -> check propagation behavior to find out how to behave.
        return handleExistingTransaction(definition, transaction, debugEnabled);
        }
        
    isExistingTransaction(transaction)默認情況下返回false,該方法需要具體子類根據情況進行覆寫(Override), 對於DataSourceTransactionManager來說,它會根據傳入的transaction所記載的信息進行判斷:
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        return (txObject.getConnectionHolder() != null && txObject.getConnectionHolder().isTransactionActive());
        
    對於HibernateTransactionManager來說,則會將transaction強制轉型為HibernateTransactionObject,然后根據HibernateTransactionObject所記載的信息來判斷之前是否存在一個事務。 其他具體實現類對isExistingTransaction(transaction)的處理亦是如此。

     

    不管isExistingTransaction(transaction)返回結果如何,實際上,下面的處理主體上都是以TransactionDefinition中的傳播行為為中心進行的, 比如同樣是PROPAGATION_REQUIRED,在存在當前事務與不存在當前事務兩種情況下的處理是不同的,前者會使用之前的事務,后者則會創建新的事務,其他的傳播行為的處理也是按照不同的場景分別處理。

    1. 如果isExistingTransaction(transaction)方法返回true,即存在當前事務的情況下

      由handleExistingTransaction()方法統一處理存在當前事務情況下應該如何創建事務對應的TransactionStatus實例並返回。

      • 如果definition定義的傳播行為是PROPAGATION_NEVER,拋出異常並退出

         

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
                    throw new IllegalTransactionStateException(
                    "Existing transaction found for transaction marked with propagation 'never'");
                    }
                    
        這是由TransactionDefinition.PROPAGATION_NEVER的語義決定的。

         

      • 如果definition定義的傳播行為是PROPAGATION_NOT_SUPPORTED,則掛起當前事務,然后返回

         

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
                    if (debugEnabled) {
                    logger.debug("Suspending current transaction");
                    }
                    Object suspendedResources = suspend(transaction);
                    boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
                    return newTransactionStatus(
                    definition, null, false, newSynchronization, debugEnabled, suspendedResources);
                    }
                    
        newTransactionStatus()方法返回一個DefaultTransactionStatus實例,因為我們掛起了當前事務,而PROPAGATION_NOT_SUPPORTED不需要事務,所以,返回的DefaultTransactionStatus不包含transaction object的信息(構造方法第二個參數)。

         

      • 如果definition定義的傳播行為是PROPAGATION_REQUIRES_NEW,則同樣掛起當前事務,並開始一個新的事務並返回

         

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
                    if (debugEnabled) {
                    logger.debug("Suspending current transaction, creating new transaction with name [" +
                    definition.getName() + "]");
                    }
                    SuspendedResourcesHolder suspendedResources = suspend(transaction);
                    try {
                    doBegin(transaction, definition);
                    }
                    catch (TransactionException beginEx) {
                    try {
                    resume(transaction, suspendedResources);
                    }
                    catch (TransactionException resumeEx) {
                    logger.error(
                    "Inner transaction begin exception overridden by outer transaction resume exception", beginEx);
                    throw resumeEx;
                    }
                    throw beginEx;
                    }
                    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                    return newTransactionStatus(
                    definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                    }
                    
        AbstractPlatformTransactionManager首先將當前事務掛起,然后調用doBegin()方法開始新的事務,如果開始事務過程中出現異常,則恢復之前掛起的事務。 doBegin(transaction, definition)方法為abstract方法,需要具體子類來實現,在DataSourceTransactionManager中, doBegin()方法會首先檢查傳入的transaction以提取必要信息判斷之前是否存在綁定的connection信息,如果沒有,則從DataSource中獲取新的connection,然后將其AutoCommit狀態改為false,並綁定到TransactionSynchronizationManager。 當然,這期間也會牽扯事務定義的應用以及條件檢查等邏輯。 當所有一起搞定之后,newTransactionStatus會創建一個包含definition,transaction object以及掛起的事務信息和其它狀態信息的DefaultTransactionStatus實例並返回。

         

      • 如果definition定義的傳播行為是PROPAGATION_NESTED,根據情況創建嵌套事務,比如通過Savepoint或者JTA的TransactionManager

         

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
                    if (!isNestedTransactionAllowed()) {
                    throw new NestedTransactionNotSupportedException(
                    "Transaction manager does not allow nested transactions by default - " +
                    "specify 'nestedTransactionAllowed' property with value 'true'");
                    }
                    if (debugEnabled) {
                    logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
                    }
                    if (useSavepointForNestedTransaction()) {
                    // Create savepoint within existing Spring-managed transaction,
                    // through the SavepointManager API implemented by TransactionStatus.
                    // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
                    DefaultTransactionStatus status =
                    newTransactionStatus(definition, transaction, false, false, debugEnabled, null);
                    status.createAndHoldSavepoint();
                    return status;
                    }
                    else {
                    // Nested transaction through nested begin and commit/rollback calls.
                    // Usually only for JTA: Spring synchronization might get activated here
                    // in case of a pre-existing JTA transaction.
                    doBegin(transaction, definition);
                    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                    return newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null);
                    }
                    }
                    
        在這種情況下,會首先通過isNestedTransactionAllowed()方法檢查AbstractPlatformTransactionManager的nestedTransactionAllowed屬性狀態, 如果允許嵌套事務,那么還得分兩種情況處理,對於DataSourceTransactionManager來說,因為它支持使用Savepoint創建嵌套事務,所以,會使用TransactionStatus創建相應的Savepoint並返回; 而像JtaTransactionManager則要依賴於具體JTA產品的TransactionManager提供嵌套事務支持。

         

        useSavepointForNestedTransaction()方法默認返回true,即默認使用Savepoint創建嵌套事務,如果具體子類不支持使用Savepoint創建嵌套事務,則需要覆寫(Override)該方法,比如JtaTransactionManager。

      • 如果需要檢查事務狀態匹配情況,則對當前存在事務與傳入的defintion中定義的隔離級別與ReadOnly屬性進行檢查,如果數據不吻合,則拋出異常;

         

        if (isValidateExistingTransaction()) {
                    // validate isolation
                    // validate read only
                    ...
                    }
                    
        AbstractPlatformTransactionManager的validateExistingTransaction屬性默認值為false,如果你想進一步加強事務屬性之間的一致性, 可以將validateExistingTransaction屬性設置為true,那么這個時候,以上代碼即會被觸發執行。

         

      • 剩下的就是其他情況下,直接構建TransactionStatus返回

        比如對應PROPAGATION_SUPPORTS和PROPAGATION_REQUIRED的情況。

    2. 如果isExistingTransaction(transaction)方法返回false,即不存在當前事務的情況下

      • 當definition中定義的傳播行為是PROPAGATION_MANDATORY的時候,拋出異常

        因為不存在當前事務,所以根據PROPAGATION_MANDATORY的語義,理當如此。

      • 當definition中定義的傳播行為是PROPAGATION_REQUIRED或者PROPAGATION_REQUIRES_NEW或者PROPAGATION_NESTED的時候,開啟新的事務

         

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                    definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                    definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
                    SuspendedResourcesHolder suspendedResources = suspend(null);
                    if (debugEnabled) {
                    logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
                    }
                    try {
                    doBegin(transaction, definition);
                    }
                    catch (TransactionException ex) {
                    resume(null, suspendedResources);
                    throw ex;
                    }
                    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                    return newTransactionStatus(
                    definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                    }
                    
        之所以在doBegin之前先調用傳入null的suspend()方法是因為考慮到如果有注冊的Synchronization的話,需要暫時將這些與將要開啟的新事務無關的Synchronization先放一邊。

         

      • 剩下的其他情況,則返回不包含任何transaction object的TransactionStatus並返回

        這種情況下雖然是空的事務,但有可能需要處理在事務過程中相關的Synchronization。

從getTransaction(TransactionDefinition)的邏輯可以看出,AbstractPlatformTransactionManager更多的關注的是事務處理過程的總體邏輯,而跟具體事務資源相關的處理則交給了具體的子類來實現。

事務處理的完成有兩種情況,即回滾事務或者提交事務,AbstractPlatformTransactionManager提供的rollback(TransactionStatus)和commit(TransactionStatus)兩個模板方法分別對應這兩種情況下的處理。 因為事務提交過程中可能需要處理回滾邏輯,我們不妨以commit(TransactionStatus)的實現流程看一下AbstractPlatformTransactionManager是如何處理事務完成的:

Procedure 1.2. 事務提交執行流程

  1. 因為在事務處理過程中,我們可以通過TransactionStatus的setRollbackOnly()方法標記事務回滾,所以,commit(TransactionStatus)在具體提交事務之前會檢查rollBackOnly狀態, 如果該狀態被設置,那么轉而執行事務的回滾操作;

    rollback(TransactionStatus)的邏輯主要包含三點:

    • 回滾事務! 這當然是必須的啦,不過,這里的回滾事務又分三種情況:

      1. 如果是嵌套事務,則通過TransactionStatus釋放Savepoint;

      2. 如果TransactionStatus表示當前事務是一個新的事務,則調用子類的doRollback(TransactionStatus)方法真正的回滾事務;

        doRollback(TransactionStatus)是抽象方法,具體子類必須實現它,DataSourceTransactionManager在實現該方法的時候無疑是調用connection.rollback()啦! 至於說HibernateTransactionManager,會通過它Session上的Transaction的rollback()方法回滾事務,其他子類對doRollback(TransactionStatus)的實現邏輯依此類推。

      3. 如果當前存在事務,並且rollbackOnly狀態被設置,則調用由子類實現的doSetRollbackOnly(TransactionStatus)方法,各子類實現通常會將當前的transaction object的狀態設置為rollBackOnly。

       

    • 觸發Synchronization事件。

      rollback的時候出發的事件比commit的時候要少,只有triggerBeforeCompletion(status)和triggerAfterCompletion()。

    • 清理事務資源。這包括:

      • 設置TransactionStatus中的completed為完成狀態;

      • 清理與當前事務相關的Synchronization;

      • 調用doCleanupAfterCompletion()釋放事務資源,並解除到TransactionSynchronizationManager的資源綁定。對於DataSourceTransactionManager來說,當然是關閉數據庫連接,然后解除對DataSource對應資源的綁定。

      • 如果之前有掛起的事務,恢復掛起的事務;

       

     

  2. 如果rollBackOnly狀態沒被設置,則執行正常的事務提交操作。

    commit(TransactionStatus)方法余下的邏輯與rollback(TransactionStatus)方法基本相似,只是幾個具體操作有所差別:

    • 回滾事務現在是提交事務;

      提交事務的時候,也會牽扯到幾種情況:

      1. 決定是否提前檢測全局的rollBackOnly標志,如果最外層事務已經被標記為rollBackOnly,並且failEarlyOnGlobalRollbackOnly為true,則拋出異常:

        boolean globalRollbackOnly = false;
                    if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
                    globalRollbackOnly = status.isGlobalRollbackOnly();
                    }
                    ...
                    if (globalRollbackOnly) {
                    throw new UnexpectedRollbackException(
                    "Transaction silently rolled back because it has been marked as rollback-only");
                    }
                    

         

      2. 如果提交事務之前發現TransactionStatus持有Savepoint,則釋放之,這實際上是在處理嵌套事務的提交;

      3. 如果TransactionStatus表示要提交的事務是一個新的事務,則調用子類的doCommit(TransactionStatus)方法實現提交事務。

        doCommit(TransactionStatus)也是AbstractPlatformTransactionManager暴露給子類實現的抽象方法,子類必須實現該方法。 對於DataSourceTransactionManager來說,因為事務的提交有Connection決定,所以會直接調用connection.commit()提交事務。 其他的子類也會使用自身的局部事務API在該方法中實現事務的提交。

       

    • 也需要出發Synchronization相關事件,不過,觸發的事件比rollback(TransactionStatus)中的要多,包括triggerBeforeCommit(), triggerBeforeCompletion(),triggerAfterCommit()和triggerAfterCompletion()。

    • 如果AbstractPlatformTransactionManager的rollbackOnCommitFailure狀態被設置為true,則表示如果在事務提交過程中出現異常,需要回滾事務, 所以,當commit(TransactionStatus)方法捕獲相應異常並且檢測到該字段被設置的時候,需要回滾事務。rollbackOnCommitFailure的默認值是false,表示即使提交過程中出現異常,也不回滾事務。

    • 既然commit(TransactionStatus)與rollback(TransactionStatus)一樣,都是意味着事務的完成,那么也需要在最后進行事務資源清理的工作,具體內容可以參照rollback(TransactionStatus)部分。

     

對於suspend和resume兩個方法來說,邏輯更好理解了,前者會把TransactionSynchronizationManager上當前事務對應的Synchroniazation信息以及資源獲取到SuspendedResourcesHolder中,然后解除這些綁定; 后者則會將SuspendedResourcesHolder中保持的信息重新綁定到TransactionSynchronizationManager。

實際上,如果將AbstractPlatformTransactionManager中處理Synchroniaztion回調以及事務傳播行為的邏輯剝離一下的話,你就會發現,整個的邏輯流程就是開始為您展示的實現原型所表達的。

下圖展示了AbstractPlatformTransactionManager需要子類實現或者覆寫(Override)的方法:

Figure 1.14. 模板類與實現類之間的紐帶

模板類與實現類之間的紐帶

對於各個子類來說,無非就是根據自身需要管理的資源和事務管理API提供這些方法的實現而已。

 

1.3.2. 使用spring進行事務管理

從Java平台尤其是J2EE平台上事務管理的傳統意義上來看,事務管理有兩種方式,即編程式事務管理以及聲明式事務管理,對於這兩種事務管理方式的支持, spring事務框架可以說是“青出於藍而勝於藍”。

1.3.2.1. 編程式事務管理

通過spring進行編程式事務管理有兩種方式,要么直接使用PlatformTransactionManager,要么使用更方便的TransactionTemplate,二者各有優缺點,但總體上來說,使用TransactionTemplate進行編程式事務管理是推薦的方式。

1.3.2.1.1. 直接使用PlatformTransactionManager進行編程式事務管理

PlatformTransactionManager接口定義了事務界定的基本操作,我們可以直接使用PlatformTransactionManager進行編程式事務管理:

DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setTimeout(20);
...
TransactionStatus txStatus = transactionManager.getTransaction(definition);
try
{
// business logic implementation
}
catch(ApplicationException e)
{
transactionManager.rollback(txStatus);
throw e;
}
catch(RuntimeException e)
{
transactionManager.rollback(txStatus);
throw e;
}
catch(Error e)
{
transactionManager.rollback(txStatus);
throw e;
}
transactionManager.commit(txStatus);
只要為transactionManager提供合適的PlatformTransactionManager實現,然后結合TransactionDefinition開啟事務,結合TransactionStatus來回滾或者提交事務就可以完成當前對象的整個事務管理。

 

直接使用PlatformTransactionManager,你可以完全的控制整個的事務處理過程,但是,缺點也是很明顯的, 從抽象事務操作以屏蔽不同事務管理API差異的角度看,PlatformTransactionManager可能已經足夠了, 但是,從應用程序開發的角度,卻依然過於“底層”,單單是期間的這些異常處理就夠我們忙活的了。 如果在每一個需要事務管理的地方,全都采用直接使用PlatformTransactionManager進行事務管理,那重復代碼的數量將是驚人的。

鑒於使用PlatformTransactionManager進行事務管理的流程比較固定,各個事務管理期間只有部分邏輯存在差異,我們可以考慮像spring的數據訪問層那樣,使用模板方法模式 + Callback的方式對直接使用PlatformTransactionManager進行事務管理的代碼進行封裝, 這就有更方便的編程式事務管理方式,即使用TransactionTemplate的編程式事務管理。

1.3.2.1.2. 使用TransactionTemplate進行編程式事務管理

org.springframework.transaction.support.TransactionTemplate對與PlatformTransactionManager相關的事務界定操作以及相關的異常處理進行了模板化封裝, 開發人員更多的關注於通過相應的callback接口提供具體的事務界定內容即可。spring針對TransactionTemplate提供了兩個callback接口,TransactionCallback和TransactionCallbackWithoutResult,二者的唯一區別就是是否需要返回執行結果。

使用TransactionTemplate進行事務管理的代碼看起來要比直接使用PlatformTransactionManager要簡潔並且容易管理的多:

TransactionTemplate txTemplate = ...;
Object result = txTemplate.execute(new TransactionCallback(){
public Object doInTransaction(TransactionStatus txStatus) {
Object result = null;
// 各種事務操作 ...
return result;
}});
或者
txTemplate.execute(new TransactionCallbackWithoutResult(){
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
// 事務操作1
// 事務操作2
// ...
}});
TransactionTemplate會捕捉TransactionCallback或者TransactionCallbackWithoutResult事務操作中拋出的“ unchecked exception”並回滾事務,然后將“ unchecked exception”拋給上層處理, 所以,現在我們只需要處理特定於應用程序的異常即可,而不用像直接使用PlatformTransactionManager那樣對所有可能的異常都進行處理。

 

如果事務處理期間沒有任何問題,TransactionTemplate最終會為我們提交事務,唯一需要我們干預的就只剩下某些情況下的事務回滾了。 如果在TransactionCallback或者TransactionCallbackWithoutResult的事務操作過程中需要讓當前事務回滾而不是最終提交,一般來說,我們有兩種方式:

  • 拋出“unchecked exception”,TransactionTemplate會為我們處理事務的回滾。 如果事務操作中可能拋出“checked exception”,這種情況下就得在callback內部捕獲,然后轉譯為“unchecked exception”后拋出。

    txTemplate.execute(new TransactionCallbackWithoutResult(){
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
        try
        {
        ...
        }
        catch(CheckedException e)
        {
        // throw specific exception here, avoid generic RuntimeException
        throw new RuntimeException(e);
        }
        }});
        

     

  • 使用callback接口暴露的TransactionStatus將事務標記為rollBackOnly,TransactionTemplate在最終提交事務的時候如果檢測到rollBackOnly標志狀態被設置,將把提交事務改為回滾事務:

    txTemplate.execute(new TransactionCallbackWithoutResult(){
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
        boolean needRollback = false;
        ...
        if(needRollback)
        txStatus.setRollbackOnly();
        }});
        
    對於將事務操作中可能拋出的“checked exception”,如果既想回滾事務又不想讓它以“unchecked exception”的形式向上層傳播的話, 我們當然不能通過將其轉譯成“unchecked exception”的方式來處理啦,不過我們現在卻可以通過TransactionStatus設置rollBackOnly:
    txTemplate.execute(new TransactionCallbackWithoutResult(){
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
        try
        {
        ...
        }
        catch(CheckedException e)
        {
        logger.warn("Tranaction is Rolled back!",e);
        txStatus.setRollbackOnly();
        }
        }});
        
    這種情況下需要注意一個問題,千萬不要只“txStatus.setRollbackOnly()”而忘記記錄日志,雖然大家可能都知道“swallow exception”是不對的, 但這個地方確實容易忽略日志的記錄,從而造成事務回滾了,而我們卻不知道的情況。 當發現數據庫中本來應該刪除的數據卻依然存在,並且日志中也沒有任何異常信息的時候,你就得撓腦袋很長時間來發現到底哪里出了問題了,程序沒跑?才怪!

     

使用TransactionTemplate雖然要比直接使用PlatformTransactionManager更加便捷,但TransactionTemplate無法處理當事務操作中需要向上層拋出原來的“ checked exception”的情況, 你應該也發現了,實際上TransactionCallback或者TransactionCallbackWithoutResult的方法定義中沒有聲明可以拋出任何“ checked exception”,直接使用PlatformTransactionManager則沒有這樣的限制。 不過,這應該並不會過多的限制TransactionTemplate展現其“ 個人魅力”吧?!

 

1.3.2.1.3. 編程創建基於Savepoint的嵌套事務

TransactionStatus不但可以在事務處理期間通過setRollbackOnly()方法來干預事務的狀態,如果需要,作為SavepointManager,它也可以幫助我們使用Savepoint機制創建嵌套事務。

我們以銀行賬戶間轉賬為例說明如何使用TransactionStatus創建基於Savepoint的嵌套事務,不過,現在不是從一個賬戶轉到另一個賬戶,而是從一個賬戶轉到兩個賬戶,一個是主賬戶,一個備用帳戶,如果向主帳戶轉賬失敗,則將金額轉入備用帳戶,總之,金額從第一個賬戶取出之后,必須存入兩個賬戶的其中一個,以保證整個事務的完整性。 在這樣的前提下,我們的事務管理代碼基本上如下所示:

txTemplate.execute(new TransactionCallbackWithoutResult(){
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
BigDecimal transferAmount = new BigDecimal("20000");
try
{
withdraw("WITHDRAW_ACOUNT_ID",transferAmount);
Object savePointBeforeDeposit = txStatus.createSavepoint();
try
{
deposit("MAIN_ACOUNT_ID",transferAmount);
}
catch(DepositException ex)
{
logger.warn("rollback to savepoint for main acount transfer failure",ex);
txStatus.rollbackToSavepoint(savePointBeforeDeposit);
deposit("SECONDARY_ACOUNT_ID", transferAmount);
}
finally
{
txStatus.releaseSavepoint(savePointBeforeDeposit);
}
}
catch(TransferException e)
{
logger.warn("failed to complete transfer operation!",e);
txStatus.setRollbackOnly();
}
}});
當然,如果在轉賬期間的異常是“ unchecked exception”,最外層的捕捉TransferException是沒有太多必要的。

 

在這里使用savepoint創建嵌套事務的好處在於,即使deposit中牽扯多筆數據的更新,通過“txStatus.rollbackToSavepoint(savePointBeforeDeposit)”也可以將這些數據恢復到沒有存入金額之前的狀態,而不會破壞當前事務的完整性。 如果在這里通過傳播行為是PROPAGATION_REQUIRES_NEW的TransactionDefinition創建一個新的事務的話,雖然deposit過程出現問題也可以回滾數據,但取款與存款的操作就不在同一個事務中了(取款在當前事務,存款在另一個新的事務),這無疑違反了事務的ACID屬性。

Note

使用TransactionStatus創建基於Savepoint的嵌套事務需要底層的PlatformTransactionManager實現類的支持,當前只有在JDBC3.0驅動下的DataSourceTransactionManager可用。

通過使用TransactionStatus創建基於Savepoint的嵌套事務並非創建嵌套事務的唯一方式,也並非最方便的方式,實際上,我們更傾向於結合PROPAGATION_NESTED傳播行為的聲明式事務管理方式。

1.3.2.2. 聲明式事務管理

直接使用編程式事務管理一個比較頭疼的問題就是事務管理代碼與業務邏輯代碼相互混雜,而聲明式事務管理則可以避免這種不同系統關注點之間的糾纏,使得事務管理代碼不用再去“污染”具體業務邏輯的實現。

1.3.2.2.1. 引子

聲明式事務實際上並沒有想象中的那么神秘,當我們將“事務管理”這一橫切關注點從原來硬編碼事務管理邏輯的系統中剝離出來之后, 你就會發現,聲明式事務已經在“幸福終點站”那里等着我們了。

我們先不管spring是如何提供聲明式事務管理的,如果要我們從原來硬編碼事務管理的系統中將這些事務管理相關的代碼從業務對象中剝離出來的話,我們會怎么做? 最土的一個辦法,為每一個Service都提供一個TransactionFacade,事務管理邏輯現在都集中到了TransactionFacade中,Service實現類可以對事務管理一無所知,只要保證針對所有Service的調用必須走TransactionFacade即可。 所以,整個情形看起來就是這個樣子:

Figure 1.15. TransactionFacade場景示意圖

TransactionFacade場景示意圖

這種方法雖然可以實現事務管理代碼與業務邏輯代碼之間的分離,但是如果不做一些變通的話,在實際的開發中也不會給你帶來更多的好處,難道你想每一個Serivce對象都給它實現一個對應的TransactionFacade對象?

 

對,經過SpringAOP的洗禮,我想你已經想到了,動態代理不就是為這個設計的嗎?!呵呵,不過,原理歸原理,要真的實現這種功能,直接使用SpringAOP才是正道啊。 事務管理本身就是一種橫切關注點,與其他的橫切關注點本質上沒有任何區別,所以,我們完全可以為其提供相應的Advice實現,然后織入(weave)到系統中需要該橫切邏輯的Joinpoint處,這樣不就達到了將事務管理邏輯從業務邏輯實現中剝離出來的目的了嗎? 現在,我們要做的實際上就是提供一個Interceptor,在業務方法執行開始之前開啟一個事務,當方法執行完成或者異常退出的時候就提交事務或者回滾事務,有了spring的編程式事務管理API的支持,實現這樣的一個Interceptor對於我們來說應該就很容易了:

public class PrototypeTransactionInterceptor implements MethodInterceptor
{
private PlatformTransactionManager transactionManager;
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
TransactionDefinition definition = getTransactionDefinitionByMethod(method);
TransactionStatus txStatus = transactionManager.getTransaction(definition);
Object result = null;
try
{
result = invocation.proceed();
}
catch(Throwable t)
{
if(needRollbackOn(t))
{
transactionManager.rollback(txStatus);
}
else
{
transactionManager.commit(txStatus);
}
throw t;
}
transactionManager.commit(txStatus);
return result;
}
private boolean needRollbackOn(Throwable t) {
// TODO ...
return false;
}
private TransactionDefinition getTransactionDefinitionByMethod(Method method) {
// TODO ...
return null;
}
public PlatformTransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
}
在實現這樣的一個Interceptor的過程中,我們會發現有些邏輯不好確定:
  • 針對每一個方法業務方法的攔截,需要知道該方法是否需要事務支持,如果需要,針對該事務的TransactionDefinition相關信息又從哪里獲得?

  • 調用方法過程中如果拋出異常的話,如何對這些異常進行處理,哪些異常拋出的情況下需要回滾事務,哪些異常拋出的情況下又不需要?

我們需要提供某種方式來記載業務方法與對應的事務信息之間的映射關系,Interceptor只要查詢這種映射關系,就可以知道要不要為當前業務方法創建事務,如果要創建事務,則以業務方法作為標志到映射關系中查找創建事務所需要的信息然后創建事務。 我們當然可以將這種映射信息寫死到Interceptor實現類中,不過,將他們放在外部配置文件(例如properties或者xml文件)或者Java源代碼的Annotation中才是當下比較流行的方式。 這種映射信息正規來講,叫做驅動事務的元數據(Metadata),有了類似於PrototypeTransactionInterceptor這樣的AOP裝備的支持,系統的事務管理就變成了僅僅需要在元數據中聲明相應的事務控制信息就成!

 

spring提供了用於聲明式事務管理的一切設施(使用org.springframework.transaction.interceptor.TransactionInterceptor實現替代我們的PrototypeTransactionInterceptor), 對於我們來說,所要做的僅僅是決定要使用xml元數據驅動的還是使用Annotation元數據驅動的聲明式事務管理啦!

Tip

啥?我還沒有公布第二個問題是如何解決的?還記得在TransactionDefinition部分提到的用於聲明式事務的TransactionAttribute定義嗎? 其實僅需要將“在什么異常類型拋出的情況下需要回滾事務”的信息補充到默認的TransactionDefintion定義中,用於事務處理的Interceptor根據獲得的TransactionDefinition提供的該信息就能夠決定出現異常的情況下應該如何結束事務。 而TransactionAttribute就是追加了異常回滾信息的TransactionDefinition定義,通過TransactionAttribute實現類代替默認的TransactionDefintion實現類創建事務不就結了?

1.3.2.2.2. XML元數據驅動的聲明式事務

spring允許你在ioc容器的配置文件中直接指定事務相關的元數據,從1.x版本發展到2.x版本,不管配置形式如何變化,所要達到的目的以及底層的機制卻是不變的,配置形式的演化只是為了能夠更加簡潔方便。

從spring1.x到2.x,大體上來說,我們可以使用以下四種配置方式在ioc容器的配置文件中指定事務需要的元數據:

  • 使用ProxyFactory(ProxyFactoryBean)+TransactionInterceptor

  • 使用“一站式”的TransactionProxyFactoryBean

  • 使用BeanNameAutoProxyCreator

  • 使用Spring2.x的聲明事務配置方式

我們將對這四種配置方式給出詳細的實例和相關內容的講解,不過在此之前,我們需要先聲明一個事務加諸於上的模型,總不能將所有的配置都構建成“ 空中樓閣”不是?

 

總是FooService,BarService的,可能大家都看煩了,所以,我們這回打算換個口味。假設我們要搭建一個QuoteService, 先暫且不管它是應用於一般的證券系統還是外匯系統,總之,通過它我們能夠查詢報價信息並且必要的話,也可以更新底層數據內容。 現在的QuoteService不是一個遠程服務,它的目的也很簡單,基本上就是實現一個Quote信息的基本管理功能(在Spring Remoting一章我們將介紹如何通過spring將QuoteService以遠程服務的方式暴露出來)。

我們首先定義的是對應QuoteService的服務接口,面向接口編程為我們管理問題域提供了很好的抽象方式:

public interface IQuoteService {
Quote getQuate();
Quote getQuateByDateTime(DateTime dateTime);
void saveQuote(Quote quote);
void updateQuote(Quote quote);
void deleteQuote(Quote quote);
}
QuoteService實現了IQuoteService接口:
public class QuoteService implements IQuoteService
{
private JdbcTemplate jdbcTemplate;
public Quote getQuate() {
return (Quote)getJdbcTemplate().queryForObject("", new RowMapper(){
public Object mapRow(ResultSet rs, int row) throws SQLException {
Quote quote = new Quote();
// ...
return quote;
}});
}
public Quote getQuateByDateTime(DateTime dateTime) {
throw new NotImplementedException();
}
public void saveQuote(Quote quote) {
throw new NotImplementedException();
}
public void updateQuote(Quote quote) {
throw new NotImplementedException();
}
public void deleteQuote(Quote quote) {
throw new NotImplementedException();
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
只是為了說明針對QuoteService的事務管理,所以,暫且直接JdbcTemplate進行數據訪問,系統如果需要進一步的抽象可以將數據訪問邏輯提取DAO一層以屏蔽數據訪問技術造成的差異。 我們僅給出一個方法的實現,其余的先拋出NotImplementedException,當系統運行的時候,這可以告訴我們還有工作沒有完成。

 

在有了QuoteService這個簡單的模型之后,我們開始進入正題。

1.3.2.2.2.1. 使用ProxyFactory(ProxyFactoryBean)+TransactionInterceptor

xml元數據驅動的聲明式事務本質上來說,是為TransactionInterceptor提供需要的TransactionDefiniton(確切的說,是TransactionAttribute)信息。 而至於說將TransactionInterceptor管理的事務加諸到相應的業務對象上的過程,則就純粹是一個AOP的配置過程了。既然如此,最基本的方式當然就是直接通過ProxyFactoryBean(或者ProxyFactory)進行事務管理這一橫切關注點到系統的織入工作啦! 所以,使用ProxyFactoryBean和TransactionInterceptor為我們的QuoteService添加聲明式事務,看起來就是這個樣子:

<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost/databaseName"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="..."/>
<property name="password" value="..."/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_20</prop>
<prop key="saveQuote">PROPAGATION_REQUIRED</prop>
<prop key="updateQuote">PROPAGATION_REQUIRED</prop>
<prop key="deleteQuote">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean id="quoteServiceTarget" class="...QuoteService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="quoteService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="quoteServiceTarget"/>
<property name="proxyInterfaces" value="...IQuoteService"/>
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
</list>
</property>
</bean>
<bean id="client" class="...QuoteSerivceClient">
<property name="quoteService" ref="quoteService"/>
</bean>
</beans>
QuoteSerivceClient是一般意義上使用QuoteService的應用類,當我們通過它調用QuoteService的業務方法的時候,除了getQuote()方法之外,其他方法(比如getQuoteService().saveQuote(quote))因為拋出了“ unchecked”的NotImplementedException,從而導致事務自動回滾,這說明我們的聲明式事務生效了,不是嗎?
1094 [main] DEBUG org.springframework.transaction.support.TransactionSynchronizationManager  - Initializing transaction synchronization
1094 [main] DEBUG org.springframework.transaction.interceptor.TransactionInterceptor  - Getting transaction for [...IQuoteService.saveQuote]
1094 [main] DEBUG org.springframework.transaction.interceptor.TransactionInterceptor  - Completing transaction for [...IQuoteService.saveQuote] after exception: org.apache.commons.lang.NotImplementedException: Code is not implemented
1094 [main] DEBUG org.springframework.transaction.interceptor.RuleBasedTransactionAttribute  - Applying rules to determine whether transaction should rollback on org.apache.commons.lang.NotImplementedException: Code is not implemented
1094 [main] DEBUG org.springframework.transaction.interceptor.RuleBasedTransactionAttribute  - Winning rollback rule is: null
1094 [main] DEBUG org.springframework.transaction.interceptor.RuleBasedTransactionAttribute  - No relevant rollback rule found: applying superclass default
1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager  - Triggering beforeCompletion synchronization
1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager  - Initiating transaction rollback
1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager  - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@b76fa]
1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager  - Triggering afterCompletion synchronization
1094 [main] DEBUG org.springframework.transaction.support.TransactionSynchronizationManager  - Clearing transaction synchronization
現在讓我們回頭探究一下在使用ProxyFactoryBean和TransactionInterceptor進行聲明式事務的過程中到底有哪些奧秘以及需要我們注意的地方。

 

聲明式事務要起作用,需要幾個方面協同工作,這包括事務要管理的具體數據資源類型,采用的數據訪問技術,特定的事務管理器實現,以及TransactionInterceptor提供的業務方法攔截功能。 具體到我們的QuoteService來說,我們要對數據庫提供的數據資源進行事務操作,所以,配置中需要dataSource的定義,這是事務操作的基礎:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost/databaseName"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="..."/>
<property name="password" value="..."/>
</bean>
要對數據資源進行訪問,QuoteService采用的是JDBC的方式,即直接使用spring提供的JdbcTemplate,那么,我們需要提供JdbcTemplate以及特定於Jdbc的事務管理器(DataSourceTransactionManager):
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
數據訪問技術與事務管理器的類型是一一對應的,否則就有“ 驢唇不對馬嘴”之嫌,讓美國總統來發號施令中國的事務,我們當然可以不理他。 所以,如果你的“ QuoteService”要用Hibernate進行數據訪問,那么,請提供HibernateTransactionManager作為事務管理器; 如果你的“ QuoteService”要用JDO進行數據訪問,請將DataSourceTransactionManager替換為JdoTransactionManager,依此類推。

 

TransactionInterceptor是整個聲明式事務的主體,要讓他發揮事務管理的職能,請為他提供一個事務管理器;要讓他知道該為哪個方法加諸什么事務,是否要加諸事務,請為他提供必要的映射信息:

<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_20</prop>
<prop key="saveQuote">PROPAGATION_REQUIRED</prop>
<prop key="updateQuote">PROPAGATION_REQUIRED</prop>
<prop key="deleteQuote">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
TransactionInterceptor所需要的業務對象上每個業務方法的事務管理信息通過org.springframework.transaction.interceptor.TransactionAttributeSource接口來獲取:
public interface TransactionAttributeSource
{
TransactionAttribute getTransactionAttribute(Method method, Class targetClass);
}
TransactionAttributeSource的具體實現類通常以不同的形式存儲了從不同位置獲取的事務管理元數據信息, 比如NameMatchTransactionAttributeSource將以方法名作為映射信息的Key,對相應的元數據進行存儲;MethodMapTransactionAttributeSource則直接以對應每個方法的Method實例作為Key來存儲元數據對應的映射信息; 而AnnotationTransactionAttributeSource則是直接從源碼中的Annotation中獲取對應每一個業務方法的事務管理信息。

 

我們通常通過兩個屬性為TransactionInterceptor設置需要的TransactionAttributeSource:

  • transactionAttributes屬性.  我們剛才就是使用transactionAttributes屬性為TransactionInterceptor設置的映射信息,transactionAttributes是Properties類型, TransactionInterceptor內部將通過transactionAttributes提供的信息構造一個NameMatchTransactionAttributeSource類型的TransactionAttributeSource使用。

  • transactionAttributeSource屬性.  transactionAttributeSource屬性就是直接可用的TransactionAttributeSource類型,但是我們在ioc容器的配置文件中指定的是String形式,所以, 容器將通過org.springframework.transaction.interceptor.TransactionAttributeSourceEditor對String形式的值進行一個轉換,再設置給TransactionInterceptor, 轉換后的具體TransactionAttributeSource實現類為MethodMapTransactionAttributeSource。同樣的元數據,通過transactionAttributeSource設置如下:

    <property name="transactionAttributeSource">
        <value>
        org.spring21.package.IQuoteService.getQuate*=PROPAGATION_SUPPORTS,readOnly,timeout_20
        org.spring21.package.IQuoteService.saveQuote=PROPAGATION_REQUIRED
        org.spring21.package.IQuoteService.updateQuote=PROPAGATION_REQUIRED
        org.spring21.package.IQuoteService.deleteQuote=PROPAGATION_REQUIRED
        </value>
        </property>
        
    唯一需要注意的就是現在需要指定全限定的類名以及方法名。

    Note

    也可以通過transactionAttributeSources屬性指定元數據信息,它是transactionAttributeSource的復數形式

     

當TransactionInterceptor准備就緒之后,剩下的就是通過spring的AOP將業務對象與其綁定就可以了:
<bean id="quoteServiceTarget" class="...QuoteService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="quoteService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="quoteServiceTarget"/>
<property name="proxyInterfaces" value="...IQuoteService"/>
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
</list>
</property>
</bean>
<bean id="client" class="...QuoteSerivceClient">
<property name="quoteService" ref="quoteService"/>
</bean>
只不過,一定不要把“ quoteServiceTarget”當作“ quoteService”而注入給需要的對象,否則,事務管理不生效那就怨不得別人了。

 

元數據中事務屬性指定規則

以String形式給出的事務屬性是有一定規則的,這個規則由org.springframework.transaction.interceptor.TransactionAttributeEditor類的邏輯來界定, TransactionAttributeEditor負責將String形式給出的事務屬性轉換為具體的TransactionAttribute實例(RuleBasedTransactionAttribute)。

String形式的事務屬性規則如下:

PROPAGATION_NAME,[ISOLATION_NAME],[readOnly],[timeout_NNNN],[+Exception1],[-Exception2]
除了PROPAGATION_NAME是必須的之外,其他規則可以根據需要決定是否需要指定,各個區段之間以逗號分隔:
  • PROPAGATION_NAME.  對應org.springframework.transaction.TransactionDefinition中定義的與傳播行為相關的常量名, 即PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_SUPPORTS,PROPAGATION_MANDATORY,PROPAGATION_NESTED,PROPAGATION_NOT_SUPPORTED和PROPAGATION_NEVER。

  • ISOLATION_NAME.  對應org.springframework.transaction.TransactionDefinition中定義的與Isolation相關的常量名, 即 ISOLATION_DEFAULT ,ISOLATION_READ_UNCOMMITTED,ISOLATION_READ_COMMITTED,ISOLATION_REPEATABLE_READ和ISOLATION_SERIALIZABLE。

  • readOnly.  如果你需要標明當前事務為只讀事務,則追加該值即可。

  • timeout_NNNN.  指定事務的超時時間,需要以“timeout_”為前綴,后面加數字的形式,如timeout_20限定超時時間為20秒。

  • [+Exception1],[-Exception2].  自定義異常回滾規則,前綴加號(+)后跟具體的異常類型表示即使業務方法拋出該異常也同樣提交事務;前綴減號(-)后跟具體的異常類型表示業務方法拋出該異常的時候回滾事務。 比如,當業務方法拋出QuoteException的時候,我們想要事務回滾,則可以這樣指定:

    serviceMethod=PROPAGATION_REQUIRED,-QuoteException
    因為“unchecked exception”默認情況下會自動回滾,所以,通過自定義異常回滾規則主要是指定“checked exception”類型的應用異常。

     

 

使用ProxyFactoryBean和TransactionInterceptor實現聲明式事務可以從最低層次上理解spring提供的聲明式事務是如何運作的,不過,為了減少配置量,進一步提高開發效率, 我們會探索更加便捷的方式,TransactionProxyFactoryBean則更近的向這一點邁進。

1.3.2.2.2.2. 使用“一站式”的TransactionProxyFactoryBean

TransactionProxyFactoryBean是專門面向事務管理的“ProxyFactoryBean”,它直接將TransactionInterceptor納入自身進行管理, 使用TransactionProxyFactoryBean代替ProxyFactoryBean進行聲明式事務管理,不需要單獨聲明TransactionInterceptor的bean定義,有關事務的元數據,事務管理器等信息全都通過TransactionProxyFactoryBean的bean定義指定就可以。

這樣,同樣針對QuoteService的聲明式事務管理,使用TransactionProxyFactoryBean后的樣子如下:

<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost/dbName"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="..."/>
<property name="password" value="..."/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="quoteServiceTarget" class="...QuoteService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="quoteService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="target" ref="quoteServiceTarget"/>
<property name="proxyInterfaces" value="...IQuoteService"/>
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_10</prop>
<prop key="saveQuote">PROPAGATION_REQUIRED</prop>
<prop key="updateQuote">PROPAGATION_REQUIRED</prop>
<prop key="deleteQuote">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean id="client" class="...QuoteSerivceClient">
<property name="quoteService" ref="quoteService"/>
</bean>
</beans>
現在,TransactionProxyFactoryBean集ProxyFactoryBean,TransactionInterceptor功能於一身,一心一意的為聲明式事務管理做貢獻了。

 

不過,你也看到了,針對TransactionProxyFactoryBean的bean定義看起來不是那么苗條,如果每一個需要聲明式事務的業務對象都來這么一下子, 那配置量可着實不輕松,所以,通常情況下,我們會使用bean定義模板的方式來簡化使用TransactionProxyFactoryBean進行聲明式事務的配置:

<bean id="txProxyFactoryBean" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" abstract="true">
<property name="proxyInterfaces" value="...IQuoteService"/>
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_10</prop>
<prop key="saveQuote">PROPAGATION_REQUIRED</prop>
<prop key="updateQuote">PROPAGATION_REQUIRED</prop>
<prop key="deleteQuote">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean id="quoteService" parent="txProxyFactoryBean">
<property name="target" ref="quoteServiceTarget"/>
</bean>
<bean id="quoteService2" parent="txProxyFactoryBean">
<property name="target" ref="otherQuoteServiceTarget"/>
</bean>
...
將共有的一些屬性提取到“ txProxyFactoryBean”的bean定義模板中,就可以減少每次配置單獨業務對象對應的bean定義的工作量。

 

相對於直接使用ProxyFactoryBean和TransactionInterceptor來說,使用TransactionProxyFactoryBean可以將聲明式事務相關的關注點集中起來, 一定程度上減少了配置的工作量。不過話又說回來了,當應用程序中僅有少量的業務對象需要配置聲明式事務的話,配置的工作量還算說的過去,一旦需要聲明式事務的業務對象數量增加, 采用這種近乎手工作坊式的配置方式就會拖開發的后腿了。這個時候,我們自然會想到AOP中的自動代理機制,而下面正是針對如何使用自動代理對聲明式事務進行管理的內容...

1.3.2.2.2.3. 使用BeanNameAutoProxyCreator

使用BeanNameAutoProxyCreator進行聲明式事務管理進一步的簡化了配置的工作,當所有的聲明式事務相關裝備一次到位之后,要為新的業務對象添加聲明式事務支持, 唯一要做的就是在你為該業務對象添加bean定義的時候,同時將它的beanName添加到BeanNameAutoProxyCreator管理的“beanNames”列表中。

使用BeanNameAutoProxyCreator為業務對象提供聲明式事務支持,通常配置如下:

<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost/databaseName"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="..."/>
<property name="password" value="..."/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributeSource">
<value>
org.spring21.package.IQuoteService.getQuate*=PROPAGATION_SUPPORTS,readOnly,timeout_20
org.spring21.package.IQuoteService.saveQuote=PROPAGATION_REQUIRED
org.spring21.package.IQuoteService.updateQuote=PROPAGATION_REQUIRED
org.spring21.package.IQuoteService.deleteQuote=PROPAGATION_REQUIRED
</value>
</property>
</bean>
<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames">
<list>
<value>...IQuoteService</value>
</list>
</property>
<property name="beanNames">
<list>
<idref bean="quoteService"/>
...
</list>
</property>
</bean>
<bean id="quoteService" class="...QuoteService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="client" class="...QuoteSerivceClient">
<property name="quoteService" ref="quoteService"/>
</bean>
</beans>
現在,我們只需要正常的向IoC容器的配置文件中追加相應的業務對象bean定義即可,BeanNameAutoProxyCreator將根據TransactionInterceptor提供的事務管理功能自動為添加到它的“ beanNames”列表的所有業務對象自動添加事務支持(當然,本質上是為其生成動態代理對象)。

 

無論應用中業務對象數量多少,使用BeanNameAutoProxyCreator都可以很便捷的處理這些業務對象的聲明式事務需求。 不過,可能在實際的開發過程中,你依然感覺使用BeanNameAutoProxyCreator有其不夠便捷之處,好消息就是,如果你的應用程序可以或者已經升級到spring2.x,那么,使用基於XSD的配置方式吧!

1.3.2.2.2.4. 使用Spring2.x的聲明事務配置方式

spring2.x后提供的基於Xml schema的配置方式專門為事務管理提供了單獨的一個命名空間用於簡化配置,結合新的aop命名空間, 現在的聲明式事務管理看起來要清晰許多:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:lang="http://www.springframework.org/schema/lang"
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-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<aop:config>
<aop:pointcut id="txServices" expression="execution(* cn.spring21.unveilspring.IQuoteService.*(..))"/>
<aop:advisor pointcut-ref="txServices" advice-ref="txAdvice"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="getQuate*" propagation="SUPPORTS" read-only="true" timeout="20"/>
<tx:method name="saveQuote"/>
<tx:method name="updateQuote"/>
<tx:method name="deleteQuote"/>
</tx:attributes>
</tx:advice>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost/dbName"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="..."/>
<property name="password" value="..."/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="quoteService" class="...QuoteService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="client" class="...QuoteSerivceClient">
<property name="quoteService" ref="quoteService"/>
</bean>
</beans>
<tx:advice>是專門為聲明事務Advice設置的配置元素,底層當然還是我們的TransactionInterceptor,僅僅是披一件新衣裳而已。 <tx:advice>的“ transaction-manager”指定了它要使用的事務管理器是哪一個,如果容器中事務管理器的beanName恰好就是“ transactionManager”,那么可以不明確指定。 <tx:advice>內部由<tx:attributes>提供聲明式事務所需要的元數據映射信息,每一條映射信息對應一個<tx:method/>元素聲明。 <tx:method/>只有“ name”屬性是必須指定的,其他的屬性代表事務定義的其他內容,比如propagation用於指定傳播行為,isolation用於指定隔離度(Isolation),timeout用於指定事務的超時時間等等, 如果不明確指定的話,將采用DefaultTransactionDefinition的設置內容。下表是<tx:method/>可用的屬性的詳細列表 [11]:

Table 1.2. <tx:method/>屬性對照表

屬性名 說明
name 事務元數據將要加諸於上的業務方法名稱,可以使用*通配符。
propagation 事務的傳播行為,不明確指定的話,默認值為REQUIRED,你會發現,該名稱去掉了TransactionDefintion中常量名稱的前綴,在指定的時候,需要注意。
isolation 用於指定事務的隔離度,不明確指定,默認值采用DEFAULT,也是TransactionDefintion中常量名稱去掉前綴。
timeout 事務的超時時間,默認值為-1。
read-only 指定事務是否為只讀事務,默認值為false;
rollback-for 用於指定能夠觸發事務回滾的異常類型,比如rollback-for=“cn.spring21.unveilspring.QuoteException”, 如果有多個異常類型需要指定,各類型之間可以通過逗號分隔。
no-rollback-for 與rollback-for正好相反,即使拋出no-rollback-for指定的異常類型也“”回滾事務。

通過<tx:advice>指定的事務信息需要通過SpringAOP的支持織入到具體的業務對象,所以,剩下的工作實際上是AOP的配置了:
<aop:config>
<aop:pointcut id="txServices" expression="execution(* cn.spring21.unveilspring.IQuoteService.*(..))"/>
<aop:advisor pointcut-ref="txServices" advice-ref="txAdvice"/>
</aop:config>
在SpringAOP一章我們已經說過,<aop:config>底層也是依賴於自動代理(Autoproxy)機制,所以,我一直強調,新的基於XSD的配置方式僅僅是換了一身簡潔明快的外衣, 而我想讓大家看到的是外衣里面的東西。

 

Note

更多使用<tx:advice>的內容可以參照spring2.x之后的參考文檔,其中有更多詳細內容,比如配置多個<tx:advice>以區分不同的事務需求等內容。

1.3.2.2.3. Annotation元數據驅動的聲明式事務

隨着Java5(Tiger)的發布,Annotation越來越受到開發人員的關注和喜愛,如果你的應用程序構建於Java5或者更高版本的虛擬機之上的話,那么恭喜你,現在你也可以使用Spring提供的基於Annotation的聲明式事務管理啦[12]

Annotation元數據驅動的聲明式事務管理的基本原理是,將對應業務方法的事務元數據直接通過Annotation標注到業務方法或者業務方法所在的對象之上, 然后在業務方法執行期間,通過反射(Reflection)讀取標注在該業務方法之上的Annotation所包含的元數據,最終將根據讀取的信息為業務方法構建事務管理的支持。

spring定義了org.springframework.transaction.annotation.Transactional用於標注業務方法所對應的事務元數據信息,通過Transactional,我們可以指定與<tx:method/>幾乎相同的信息,當然,現在不用指定方法名稱了, 因為Transactional直接標注到了業務方法或者業務方法所在的對象定義之上。通過查看Transactional的定義,我們可以獲取所有可以指定的事務定義內容:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional
{
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
要為我們的QuoteService添加基於Annotation的聲明式事務管理,需要為其添加Transactional以標注必要的事務管理信息:
@Transactional
public class QuoteService implements IQuoteService
{
private JdbcTemplate jdbcTemplate;
@Transactional(propagation=Propagation.SUPPORTS,readOnly=true,timeout=20)
public Quote getQuate() {
return (Quote)getJdbcTemplate().queryForObject("SELECT * FROM fx_quote where quote_id=2", new RowMapper(){
public Object mapRow(ResultSet rs, int row) throws SQLException {
Quote quote = new Quote();
// ...
return quote;
}});
}
@Transactional(propagation=Propagation.SUPPORTS,readOnly=true,timeout=20)
public Quote getQuateByDateTime(DateTime dateTime) {
throw new NotImplementedException();
}
public void saveQuote(Quote quote) {
throw new NotImplementedException();
}
public void updateQuote(Quote quote) {
throw new NotImplementedException();
}
public void deleteQuote(Quote quote) {
throw new NotImplementedException();
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
如果將@Transactional標注為對象級別的話,對象中的方法將“ 繼承”該對象級別上的@Transactional的事務管理元數據信息, 如果某個方法有特殊的事務管理需求,可以在方法級別添加更加詳細的@Transactional設定,比如我們的getQuote*()方法。通過將相同的事務管理行為提取到對象級別的@Transactional, 可以有效的減少標注的數量。如果不為@Transactional指定自定義的一些設定,它也會像<tx:method/>那樣采用DefaultTransactionDefinition一樣的事務定義內容。

 

僅僅通過@Transactional標注業務對象以及對象中的業務方法並不會為業務方法帶來任何事務管理的支持,@Transactional只是一個標志而已,需要我們在執行業務方法的時候通過反射讀取這些信息並根據這些信息構建事務才能使這些聲明的事務行為生效。 這就好像下面的代碼所演示的那樣:

...
public Quote getQuate()
{
try {
Method method = quoteService.getClass().getDeclaredMethod("getQuate", null);
boolean isTxAnnotationPresent = method.isAnnotationPresent(Transactional.class);
if(!isTxAnnotationPresent)
{
return (Quote)quoteService.getQuate();
}
Transactional txInfo = method.getAnnotation(Transactional.class);
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
if(!txInfo.propagation().equals(Propagation.REQUIRED))
transactionTemplate.setPropagationBehavior(txInfo.propagation().value());
if(txInfo.readOnly())
transactionTemplate.setReadOnly(true);
// ...
return (Quote)transactionTemplate.execute(new TransactionCallback(){
public Object doInTransaction(TransactionStatus txStatus) {
return quoteService.getQuate();
}});
} catch (SecurityException e) {
e.printStackTrace(); // don't do this
return null;
} catch (NoSuchMethodException e) {
e.printStackTrace(); // don't do this
return null;
}
}
...
不過,我們不用自己去寫這些底層的邏輯了,通過在容器的配置文件中指定如下一行配置,這些搜尋Annotation,讀取內容,構建事務等工作全都由spring的IoC容器幫我們搞定:
<tx:annotation-driven transaction-manager="transactionManager"/>
所以,使用Annotation元數據驅動的聲明式事務管理,基本上就需要做兩件事:
  1. 使用@Transactional標注相應的業務對象以及相關業務方法.  這一步通常由業務對象的開發者統一負責了,@Transactional的使用使得元數據以及業務邏輯的實現全部集中到了一處,有了IDE的支持, 管理起來更是得心應手。

  2. 在容器的配置文件中設定事務基礎設施.  我們需要添加<tx:annotation-driven transaction-manager="transactionManager"/>以便有人使用我們所標注的@Transactional, 並且,需要給出相應的事務管理器,要進行事務管理,沒有他可不行。

對於QuoteService來說,當已經使用@Transactional標注了相應的事務管理信息之后,剩下的就是對應容器配置的內容了:
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost/dbName"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="..."/>
<property name="password" value="..."/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="quoteService" class="...QuoteService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="client" class="...QuoteSerivceClient">
<property name="quoteService" ref="quoteService"/>
</bean>
至此,基於Annotation的聲明式事務就算大功告成了。

 

Tip

spring推薦將@Transactional標注於具體的業務實現類或者實現類的業務方法上, 之所以如此,是因為spring aop可以采用兩種方式來生成代理對象(dynamic proxy 或者cglib)。 如果將@Transactional標注於業務接口的定義上,那么,當使用Dynamic proxy機制構建代理對象的時候,讀取接口定義上的@Transactional信息是沒有問題的; 可是當使用CGLIB構建代理對象的時候,則無法讀取接口上定義的@Transactional數據。

1.4. spring事務管理之擴展篇

 

1.4.1. 理解並活用ThreadLocal

本章之初,在事務管理器實現原型的講解中,我們為您展示了ThreadLocal在Spring的事務管理框架中所起的核心作用,即通過ThreadLocal的使用來避免“connection passing”方式最初的尷尬局面。不過,我們依然認為這只是反映了ThreadLocal使用中的一個側面,為了讓大家更加深入的理解ThreadLocal並在之后的開發活動中靈活使用之,特追加這部分擴展內容。

1.4.1.1.  理解ThreadLocal的存在背景

ThreadLocal是Java語言提供的用於支持“線程局部變量(thread-local variable)”的標准實現類,我們可以在稱為“Thread-Specific Storage Pattern[13]的多線程編程模式中一窺ThreadLocal的典型應用場景。

國內的Java技術社區對ThreadLocal的理解可以說百家爭鳴,異常熱鬧,有的將ThreadLocal和synchronized放在一起討論,有的說他們之間沒有任何關系,還有的說ThreadLocal不是用來解決多線程環境下的對象共享問題的, 這些觀點通常會令許多開發人員對ThreadLocal更加迷惑。不能說這些觀點是不對的,但筆者認為這些都沒有接觸到真正的點子上! 那么,ThreadLocal到底是為何而生那?它又是否與之前的觀點有什么千絲萬縷的聯系那? 下面讓筆者帶您一起來探索這些問題的答案。

單單從程序層面來看,我們所編寫的代碼實際上是在管理系統中各個對象的相關狀態,如果不能夠對各個對象狀態的訪問進行合理的管理,對象的狀態將被破壞,進而導致系統的不正常運行。特別是多線程環境下,多個線程可能同時對系統中的單一或者多個對象狀態進行訪問,如果不能保證在此期間的線程安全,將會把整個系統帶向崩潰的邊緣。

為了保證整個應用程序的線程安全,我們可以采用多種方式,不過,在此之前,我們不妨先來看看打架與我們管理線程安全問題有何相似之處。現在你就是系統中的一個對象,假設某一天你出門在外遇到三名歹徒(當然,僅僅是假設,不用害怕,呵呵),你毫無退路,只能一搏,那么,你會采用什么樣的策略來保證你的人身安全那?畢竟,我們最終的目的都是保證你這個對象的狀態始終處於“正確”的狀態,對吧?!

策略一,你是個練家子,而且身邊並無他人相助,所以,你只能同時對付三名歹徒,為了不受傷害,你是輾轉騰挪,左躲右閃,盡量保證每一時刻都只是對付一名歹徒,以便最終能夠全身而退。如果把三名歹徒對你的攻擊順序比作三個線程的話,你實際上是在用同步(Synchronization)的方式來管理這三個線程對你的訪問;

使用同步(Synchronization)方式來管理多個線程對對象狀態的訪問以保證應用程序的線程安全是最常用的方式,不過,你也看到了,你需要很辛苦的來對待,稍一不慎,就有可能受傷啊。所以,這個時候我們就在想,我要是孫悟空有多好。

但見那孫猴子揪一撮猴毛一吹,片刻化作多個分身”,“小的們,給我上”,現在,我們再也不用苦熬了,讓一個分身對付一個歹徒(小妖),讓他們在各自的線程內各自折騰去吧!不用一對三,當然也就不可能破壞到我的完好狀態咯。

這就是策略二,通過避免對象的共享,同樣達到線程安全的目的。你想啊,都在各自的線程內跟各自的分身折騰去了,自然也就不會需要同步對單一共享資源的訪問啦。ThreadLocal的出現,實際上就是幫助我們以策略二的方式來管理程序的線程安全。只要當前環境允許,能不共享的盡量不共享,反而更容易管理應用程序的線程安全。

綜上來說那,Synchronization和ThreadLocal在橫向上可能沒有任何的關系,但從縱向上看,他們實際上都服務於同一個目的,那就是幫助我們實現應用程序的線程安全。 另外,說ThreadLocal不是用來解決多線程環境下對象共享問題的,也就更好解釋了, ThreadLocal的目的是通過避免對象的共享來保證應用程序實現中的線程安全,共享對象是ThreadLocal盡量避免的,如果要管理的對象非要共享,ThreadLocal自然不會理會這碼子事兒啦。

1.4.1.2. 理解ThreadLocal的實現

既然我們已經了解了ThreadLocal因何而生,現在該是我們探索ThreadLoca又是如何運作的時候了,它到底是如何來完成它的職責的那?

我們雖然是通過ThreadLocal來設置特定於各個線程的數據資源,但ThreadLocal自身不會保存這些特定的數據資源,既然是特定於線程的數據資源,自然是由每一個線程自己來管理啦。 每一個Thread類都有一個ThreadLocal.ThreadLocalMap類型的名為threadLocals的實例變量,它就是保持那些通過ThreadLocal設置給這個線程的數據資源的地方。 當我們通過ThreadLocal的set(data)方法來設置數據的時候,ThreadLocal會首先獲取當前線程的引用,然后通過該引用獲取當前線程持有的threadLocals,最后,以當前ThreadLocal作為Key,將要設置的數據設置到當前線程:

Thread thread = Thread.currentThread();
ThreadLocalMap threadlocalmap = thread.threadLocals;
...
threadlocalmap.set(this, obj);
而至於余下的get()之類的方法,基本上也是同樣的道理,都是首先取得當前線程,然后根據每個方法的語義對當前線程所持有的threadLocals中的數據進行操作。

 

實際上,ThreadLocal就好像是一個窗口,通過這個窗口,我們可以將特定於線程的數據資源綁定到當前線程, 我們也可以通過這個窗口獲取綁定的數據資源,當然,我們也可以解除之前綁定到當前線程的數據資源。在整個線程的生命周期內,你都可以通過ThreadLocal這個窗口與當前線程打交道。

為了更好的理解Thread與ThreadLocal之間的關系,我們不妨設想一下城市的公交系統:

Figure 1.16. 公交線路和車站與線程和ThreadLocal之間的類比示意圖

公交線路和車站與線程和ThreadLocal之間的類比示意圖

城市中的各條公交線路就好像我們系統中的那一個個線程,在各條公交線路上,會有相應的公交車輛, 這些公交車輛就好像Thread的threadLocals,用來運送特定於該條線路的乘客(數據資源)。 為了乘客可以乘車或者下車,各條公交線路沿線都會設置多個乘車點(Bus stop),而這些乘車點實際上就是ThreadLocal。 雖然同一個乘車點可能會有多條公交線路共用,但同一時間,乘客只會搭乘他要乘坐並且當前經過的公交車。這與ThreadLocal和Thread的關系是類似的, 同一個ThreadLocal雖然可以為多個線程指定數據資源,但只會將數據資源指定到當前的線程。

 

至此,您是否再也不會對ThreadLocal感覺神秘了那?

1.4.1.3. ThreadLocal的應用場景

ThreadLocal的概念和提供的功能其實很簡單,但如果能夠充分發揮ThreadLocal的能力, 將會為我們的開發工作帶來意想不到的效果。

基本上,我們可以從兩個方面來看待並靈活應用ThreadLocal:

Figure 1.17. ThreadLocal功能分析圖

ThreadLocal功能分析圖

  • 橫向上看,我們是更注重於ThreadLocal橫跨多個線程的能力, 這當然是ThreadLocal最初的目的之所在, 為了以更加簡單的方式來管理應用程序的線程安全, ThreadLocal干脆將沒有必要共享的對象不共享,直接為每一個線程分配一份各自特定的數據資源。

  • 縱向上看,我們則着眼於ThreadLocal於單一線程內可以發揮的能力, 通過ThreadLocal設置的特定於各個線程的數據資源可以隨着所在線程的執行流程“隨波逐流”。

當然,兩個方面不是嚴格獨立的,更多時候他們則是相互依存,緊密結合的。

 

在充分發揮ThreadLocal兩方面能力的基礎上,我們可以發掘出以下幾種ThreadLocal的應用場景:

  • 管理應用程序實現中的線程安全.  對於某些Stateful的或者非線程安全的對象,我們可以在多線程程序中為每一個線程都分配相應的副本(copy), 而不是讓多個線程共享該類型的某個單一對象,從而避免了需要協調多個線程對這些對象進行訪問的“危險”工作。

    在使用JDBC進行數據訪問的過程中,Connection對象就屬於那種Stateful並且非線程安全的類, 所以,為了保證多個線程使用Connection進行數據訪問過程中的安全,我們通過ThreadLocal為每一個線程分配了一個他們各自持有的Connection, 從而避免了對單一Connection資源的爭用。畢竟,在JDBC中是一個Connection對應一個事務, 如果所有的線程都共用一個Connection的話,那整個事務管理就有點兒失控的感覺了。

    在本章開始部分,當以簡化的形式向你展示spring的事務框架是如何對Connection進行管理的時候,我們只是強調了使用ThreadLocal來避免“Connection Passing”的尷尬, 而實際上,通過ThreadLocal來保證Connection在多線程環境下的正確使用,應該也是spring的事務框架使用ThreadLocal進行Connection管理的原因之一。

  • 實現當前程序執行流程內的數據傳遞.  這種場景下,我們更多關注的是在單一的線程環境中使用ThreadLocal,當剝離“線程安全”等因素的考慮之后, “connecton passing”實際上就可以看作這個場景下的一例。

    除此之外,我們也可以通過ThreadLocal來跟蹤保存線程內的日志序列,在程序執行的任何必要執行點將系統跟蹤信息添加到ThreadLocal,然后在合適的時點取出分析; 我們還可以通過ThreadLocal來保存某種全局變量,在線程內執行流程的某個時點設置該全局變量,然后在合適的位置獲取其值以做某些判斷工作等等。 只要你願意,任何合適的數據都可以通過ThreadLocal在單一線程內傳遞數據這一功能進行傳遞。

    采用ThreadLocal進行當前執行流程內的數據傳遞可以避免耦合性很強的方法參數形式的數據傳遞方式,但這有些像是讓數據隨着“暗流”漂泊的意思, 一旦處理不當就會出現“觸礁”之類的事故,比如資源沒有適當的清理導致系統行為差異之類,所以,通常應該通過一組框架類來規范並屏蔽對ThreadLocal的直接操作,盡量避免應用代碼的直接接觸。

  • 某些情況下的性能優化.  有些情況下,系統中一些沒有必要共享的對象被設置成了共享,為了保證應用程序的線程安全以及對象狀態的正確, 我們往往就得通過同步等方式對多線程的訪問進行管理和控制,這個時候,各個線程在走到這個共享對象的時候就得排隊, 一個一個的對該共享對象進行訪問,顯然,這將嚴重影響系統的處理性能,去銀行取款的時候,如果就一個窗口在營業,而你又急着取錢,可以想象一下你現在是一種什么樣的心情。 所以,能夠避免共享的時候,就盡量不要共享,多開幾個營業窗口,要比單一營業窗口的處理速度快的多。

    某些情況下,通過ThreadLocal這種“以空間換時間”的方式來管理對象訪問,可以收到更好的響應效果。

  • per-thread Singleton”.  當某項資源的初始化代價有些大,並且在整個執行流程中還會多次訪問它的時候, 為了避免在訪問的時候需要每次都去初始化該項資源,我們可以第一次將該資源初始化完成之后,直接通過ThreadLocal將其綁定到當前線程, 之后,所有對該資源的訪問都從當前線程獲取即可。

    這實際上與“實現當前程序執行流程內的數據傳遞”的應用場景很相似,不過,該場景更側重於資源管理, 所以,單獨羅列與此也不為過啦。

我想,靈活運用,ThreadLocal可以在更多場景中發揮作用, 故希望大家也能夠在平時的開發過程中發掘更多ThreadLocal的應用場景。

 

1.4.1.4. 使用ThreadLocal管理多數據源切換的條件

在spring的數據訪問一章我們介紹了如何實現一個簡單的AbstractRountingDataSource原型實現來管理多個數據源之間的切換功能, 不過,當時的原型實現可能讓你覺得過於簡單而不甚真實,所以,這里我們引入ThreadLocal來協助管理多數據源切換的條件,以期拋磚引玉,使得大家在日常的開發工作中靈活運用ThreadLocal帶給我們的便利。

在多數據源的切換過程中,切換的條件可能隨着應用程序的需求而各異, 而且,通常不會像我們的AbstractRoutingDataSource原型實現那樣只需要內部條件就可以實現數據源切換的判斷, 更多時候我們會需要外部條件的介入,這個時候就會有一個問題,如何為AbstractRoutingDataSource的實現子類傳入這些外部條件相關數據?!ThreadLocal這個時候就可以派上用場。

我們的思路是,通過ThreadLocal保存每一個數據源所對應的標志(該標志我們以枚舉類的形式給出), AbstractRoutingDataSource在通過determineCurrentLookupKey()獲取對應數據源的鍵值的時候,從ThreadLocal獲取當前線程所持有的數據源對應標志然后返回。 而至於說什么情況下使用哪一個具體的數據源的問題,則是由應用程序的需求來決定,只要在必要的地方將所要使用的具體數據源的對應標志通過ThreadLocal綁定到當前線程即可。

Procedure 1.3. 基於ThreadLocal管理多數據源切換條件的AbstractRoutingDataSource實現流程

  1. 假設我們有MAIN,INFO和DBLINK三個數據源可用,第一步要做的事情就是先給出一個枚舉類, 其中定義了對應這三個數據源的標志:

    public enum DataSources {
        MAIN,INFO,DBLINK;
        }
        

     

  2. 我們定義所使用的ThreadLocal,沒有“車站”,我們可沒法上車啊:

    public class DataSourceTypeManager
        {
        private static final ThreadLocal<DataSources> dsTypes = new ThreadLocal<DataSources>(){
        @Override
        protected DataSources initialValue() {
        return DataSources.MAIN;
        }
        };
        public static DataSources get()
        {
        return dsTypes.get();
        }
        public static void set(DataSources dataSourceType)
        {
        dsTypes.set(dataSourceType);
        }
        public static void reset()
        {
        dsTypes.set(DataSources.MAIN);
        }
        }
        

     

  3. 有了標志枚舉類和相應的ThreadLocal定義之后,我們就可以實現我們的AbstractRoutingDataSource了:

    public class ThreadLocalVariableRountingDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
        return DataSourceTypeManager.get();
        }
        }
        

     

  4. 現在我們需要將ThreadLocalVariableRountingDataSource以及相關的依賴注冊到IoC容器當中:

    <bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="url" value=".."/>
        <property name="driverClassName" value=".."/>
        <property name="username" value=".."/>
        <property name="password" value="."/>
        </bean>
        <bean id="infoDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="url" value=".."/>
        <property name="driverClassName" value=".."/>
        <property name="username" value=".."/>
        <property name="password" value="."/>
        </bean>
        <bean id="dblinkDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="url" value=".."/>
        <property name="driverClassName" value=".."/>
        <property name="username" value=".."/>
        <property name="password" value="."/>
        </bean>
        <bean id="dataSource" class="..ThreadLocalVariableRountingDataSource">
        <property name="defaultTargetDataSource" ref=""/>
        <property name="targetDataSources">
        <map key-type="..DataSources">
        <entry key="MAIN" value-ref="mainDataSource"/>
        <entry key="INFO" value-ref="infoDataSource"/>
        <entry key="DBLINK" value-ref="dblinkDataSource"/>
        </map>
        </property>
        </bean>
        
    注意,我們在配置ThreadLocalVariableRountingDataSource所使用的多個目標數據源的時候,使用了<map>的key-type屬性指明了鍵值的類型, 否則,你得通過其他的方式來確定枚舉類的各值作為key與目標數據源之間的對應關系。

     

  5. 萬事俱備之后,你可以使用如下代碼使得數據源的切換生效:

    DataSourceTypeManager.set(DataSources.INFO);
        // 或者
        DataSourceTypeManager.set(DataSources.DBLINK);
        ...
        
    此后通過ThreadLocalVariableRountingDataSource所進行的數據訪問則會使用你所設定的具體數據源。

     

    至於說以上代碼你要放在什么地方,可以因應用程序而異,比如你可以在程序或者某個線程啟動之后進行設置; 你也可以在某個AOP的Interceptor中根據條件判斷來進行設定以實現數據源的切換,等等。

至此,打完收功。 怎么樣?你現在是否對ThreadLocal的應用躍躍欲試了那?

1.4.2. 談Strategy模式在開發過程中的應用

Strategy模式的本意是封裝一系列可以互相替換的算法邏輯,使得具體算法的演化獨立於使用它們的客戶端代碼。 為了理解為什么要這么做,我們不妨來看一個具體的場景:

在一個信貸系統中,通常會提供多種還款方式,比如等額本金還款方式,等額本息還款方式,一次還本付息方式等等, 那么,針對每一位顧客所選擇的還款方式,我們就需要按照這些還款方式的具體邏輯為顧客計算每次所需要歸還的本金以及利息的額度, 如果要我們來實現這個根據還款方式計算額度的邏輯,我們會怎么做那?

對於諳熟結構化編程或者面向對象編程不甚嫻熟的開發人員來說,他們可能會直接使用多重條件語句來實現這段計算邏輯:
public RepaymentDetails calculateRepayment(BigDecimal totalAmount,String customerId)
{
RepaymentDetails details = new RepaymentDetails();
Object type =  getRepaymentTypeByCustomerId(customerId);
if(isEqualInterestRepaymentType(type))
{
BigDecimal interest = getEqualInterestOfCentrelBank();
YearMonthDay repaymentInterval = getRepaymentIntervalByCustomerId(customerId);
// carry out caculation according to totalAmount and other data
}
if(isEqualPrincipalRepaymentType(type))
{
BigDecimal interest = getStandardInterestOfCentrelBank();
YearMonthDay repaymentInterval = getRepaymentIntervalByCustomerId(customerId);
// carry out caculation according to totalAmount and other data
}
if(isOnceForAll(type))
{
BigDecimal interest = getStandardInterestOfCentrelBank();
// carry out caculation according to totalAmount and other data
}
...
return details;
}
當然,你可以對這些代碼做進一步的改進,但是,如果總體結構上不做任何變更的話,這種實現方式暴露的問題會依然存在:
  • 客戶端代碼與算法邏輯代碼相互混雜, 導致客戶端代碼的過於復雜並且后期難以維護;

  • 混雜的算法邏輯代碼與客戶端代碼耦合性太強,算法的變更或者添加新的算法都會直接導致客戶端代碼的調整,使得客戶端代碼和算法邏輯代碼無法獨立演化;

  • 幾乎同一邏輯單元內實現的各種算法無可避免的需要多重的條件語句來區分針對不同算法所使用的數據或者對應算法的特定邏輯實現;

所以,該是Strategy模式登場的時間啦!

 

使用Strategy模式來重構這段代碼的話,我們首先通過RepaymentStrategy定義來抽象還款邏輯算法, 然后,針對不同的還款方式,給出RepaymentStrategy定義的不同實現。對於使用還款邏輯的客戶端代碼來說, 它只需要獲取相應的RepaymentStrategy引用,並調用接口暴露的計算接口即可:

Figure 1.18. RepaymentStrategy場景圖

RepaymentStrategy場景圖

客戶端代碼只需要跟策略接口打交道,而算法的變更以及添加對於使用策略接口進行計算操作的客戶端代碼來說幾乎沒有任何影響。

 

Strategy模式雖然定義上強調的是對算法的封裝,但我們不應該只着眼“算法”一詞,實際上, 只要能夠有效的剝離客戶端代碼與特定關注點之間的依賴關系,Strategy模式就應該進入考慮之列,在這一點上, spring框架的事務抽象就是一個很好的范例,通過將使用不同事務管理API進行事務管理的界定行為進行統一的抽象, 客戶端代碼可以透明的方式使用PlatformTransactionManager這一策略接口進行事務界定,即使具體的事務策略需要變更, 對於客戶端代碼來說也不會造成過大的沖擊。

spring框架中使用Strategy模式的地方很多,除了本章的事務抽象框架,還包括以下幾處:

  • IoC容器根據bean定義的內容實例化相應bean對象的時候,會根據情況決定使用反射還是使用cglib來實例化相應的對象。 InstantiationStrategy是容器使用的實例化策略的抽象接口,spring框架默認提供了CglibSubclassingInstantiationStrategy和SimpleInstantiationStrategy兩個具體實現類。

  • spring的validation框架中,org.springframework.validation.Validator定義也是一個策略接口, 具體的實現類將根據具體場景提供不同的驗證邏輯,而這些具體驗證邏輯的差異性,對於使用Validator進行數據驗證的客戶端代碼來說,則是透明的。

除了在spring框架內大量使用Strategy模式,我們也可以在其他的框架設計中發現Strategy模式的影子,比如我們最常用的jakarta commons logging中,Log接口就是一個策略接口, Jdk14Logger,Log4JLogger以及SimpleLog等都是具體的策略實現類。 可見,只要針對同一件事情有多種選擇的時候,我們都可以考慮用Strategy模式來統一一下抽象接口,為客戶端代碼“ 造福”。

 

Strategy模式的重點在於通過統一的抽象向客戶端屏蔽其所依賴的具體行為,但該模式並沒有關注客戶端代碼應該如何來使用這些行為。 一般來講,客戶端代碼使用Strategy模式的方式可以簡單划分為兩種:

  • 客戶端整個生命周期內只依賴於單一的策略.  Spring提供的事務抽象可以歸屬這一類情況。使用PlatformTransactionManager進行事務界定的客戶端代碼在其整個生命周期內只依賴於一個PlatformTransactionManager的實現類, 或者DataSourceTransactionManager,或者HibernateTransactionManager等等,這樣的情況比較容易處理,直接為客戶端代碼注入所需要的策略實現類即可。

  • 客戶端整個生命周期內可能動態的依賴多個策略.  比如我們的還款場景中,客戶端可能需要根據每一個顧客所選擇的還款方式來決定使用哪一個策略實現類為其計算對應的還款明細, 對於這種情況,你會發現,Strategy模式通常宣稱的可以避免多重條件語句的問題其實僅僅是將其轉移給了客戶端代碼而已:

    RepaymentStrategy strategy = fallbackStrategy();
        ...
        public RepaymentDetails calculateRepayment(BigDecimal totalAmount,String customerId)
        {
        Object type =  getRepaymentTypeByCustomerId(customerId);
        if(isEqualInterestRepaymentType(type))
        {
        strategy = EqualInterestStrategy();
        }
        if(isEqualPrincipalRepaymentType(type))
        {
        strategy = EqualPrincipalStrategy();
        }
        if(isOnceForAll(type))
        {
        strategy = OnceForAllStrategy();
        }
        ...
        return strategy.performCalculation();
        }
        
    不過,如果你想真正的避免多重條件語句的話,也不是沒有辦法,最簡單的方法就是提前准備一個具體策略類型與其對應條件之間的關系映射。 對於還款的場景來說,我們可以這么做:

     

    1. 在客戶端代碼中聲明一個對關系映射的依賴:

      public class StrategyContext
              {
              private Map<Object,RepaymentStrategy> strategyMapping;
              ...
              // setters and getters
              }
              

       

    2. 通過ioc容器注入所有客戶端可能動態依賴的策略實現類實例:

      <bean id="strategyContext" class="...StrategyContext">
              <property name="strategyMapping">
              <ref local="strategyMapping"/>
              </property>
              </bean>
              <util:map id="strategyMapping">
              <entry key="EQAUL_INTEREST">
              <bean class="...EqualInterestStrategy"></bean>
              </entry>
              <entry key="EQAUL_PRINCIPAL">
              <bean class="...EqualPrincipalStrategy"></bean>
              </entry>
              <entry key="ONCE_FOR_ALL">
              <bean class="...OnceForAllStrategy"></bean>
              </entry>
              </util:map>
              

       

    3. 在計算還款明細的方法中,使用還款策略的代碼直接從關系映射中獲取具體的策略即可:

      RepaymentStrategy strategy = fallbackStrategy();
              ...
              public RepaymentDetails calculateRepayment(BigDecimal totalAmount,String customerId)
              {
              Object type =  getRepaymentTypeByCustomerId(customerId);
              RepaymentStrategy strategy = strategyMapping.get(type);
              // check constraint
              if(strategy == null)
              stargety = fallbackStrategy();
              return strategy.performCalculation();
              }
              

       

    Tip

    除了使用ioc容器注入映射關系,你還可以將對應應用程序的映射關系放到數據庫或者其他外部配置文件,甚至Annotation中。 通過ioc容器一次注入多個策略實例可能需要占用多一些的系統資源,對於資源緊要的應用來說,可以考慮通過反射等方式按需構建具體策略實例,這個就留給讀者來完成吧!

 

在系統中合理的使用Strategy模式可以使得系統向着“高內聚,低耦合”的理想方向邁進,在改善應用程序代碼結構的同時,進一步的提高產品質量。 實際上,Strategy模式更是多態(Polymorphism)的完美體現,當你的OO內功修煉到“爐火純青”之地的時候,你也就發現,所謂的Strategy模式的概念,在你的腦海中或許已經淡然了。

1.4.3. Spring與JTA背后的奧秘(Magic behind Spring and JTA)

無論是spring的參考文檔還是大多數介紹spring的書籍在提到使用Spring的JtaTransactionManager進行分布式事務管理的時候, 都強調需要使用從應用服務器的JNDI服務獲取的dataSource,而不是本地配置的普通dataSource:

Figure 1.19. JtaTransactionManager和DataSource之間的是與非

JtaTransactionManager和DataSource之間的是與非

那么原因是什么那?

 

我們知道事務管理是要加諸於具體的事務資源之上的,所以,通常的PlatformTransactionManager的實現都會有相對應的事務資源的引用,比如,DataSourceTransactionManager會需要指定DataSource,HibernateTransactionManager會需要指定SessionFactory等等:

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
從常識上來講,像DataSourceTransactionManager這樣才是正常的情況, 而我們也看到JtaTransactionManager確實沒有明確指定依賴於哪一個資源,卻依然能夠將需要加入分布式事務的資源納入其管理范圍,那么,它是怎么做到的那? 如果揭開這個謎團,是否就能搞清楚當使用JtaTransactionManager的時候,要求我們使用從應用服務器的JNDI服務查找到的DataSource的真正原因那?

 

在介紹JtaTransactionManager的時候,我們說到JtaTransactionManager只是對各種具體的JTA實現產品提供的分布式事務管理功能進行了封裝, 最終的工作JtaTransactionManager都會委派給具體的JTA實現來做,所以,追根溯源,我們不得不追到JTA規范以及JTA實現中去。

略去JTA以及X/Open規范千言不提,我們還是長話短說,直接進入正題吧![14]

首先,具體的事務資源(RDBMS,MessageQueue,etc)要加入JTA管理的分布式事務,JTA規范要求其實現javax.transaction.xa.XAResource接口, 所以,希望加入JTA管理的分布式事務的資源管理器(RM)通常會提供相應的適配器(Adaptor)用於提供基於XAResource的分布式事務交互能力,比如關系數據庫提供的支持XA的JDBC驅動程序就屬於這樣的適配器(Adaptor)。 這樣,所有資源管理與事務交互的工作基本上就由RM的Adaptor來統一管理啦。

當想要參與JTA分布式事務的事務資源擁有了XAResource支持之后,JTA的javax.transaction.TransactionManager(我們稱其為JTA TransactionManager,區別於spring的JtaTransactionManager)與RM之間就可以進行通信:

Figure 1.20. TM與RM之間的通信

TM與RM之間的通信

Adaptor通常都有應答能力,這樣,當JTA TransactionManager使用兩階段提交協議管理分布式事務的過程中,可以同每一個RM之間進行交互。

 

不過,JTA TransactionManager在事務管理過程中要與哪一些RM打交道卻不是由它自己說了算的,想要參與JTA分布式事務的RM何時何地甚至怎樣加入JTA TransactionManager管理的分布式事務, 也不是每一個RM自己說了算,JTA TransactionManager與各個RM之間的聯系要由ApplicationServer(一般意義上的TP Monitor)來進行協調! ApplicationServer為基於JTA的分布式事務提供運行時環境,並負責協調JTA TransactionManager與各RM之間的交互:

Procedure 1.4. ApplicationServer內部的運作

  1. ApplicationServer一開始當然要先通過JNDI綁定它的JTA實現中的UserTransaction或者TransactionManager具體實現類, 這樣,客戶端應用程序就可以通過JNDI獲取他們。現在客戶端應用程序想要開始一個分布式事務,進而UserTransaction或者TransactionManager的相應方法被調用;

  2. ApplicationServer內部會要求TransactionManager為當前事務分配一個唯一的標志(以Xid表示),然后開始事務,並將當前事務綁定到當前線程;

  3. 客戶端跟ApplicationServer要求相應的數據資源進行數據訪問,ApplicationServer會跟RM的Adaptor要一個事務資源對象,我們暫且稱之為TransactionalResource,該資源對象包含兩部分,一部分是JTA TransactionManager需要與之交互的XAResource,另一部分是要暴露給客戶端應用程序使用的Connection資源, 取得TransactionalResource之后,ApplicationServer要做兩件事情:

    1. ApplicationServer從TransactionalResource中取得XAResource給TransactionManager,TransactionManager開始通過獲得的這個XAResource與RM進行交互, 實際上,現在TransactionManager只是調用XAResource的start(xid)方法通知RM開始記錄;

    2. ApplicationServer然后再把跟XAResource屬於同一個TransactionalResource的Connection傳給客戶端應用程序使用, 然后客戶端應用程序就可以使用ApplicationServer傳給的Connection進行數據訪問操作了。

  4. 客戶端應用程序數據訪問操作完成,關閉之前ApplicationServer傳給的Connection,ApplicationServer在感知到Connection被關閉之后,會通知TransactionManager, TransactionManager則調用與這個Connection屬於同一個TransactionalResource的XAResource的end(xid)方法結束事務記錄;

    • 如果在當前分布式事務期間還有使用其他RM進行的數據操作,ApplicationServer以幾乎同樣的方式從RM的Adaptor那里獲取TransactionalResource類似的對象, 然后協調TransactionManager重復余下的工作;

  5. 當客戶端通過UserTransaction或者TransactionManager的相應方法要求結束事務的時候,ApplicationServer就會通知TransactionManager使用兩階段提交協議提交當前事務:

    1. TransactionManager調用XAResource的prepare(xid)方法通知各個RM准備提交事務;

    2. 如果各個XAResource回答全部OK,TransactionManager調用XAResource的commit(xid)方法通知各個RM最終提交事務;

可見,各個RM確實參與到了JTA TransactionManager所管理的分布式事務中,只不過,參與的過程由ApplicationServer對客戶端應用程序屏蔽了。 之所以要求客戶端應用程序通過應用服務器的JNDI獲取DataSource等資源,是因為只有使用ApplicationServer暴露的與XAResource綁定到同一TransactionalResource的Connection, 才可以保證客戶端應用程序所做的所有數據訪問操作能夠加入ApplicationServer所協調的分布式事務中:

Figure 1.21. ApplicationServer暴露的Connection

ApplicationServer暴露的Connection

ApplicationServer為客戶端應用程序暴露與當前分布式事務相關的Connection的方式,就是實現一個DataSource,然后把該DataSource綁定到JNDI,這樣,客戶端應用程序就可以通過從JNDI取得的DataSource中獲取與事務相關的Connection了。 如果你使用本地定義的DataSource,因為它與當前分布式事務不發生任何關系,所以,也就根本不可能參與到分布式事務中去。

 

不過,如果你使用的JTA實現不是相應的ApplicationServer提供的,比如,可以獨立使用的Atomikos或者JOTM等JTA實現,要求你從應用服務器的JNDI服務取得相應的DataSource也是不成立的, 這個時候,你直接使用各個JTA產品提供的DataSource封裝類進行數據訪問即可,與ApplicationServer屏蔽掉RM與TransactionManager之間的關系一樣,這些產品也有與ApplicationServer完成同樣工作的角色為我們關聯具體的RM與當前產品的TransactionManager。 下面是在spring中使用Atomikos的典型配置方式:

<bean id="datasource1"
class="com.atomikos.jdbc.SimpleDataSourceBean" init-method="init" destroy-method="close">
<property name="uniqueResourceName" value="XADBMS_ONE"/>
<property name="xaDataSourceClassName" value="COM.FirstSQL.Dbcp.DbcpXADataSource"/>
<property name="xaDataSourceProperties" value="user=username;portNumber=8000"/
<property name="exclusiveConnectionMode" value="true"/>
</bean>
<bean id="datasource2"
class="com.atomikos.jdbc.SimpleDataSourceBean" init-method="init" destroy-method="close">
<property name="uniqueResourceName" value="XADBMS_TWO"/>
<property name="xaDataSourceClassName" value="COM.FirstSQL.Dbcp.DbcpXADataSource"/>
<property name="xaDataSourceProperties" value="user=username;portNumber=8000"/
<property name="exclusiveConnectionMode" value="true"/>
</bean>
<bean id="atomikosTransactionManager"
class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init" destroy-method="close">
<property name="forceShutdown" value="true"/>
</bean>
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">
<property name="transactionTimeout" value="200"/>
</bean>
<bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="atomikosTransactionManager"/>
<property name="userTransaction" ref="atomikosUserTransaction"/>
</bean>
<bean id="dao" class= "...">
<property name="dataSource" ref="datasource"/>
</bean>
我們需要告知Spring的JtaTransactionManager使用Atomikos的UserTransaction和TransactionManager實現,而至於Atomikos的UserTransaction和TransactionManager到底如何與RM(database, messsage queue,etc)進行交互,那就是Atomikos的事情了。

Note

不要被Atomikos的SimpleDataSourceBean的名字給迷惑了,另外,只要你的數據庫提供了有XA支持的數據庫驅動,你就可以通過SimpleDataSourceBean來配置需要的支持XA的DataSource,但各個參數的設置需要參考相應驅動程序的文檔, 比如xaDataSourceClassName以及xaDataSourceProperties屬性。

多個Atomikos的SimpleDataSourceBean存在的情況下,他們對應的uniqueResourceName必須是不同的!!!

 

最后的內容是專門為spring的JtaTransactionManager的,2.5版本的spring發布后,在XSD的配置中,可使用tx命名空間下專門為JtaTransactionManager提供的配置元素來簡化其配置:

<tx:jta-transaction-manager/>
當然,如果需要,也可以通過內嵌的<property>指定使用自定義的UserTransaction或者TransactionManager。

 



[1這里的“正確”有所泛指,比如,系統狀態需要始終完整,各項數據的關系始終保持一致等等,當系統各項狀態以合理的形式存在的時候,那我們說系統處於一個正確的狀態,這樣的表達方式我想應該可以接受吧?!

[2也可稱為“隔離度”,大家只需要知道我們指的都是Isolation這個概念就可以。

[3又稱“幻影讀”。

[4更確切的說是JTA(Java Transaction API)以及JTS規范(Java Transaction Service Specification)

[5事務界定規定了一個事務什么時候開始以及什么時候結束

[6事務的上下文傳播指的是,在整個事務處理工程中,如何在各事務操作之間傳播事務信息的行為。

[7這里的connection不是特指java.sql.Connection類型,而是泛指應用程序與事務資源之間的通信通道,對於JDBC來說,恰好對應java.sql.Connection,如果是Hibernate,那就應該是Session,諸如此類

[8實際上,你可以通過配置來決定讓Hibernate使用基於Jdbc Connection的局部事務管理,還是讓Hibernate使用分布式的事務管理。

[9當然,如果只是檢測單條數據插入的主鍵沖突,然后改為更新數據的話,更多時候,我們會直接在一個數據訪問方法中搞定。

[10因為UML的Sequence圖對於條件判斷等邏輯無法以恰當的方式表示,而Activity圖整個表示下來過大,不便顯示,所以,最后決定以文字描述的方式進行。

[11實際上,只要有一個支持XSD的XML編輯器,對於我們來說根本就不用記住每一個屬性的具體特征,通常這種編輯器都會有提示功能, 只要提示的每個屬性你知道它具體代表什么意思,剩下的就是個選擇的問題。

[12當然,即使你不使用Java5,也不意味着你不能使用源碼中的元數據驅動的聲明式事務,比如你可以使用commons attributes(http://commons.apache.org/attributes/),但是, Java5的Annotation可以為我們帶來更多的東西,比如編譯器檢查,IDE良好的重構支持等等。 如果當前應用無法使用Java5,而又要使用源代碼中元數據驅動的聲明式事務,你可以參照spring參考文檔中提供的使用commons attributes的相關信息。

[13在《Java多線程設計模式》一書中,對“Thread-Specific Storage Pattern”有詳細的介紹和講解。

[14實際上JTA規范本身不到100頁,想要更深入的了解JTA的內容的話,看一下也無妨啊!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM