1. 程式人生 > >淺談Spring宣告式事務管理ThreadLocal和JDKProxy

淺談Spring宣告式事務管理ThreadLocal和JDKProxy

寫這篇文章的目的,為了使大家更好的理解和摸清事務的規律,希望對新手學習事務這塊內容時有所幫助。

   在我們開發一個應用時,很多時候我們的一個業務操作會對資料庫進行多次操作,有時候我們需要保證這麼一系列的操作要麼全部成功,要麼全部失敗,其實這個這個概念就是我們今天要談論的事務。
  
   現在我們開發應用一般都採用三層結構,如果我們控制事務的程式碼都放在DAO(DataAccessObject)物件中,在DAO物件的每個方法當中去開啟事務和關閉事務,當Service物件在呼叫DAO時,如果只調用一個DAO,那我們這樣實現則效果不錯,但往往我們的Service會呼叫一系列的DAO對資料庫進行多次操作,那麼,這個時候我們就無法控制事務的邊界了,因為實際應用當中,我們的Service呼叫的DAO的個數是不確定的,可根據需求而變化,而且還可能出現Service呼叫Service的情況,看來手工來控制事務對於一個稍微嚴謹一點的系統來說完全是不現實的。

   那麼現在我們有什麼好的解決辦法嗎?還記得EJB引以為傲的宣告式事務嗎,雖然它現在已經慢慢沒落,但是它的思想被後人所吸取,我們的Spring框架是一個輕量級框架,它同樣的實現了宣告式事務的支援,使我們能夠通過配置及可插拔的方式的完成整個應用的事務的管理。

  
   談到Sping事務,我們今天要說到的一個東東是ThreadLocal,早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。簡單的說,ThreadLocal是為每個執行緒儲存一份變數,各個執行緒訪問自己對應的變數,所以我們就可以不使用synchronized關鍵字同樣可以實現執行緒同步,要了解關於ThreadLocal的詳細資訊,請參看
http://hi.baidu.com/cjjic02/blog/item/1ba41813aabde8886438dbe5.html



為了簡單明瞭,今天我們先拋開AOP,還是先用手工的方式通過ThreadLocal來管理連線,廢話不多說,先來看程式碼
TransactionHelper
Java程式碼  收藏程式碼
  1. package com.hwadee.demo;  
  2. import java.io.IOException;  
  3. import java.io.InputStream;  
  4. import java.sql.Connection;  
  5. import java.sql.DriverManager;  
  6. import
     java.sql.SQLException;  
  7. import java.util.Properties;  
  8. publicfinalclass TransactionHelper {  
  9.     //使用ThreadLocal持有當前執行緒的資料庫連線
  10.     privatefinalstatic ThreadLocal<Connection> connection_holder = new ThreadLocal<Connection>();  
  11.     //連線配置,來自connection.properties
  12.     privatefinalstatic Properties connectionProp = 
    new Properties();  
  13.     static{       
  14.         //載入配置檔案
  15.         InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("connection.properties");  
  16.         try {  
  17.             connectionProp.load(is);  
  18.             is.close();  
  19.             //載入驅動程式
  20.             Class.forName(connectionProp.getProperty("driverClassName"));  
  21.         } catch (IOException e) {  
  22.              thrownew RuntimeException(e.getMessage(),e);  
  23.         }catch(ClassNotFoundException e){  
  24.             thrownew RuntimeException("驅動未找到",e);  
  25.         }  
  26.     }  
  27.     //獲取當前執行緒中的資料庫連線
  28.     privatestatic Connection getCurrentConnection()  
  29.     {  
  30.         Connection conn = connection_holder.get();  
  31.         if(conn == null){  
  32.             conn =  createNotAutoCommitConnection();              
  33.             connection_holder.set(conn);  
  34.         }  
  35.         return conn;  
  36.     }  
  37.     //執行SQL語句
  38.     publicstaticint executeNonQuery(String sql) throws SQLException{  
  39.         Connection conn = getCurrentConnection();  
  40.         return conn.createStatement().executeUpdate(sql);  
  41.     }  
  42.     //提交事務
  43.     publicstaticvoid commit(){  
  44.         Connection conn = getCurrentConnection();  
  45.         try {  
  46.             conn.commit();  
  47.             conn.close();  
  48.             connection_holder.set(null);  
  49.         } catch (SQLException e) {  
  50.             thrownew RuntimeException(e.getMessage(),e);  
  51.         }  
  52.     }  
  53.     //回滾事務
  54.     publicstaticvoid rollback(){  
  55.         Connection conn = getCurrentConnection();  
  56.         try {  
  57.             conn.rollback();  
  58.             conn.close();  
  59.             connection_holder.set(null);  
  60.         } catch (SQLException e) {  
  61.             thrownew RuntimeException(e.getMessage(),e);  
  62.         }  
  63.     }  
  64.     //建立一個不自動Commit的資料庫連線
  65.     privatestatic Connection createNotAutoCommitConnection() {  
  66.         try {  
  67.             Connection conn = DriverManager.getConnection(connectionProp.getProperty("url")+";databaseName="+ connectionProp.getProperty("databaseName")  
  68.                     ,connectionProp.getProperty("username")  
  69.                     ,connectionProp.getProperty("password"));  
  70.             conn.setAutoCommit(false);  
  71.             return conn;  
  72.         } catch (SQLException e) {  
  73.              thrownew RuntimeException(e.getMessage(),e);  
  74.         }  
  75.     }     
  76. }  

這個類實現了基本的連線管理與執行SQL語句的方法,可以在多執行緒環境下執行


程式入口
Java程式碼  收藏程式碼
  1. package com.hwadee.demo;  
  2. import java.sql.SQLException;  
  3. publicclass MainModule {  
  4.     publicstaticvoid main(String[] args) {          
  5.         try{  
  6.             insert1();  
  7.             insert2();  
  8.             //方法1和2都無異常,提交事務,任何一個方法出現異常都將導致事務回滾。
  9.             TransactionHelper.commit();  
  10.         }catch(SQLException e){           
  11.             TransactionHelper.rollback();  
  12.             thrownew RuntimeException(e.getMessage(),e);  
  13.         }catch(RuntimeException e){            
  14.             TransactionHelper.rollback();  
  15.             thrownew RuntimeException(e.getMessage(),e);  
  16.         }  
  17.     }  
  18.     staticvoid insert1() throws SQLException{        
  19.         String sql = "insert into department values(1,'市場部')";  
  20.         TransactionHelper.executeNonQuery(sql);        
  21.     }  
  22.     staticvoid insert2() throws SQLException{        
  23.         String sql = "insert into department values(2,'研發部')";  
  24.         TransactionHelper.executeNonQuery(sql);   
  25.         //throw new RuntimeException("回滾");     
  26.     }  
  27. }  



連線字串配置,請將此檔案放入classpath根目錄中
connection.properties
Java程式碼  收藏程式碼
  1. url=jdbc:sqlserver://localhost:1433
  2. databaseName=pubs  
  3. username=sa  
  4. password=password  
  5. driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver  



建表語句
Sql程式碼  收藏程式碼
  1. USE [pubs]  
  2. go  
  3. CREATETABLE [Department](  
  4.     [DEPT_ID] [intprimarykey,  
  5.     [DEPT_NAME] [varchar](50)  
  6. )  
  7. GO  



   好了現在執行這個應用,可以正常的插入兩條資料,接下來,取消insert2方法裡面的註釋,再執行看看效果。
Java程式碼  收藏程式碼
  1. staticvoid insert2() throws SQLException{        
  2.         String sql = "insert into department values(2,'研發部')";  
  3.         TransactionHelper.executeNonQuery(sql);   
  4.         thrownew RuntimeException("回滾");         
  5.     }  


很重要的一點是要想實現事務,我們必須用同一個資料庫連線執行這些語句,最終才能做到統一的提交和回滾。
我們可以這樣假設
insert1和insert2為不同DAO的方法
仔細觀察,我們的insert1和insert2並沒有負責開啟連線和關閉連線。而是間接的呼叫TransactionHelper.executeNonQuery(sql);
這樣使我們執行的所有方法都是使用同一個連線進行資料庫操作。

   其實這個例子只是想告訴大家要實現宣告式事務的一部分內容,這個例子只能實現簡單的單事務模型,要實現更復雜的事務傳播模型如巢狀等,還需要我們使用更多的技術,如AOP等等。先寫到這裡,希望對大家有所幫助!

   感謝大家的支援,應為最近在比較忙,一直沒有更新此貼,週末終於有時間可以繼續來完成這篇文章了。
   
    前面我們講到了ThreadLocal管理連線,當然這麼一點內容確實和Spring的宣告式事務沒有多大聯絡,前面的例子還是由我們自己在管理事務的起點和終點,但大多數時候我們在編寫一個業務邏輯時並不能確定事務的邊界,而卻隨著系統越發複雜化,之前的一個事務可能會作為另一個業務邏輯的子事務,那要做到事務原子性,我們就根本沒辦法在程式碼裡面去寫事務控制的邏輯,我們需要一種能夠靈活的配置的方式來管理事務,這樣,我們只需要在配置檔案裡面配哪些物件的哪些方法需要事務,而且可以配置事務的傳播特性。

這裡簡要的看看事務傳播特性
Spring在TransactionDefinition介面中7種類型的事務傳播行為,它們規定了事務方法和事務方法發生巢狀呼叫時事務如何進行傳播:

1.PROPAGATION_REQUIRED
如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。

2.PROPAGATION_SUPPORTS
支援當前事務,如果當前沒有事務,就以非事務方式執行。

3.PROPAGATION_MANDATORY
使用當前的事務,如果當前沒有事務,就丟擲異常。

4.PROPAGATION_REQUIRES_NEW
新建事務,如果當前存在事務,把當前事務掛起。

5.PROPAGATION_NOT_SUPPORTED
以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

6.PROPAGATION_NEVER
以非事務方式執行,如果當前存在事務,則丟擲異常。

7.PROPAGATION_NESTED
如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

   我們看到了上面的定義的7中事務傳播特性,那麼Spring到底是如何來實現事務傳播特性呢?Proxy,使用Proxy,我們為所有需要實現事務的物件建立一個代理物件,代理物件能夠在目標物件的方法被呼叫的前後,加入事務判斷的邏輯,這樣子就可以實現事務傳播特性,這就是我們下面要講的AOP。在Java下實現AOP最常用的兩種方案分別為JDK動態代理和CGLib動態代理,它們各有優勢。下面簡單的對比一下。

JDK動態代理                    |                CGlib動態代理
JDK原生支援                    |                需要第三方庫
只能針對介面代理                |               可以針對介面和類進行代理
建立代理的速度快                |             建立代理速度慢
代理物件效能一般                |             代理物件效能很好

  根據上面的這些特性,總結如下,對於Singleton的代理物件或者具有例項池的代理,因為無須頻繁建立代理物件,比較適合用CGLib動態代理技術,反之適合用JDK動態代理技術。
順便要提一點,由於CGLib採用動態建立子類的方式生成代理,所以不能對目標類中的final方法進行代理。

   這裡我們來看一個JDK動態代理實現Required事務的例子,程式碼比較多,需要大家一點點耐心,先來看看程式碼。

介面 DAO 使用JDK動態代理必須要有介面
Java程式碼  收藏程式碼
  1. package com.hwadee.demo.aop;  
  2. publicinterface DAO {  
  3.     void doWork();  
  4. }  


實現類 DAOImpl1
Java程式碼  收藏程式碼
  1. package com.hwadee.demo.aop;  
  2. import java.sql.SQLException;  
  3. import com.hwadee.demo.TransactionHelper;  
  4. publicclass DAOImpl1 implements DAO {  
  5.     publicvoid doWork() {  
  6.         System.out.println(this.getClass().getName() + "." + "doWork  Invoke");  
  7.         String sql = "insert into department values(1,'市場部')";  
  8.         try {  
  9.             TransactionHelper.executeNonQuery(sql);  
  10.         } catch (SQLException e) {  
  11.             thrownew RuntimeException(e.getMessage(), e);  
  12.         }  
  13.         // 呼叫dao2
  14.         DAO dao2 = (DAO) BeanFactory.getBean("dao2");  
  15.         dao2.doWork();  
  16.     }  
  17. }  


實現類 DAOImpl2
Java程式碼  收藏程式碼
  1. package com.hwadee.demo.aop;  
  2. import java.sql.SQLException;  
  3. import com.hwadee.demo.TransactionHelper;  
  4. publicclass DAOImpl2 implements DAO {  
  5.     publicvoid doWork() {  
  6.         System.out.println(this.getClass().getName() + "." + "doWork  Invoke");  
  7.         String sql = "insert into department values(2,'研發部')";  
  8.         try {  
  9.             TransactionHelper.executeNonQuery(sql);  
  10.         } catch (SQLException e) {  
  11.             thrownew RuntimeException(e.getMessage(), e);  
  12.         }  
  13.         //throw new RuntimeException("回滾");
  14.     }  
  15. }  


修改過後的 TransactionHelper
Java程式碼  收藏程式碼
  1. package com.hwadee.demo;  
  2. import java.io.IOException;  
  3. import java.io.InputStream;  
  4. import java.sql.Connection;  
  5. import java.sql.DriverManager;  
  6. import java.sql.SQLException;  
  7. import java.util.Properties;  
  8. publicfinalclass TransactionHelper {  
  9.     // 使用ThreadLocal持有當前執行緒的資料庫連線
  10.     privatefinalstatic ThreadLocal<Connection> connection_holder = new ThreadLocal<Connection>();  
  11.     // 當前是否處於事務環境
  12.     privatefinalstatic ThreadLocal<Boolean> existsTransaction = new ThreadLocal<Boolean>() {  
  13.         @Override
  14.         protected Boolean initialValue() {  
  15.             return Boolean.FALSE;  
  16.         }  
  17.     };  
  18.     // 是否必須回滾
  19.     privatefinalstatic ThreadLocal<Boolean> rollbackOnly = new ThreadLocal<Boolean>() {  
  20.         @Override
  21.         protected Boolean initialValue() {  
  22.             return Boolean.FALSE;  
  23.         }  
  24.     };  
  25.     // 連線配置,來自connection.properties
  26.     privatefinalstatic Properties connectionProp = new Properties();  
  27.     static {  
  28.         // 載入配置檔案
  29.         InputStream is = Thread.currentThread().getContextClassLoader()  
  30.                 .getResourceAsStream("connection.properties");  
  31.         try {  
  32.             connectionProp.load(is);  
  33.             is.close();  
  34.             // 載入驅動程式
  35.             Class.forName(connectionProp.getProperty("driverClassName"));  
  36.         } catch (IOException e) {  
  37.             thrownew RuntimeException(e.getMessage(), e);  
  38.         } catch (ClassNotFoundException e) {  
  39.             thrownew RuntimeException("驅動未找到", e);  
  40.         }  
  41.     }  
  42.     /** 
  43.      * 是否必須回滾 
  44.      */
  45.     publicstaticboolean isRollbackOnly() {  
  46.         return rollbackOnly.get();  
  47.     }  
  48.     /** 
  49.      * 設定當前事務環境的回滾狀態 
  50.      */
  51.     publicstaticvoid setRollbackOnly(boolean flag) {  
  52.         rollbackOnly.set(flag);  
  53.     }  
  54.     /** 
  55.      * 當前是否存在事務 
  56.      */
  57.     publicstaticboolean existsTransaction() {  
  58.         return existsTransaction.get();  
  59.     }  
  60.     // 設定當前事務環境
  61.     privatestaticvoid setExistsTransaction(boolean flag) {  
  62.         existsTransaction.set(flag);  
  63.     }  
  64.     /** 
  65.      * 開始一個事務 
  66.      */
  67.     publicstaticvoid beginTransaction() {  
  68.         Connection conn = createNotAutoCommitConnection();  
  69.         connection_holder.set(conn);  
  70.         setExistsTransaction(Boolean.TRUE);  
  71.     }  
  72.     // 獲取當前執行緒中的資料庫連線
  73.     privatestatic Connection getCurrentConnection() {  
  74.         return connection_holder.get();  
  75.     }  
  76.     // 執行SQL語句
  77.     publicstaticint executeNonQuery(String sql) throws SQLException {  
  78.         Connection conn = getCurrentConnection();  
  79.         return conn.createStatement().executeUpdate(sql);  
  80.     }  
  81.     /** 
  82.      * 提交事務 
  83.      */
  84.     publicstaticvoid commit() {  
  85.         Connection conn = getCurrentConnection();  
  86.         try {  
  87.             conn.commit();  
  88.             conn.close();  
  89.             connection_holder.set(null);  
  90.             setExistsTransaction(Boolean.FALSE);  
  91.         } catch (SQLException e) {  
  92.             thrownew RuntimeException(e.getMessage(), e);  
  93.         }  
  94.     }  
  95.     /** 
  96.      * 回滾事務 
  97.      */
  98.     publicstaticvoid rollback() {  
  99.         Connection conn = getCurrentConnection();  
  100.         try {  
  101.             conn.rollback();  
  102.             conn.close();  
  103.             connection_holder.set(null);  
  104.             setExistsTransaction(Boolean.FALSE);  
  105.         } catch (SQLException e) {  
  106.             thrownew RuntimeException(e.getMessage(), e);  
  107.         }  
  108.     }  
  109.     // 建立一個不自動Commit的資料庫連線
  110.     privatestatic Connection createNotAutoCommitConnection() {  
  111.         try {  
  112.             Connection conn = DriverManager.getConnection(connectionProp  
  113.                     .getProperty("url")  
  114.                     + ";databaseName="
  115.                     + connectionProp.getProperty("databaseName"),  
  116.                     connectionProp.getProperty("username"), connectionProp  
  117.                             .getProperty("password"));  
  118.             conn.setAutoCommit(false);  
  119.             return conn;  
  120.         } catch (SQLException e) {  
  121.             thrownew RuntimeException(e.getMessage(), e);  
  122.         }  
  123.     }  
  124. }  


RequiredTransactionInterceptor 事務攔截器
Java程式碼  收藏程式碼
  1. package com.hwadee.demo.aop;  
  2. import java.lang.reflect.InvocationHandler;  
  3. import java.lang.reflect.InvocationTargetException;  
  4. import java.lang.reflect.Method;  
  5. import com.hwadee.demo.TransactionHelper;  
  6. /** 
  7.  * 事務攔截器 代理物件執行介面的任意方法都會被攔截 對方法的呼叫交由本類的 invoke 方法處理 
  8.  */
  9. publicclass RequiredTransactionInterceptor implements InvocationHandler {  
  10.     // 目標物件
  11.     private Object target;  
  12.     // 在構造方法中傳入目標物件
  13.     public RequiredTransactionInterceptor(Object target) {  
  14.         this.target = target;  
  15.     }  
  16.     /** 
  17.      * 在代理物件呼叫介面方法時的請求會被此方法攔截 
  18.      *  
  19.      * @param proxy 
  20.      *            代理物件 
  21.      * @param method 
  22.      *            目標物件當前呼叫的方法 
  23.      * @param args 
  24.      *            呼叫此方法時傳遞的引數 
  25.      */
  26.     public Object invoke(Object proxy, Method method, Object[] args)  
  27.             throws Throwable {  
  28.         // 在目標方法被呼叫前織入的邏輯,此處以Required傳播屬性為例
  29.         // 判斷當前的事務環境,是開始一個新事務還是加入已有的事務
  30.         boolean existsTransaction = TransactionHelper.existsTransaction();  
  31.         if (existsTransaction == false) {  
  32.             TransactionHelper.beginTransaction();  
  33.             System.out.println("當前事務環境還沒有事務,開啟一個新事務");  
  34.         } else {  
  35.             System.out.println("當前事務環境已存在事務,加入事務");  
  36.         }  
  37.         // 目標方法的返回值
  38.         Object result = null;  
  39.         // 此處才真正呼叫目標物件的方法
  40.         try {  
  41.             result = method.invoke(target, args);  
  42.         } catch (InvocationTargetException e) {  
  43.             // 捕獲呼叫目標異常,如果目標異常是執行時異常則設定回滾標誌
  44.             Throwable cause = e.getCause();  
  45.             if (cause instanceof RuntimeException) {  
  46.                 TransactionHelper.setRollbackOnly(true);  
  47.                 System.out.println("出現執行時異常,事務環境被設定為必須回滾");  
  48.             } else {  
  49.                 System.out.println("出現非執行時異常,忽略");  
  50.             }  
  51.         }  
  52.         // 在目標方法被呼叫後織入的邏輯
  53.         System.out.println("判斷當前的事務環境,是應該提交事務還是回滾事務");  
  54.         if (existsTransaction == false
  55.                 && TransactionHelper.isRollbackOnly() == false) {  
  56.             TransactionHelper.commit();  
  57.             System.out.println("事務已提交");  
  58.         } elseif (existsTransaction == false
  59.                 && TransactionHelper.isRollbackOnly() == true) {  
  60.             TransactionHelper.rollback();  
  61.             System.out.println("事務已回滾");  
  62.         } elseif (existsTransaction == true) {  
  63.             System.out.println("子事務忽略事務提交或回滾");  
  64.         }  
  65.         System.out.println("=============================");  
  66.         return result;  
  67.     }  
  68. }  


BeanFactory  Bean工廠,負責建立代理物件
Java程式碼  收藏程式碼
  1. package com.hwadee.demo.aop;  
  2. import java.lang.reflect.Proxy;  
  3. import java.util.HashMap;  
  4. import java.util.Map;  
  5. /** 
  6.  * 模擬Spring BeanFactory 最簡化的實現 
  7.  *  
  8.  * 預設會建立兩個Bean dao1 dao2 
  9.  *  
  10.  * 它們都是經過了代理過後的物件 
  11.  */
  12. publicclass BeanFactory {  
  13.     // Bean容器
  14.     privatefinalstatic Map<String, Object> beanContainer = new HashMap<String, Object>();  
  15.     // 初始化建立兩個代理物件
  16.     static {  
  17.         DAO dao1 = new DAOImpl1();  
  18.         Object dao1Proxy = createTransactionProxy(dao1);  
  19.         beanContainer.put("dao1", dao1Proxy);  
  20.         DAO dao2 = new DAOImpl2();  
  21.         Object dao2Proxy = createTransactionProxy(dao2);  
  22.         beanContainer.put("dao2", dao2Proxy);  
  23.     }  
  24.     // 建立代理物件
  25.     privatestatic Object createTransactionProxy(Object target) {  
  26.         // 使用 Proxy.newProxyInstance 方法建立一個代理物件
  27.         Object proxy = Proxy.newProxyInstance(target.getClass()  
  28.                 .getClassLoader(), target.getClass().getInterfaces(),  
  29.                 new RequiredTransactionInterceptor(target));  
  30.         return proxy;  
  31.     }  
  32.     // 獲取Bean
  33.     publicstatic Object getBean(String id) {  
  34.         return beanContainer.get(id);  
  35.     }  
  36. }  


MainModule  程式入口
Java程式碼  收藏程式碼
  1. package com.hwadee.demo.aop;  
  2. publicclass MainModule {  
  3.     publicstaticvoid main(String[] args) {  
  4.         DAO dao1 = (DAO) BeanFactory.getBean("dao1");  
  5.         // 呼叫dao1,doa1的doWork方法內又呼叫了dao2
  6.         dao1.doWork();        
  7.     }  
  8. }  


   好了,現在我們可以執行一下這個應用,記得先把資料庫裡已有的記錄清空。
看看控制檯輸出的內容
Xml程式碼  收藏程式碼
  1. 當前事務環境還沒有事務,開啟一個新事務  
  2. com.hwadee.demo.aop.DAOImpl1.doWork  Invoke  
  3. 當前事務環境已存在事務,加入事務  
  4. com.hwadee.demo.aop.DAOImpl2.doWork  Invoke  
  5. 判斷當前的事務環境,是應該提交事務還是回滾事務  
  6. 子事務忽略事務提交或回滾  
  7. =============================  
  8. 判斷當前的事務環境,是應該提交事務還是回滾事務  
  9. 事務已提交  
  10. =============================  


接下來,把DAOImpl2中這段程式碼的註釋取消掉,再次執行此應用,記得先清空資料庫資料
Java程式碼  收藏程式碼
  1. //throw new RuntimeException("回滾");


再來看控制檯輸出的內容
Xml程式碼  收藏程式碼
  1. 當前事務環境還沒有事務,開啟一個新事務  
  2. com.hwadee.demo.aop.DAOImpl1.doWork  Invoke  
  3. 當前事務環境已存在事務,加入事務  
  4. com.hwadee.demo.aop.DAOImpl2.doWork  Invoke  
  5. 出現執行時異常,事務環境被設定為必須回滾  
  6. 判斷當前的事務環境,是應該提交事務還是回滾事務  
  7. 子事務忽略事務提交或回滾  
  8. =============================  
  9. 判斷當前的事務環境,是應該提交事務還是回滾事務  
  10. 事務已回滾  
  11. =============================  


  朋友們,這不就是Required事務傳播模型嗎,離Spring的宣告式事務已經不遠了,附上最新的程式碼。歡迎拍磚!待續!