交易處理是公司專屬應用程式需要解決的最主要的問題之一。J2EE通過JTA提供了完整的交易管理能力,包括多個事務性資源的管理能力。但是大部分應用都是運行在單一的事務性資源之上(一個資料庫),他們並不需要全域性的事務服務。本地事務服務已然足夠(比如JDBC交易管理)。
本文並不討論應該採用何種交易處理方式,主要目的是討論如何更為優雅地設計事務服務。僅以JDBC交易處理為例。涉及到的DAO,Factory,Proxy,Decorator等模式概念,請閱讀相關資料。
也許你聽說過,交易處理應該做在service層,也許你也正這樣做,但是否知道為什麼這樣做?為什麼不放在DAO層做交易處理。顯而易見的原因是業務層介面的每一個方法有時候都是一個業務用例(User Case),它需要調用不同的DAO對象來完成一個業務方法。比如簡單地以網上書店購書最後的確定定單為例,業務方法首先是調用BookDAO對象(一般是通過DAO工廠產生),BookDAO判斷是否還有庫存餘量,取得該書的價格資訊等,然後調用CustomerDAO從帳戶扣除相應的費用以及記錄資訊,然後是其他服務(通知管理員等)。簡化商務程序大概如此:
注意,我們的例子忽略了串連的處理,只要保證同一個線程內取的是相同的串連即可(可用ThreadLocal實現):
首先是業務介面,針對介面,而不是針對類編程:
-
- public interface BookStoreManager{
- public boolean buyBook(String bookId,int quantity)throws SystemException;
- ....其他業務方法
- }
接下來就是業務介面的實作類別??業務對象:
-
- public class BookStoreManagerImpl implements BookStoreManager{
- public boolean buyBook(String bookId)throws SystemException{
- Connection conn=ConnectionManager.getConnection();//擷取資料庫連接
- boolean b=false;
-
- try{
- conn.setAutoCommit(false); //取消自動認可
- BookDAO bookDAO=DAOFactory.getBookDAO();
- CustomerDAO customerDAO=DAOFactory.getCustomerDAO();
- //嘗試從庫存中取書
- if(BookDAO.reduceInventory(conn,bookId,quantity)){
- BigDecimal price=BookDAO.getPrice(bookId); //取價格
- //從客戶帳戶中扣除price*quantity的費用
- b=
- CustomerDAO.reduceAccount(conn,price.multiply(new BigDecimal(quantity));
- ....
- 其他業務方法,如通知管理員,產生定單等.
- ...
- conn.commit(); //提交事務
- conn.setAutoCommit(true);
- }
- }catch(SQLException e){
- conn.rollback(); //出現異常,復原事務
- con.setAutoCommit(true);
- e.printStackTrace();
- throws new SystemException(e);
- }
- return b;
- }
- }
-
然後是業務代表工廠:
-
- public final class ManagerFactory {
- public static BookStoreManager getBookStoreManager() {
- return new BookStoreManagerImpl();
- }
- }
這樣的設計非常適合於DAO中的簡單活動,我們項目中的一個小系統也是採用這樣的設計方案,但是它不適合於更大規模的應用。首先,你有沒有聞到代碼重複的 bad smell?每次都要設定AutoCommit為false,然後提交,出現異常復原,封裝異常拋到上層,寫多了不煩才怪,那能不能消除呢?其次,業務代表對象現在知道它內部交易管理的所有的細節,這與我們設計業務代表對象的初衷不符。對於業務代表對象來說,瞭解一個與事務有關的業務約束是相當恰當的,但是讓它負責來實現它們就不太恰當了。再次,你是否想過嵌套業務對象的情境?業務代表對象之間的互相調用,層層嵌套,此時你又如何處理呢?你要知道按我們現在的方式,每個業務方法都處於各自獨立的事務上下文當中(Transaction Context),互相調用形成了嵌套事務,此時你又該如何處理?也許辦法就是重新寫一遍,把不同的業務方法集中成一個巨無霸封裝在一個事務上下文中。
我們有更為優雅的設計來解決這類問題,如果我們把Transaction Context的控制交給一個被業務代表對象、DAO和其他Component所共知的外部對象。當業務代表對象的某個方法需要交易管理時,它提示此外部對象它希望開始一個事務,外部對象擷取一個串連並且開始資料庫事務。也就是將事務控制從service層抽離,當web層調用service層的某個業務代表對象時,返回的是一個經過Transaction Context外部對象封裝(或者說代理)的業務對象。此代理對象將請求發送給原始業務代表對象,但是對其中的業務方法進行事務控制。那麼,我們如何?此效果呢?答案是JDK1.3引進的動態代理技術。動態代理技術只能代理介面,這也是為什麼我們需要業務介面BookStoreManager的原因。
首先,我們引入這個Transaction Context外部對象,它的代碼其實很簡單,如果不瞭解動態代理技術的請先閱讀其他資料。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import com.strutslet.demo.service.SystemException;
public final class TransactionWrapper {
/**
* 裝飾原始的業務代表對象,返回一個與業務代表對象有相同介面的代理對象
*/
public static Object decorate(Object delegate) {
return Proxy.newProxyInstance(delegate.getClass().getClassLoader(),
delegate.getClass().getInterfaces(), new XAWrapperHandler(
delegate));
}
//動態代理技術
static final class XAWrapperHandler implements InvocationHandler {
private final Object delegate;
XAWrapperHandler(Object delegate) {
this.delegate = delegate;
}
//簡單起見,封裝業務代表對象所有的業務方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result = null;
Connection con = ConnectionManager.getConnection();
try {
//開始一個事務
con.setAutoCommit(false);
//調用原始業務對象的業務方法
result = method.invoke(delegate, args);
con.commit(); //提交事務
con.setAutoCommit(true);
} catch (Throwable t) {
//復原
con.rollback();
con.setAutoCommit(true);
throw new SystemException(t);
}
return result;
}
}
} 正如我們所見,此對象只不過把業務對象需要事務控制的業務方法中的事務控制部分抽取出來而已。請注意,業務代表對象內部調用自身的方法將不會開始新的事務,因為這些調用不會傳給代理對象。如此,我們去除了代表重複的味道。此時,我們的業務代表對象修改成:
-
- public class BookStoreManagerImpl implements BookStoreManager {
- public boolean buyBook(String bookId)throws SystemException{
- Connection conn=ConnectionManager.getConnection();// 擷取資料庫連接
- boolean b=false;
- try{
- BookDAO bookDAO=DAOFactory.getBookDAO();
- CustomerDAO customerDAO=DAOFactory.getCustomerDAO();
- // 嘗試從庫存中取書
- if(BookDAO.reduceInventory(conn,bookId,quantity)){
- BigDecimal price=BookDAO.getPrice(bookId); // 取價格
- // 從客戶帳戶中扣除price*quantity的費用
- b=
- CustomerDAO.reduceAccount(conn,price.multiply(new BigDecimal(quantity));
- ....
- 其他業務方法,如通知管理員,產生定單等.
- ...
- }
- }catch(SQLException e){
- throws new SystemException(e);
- }
- return b;
- }
- ....
- }
- 可以看到,此時的業務代表對象專註於實現商務邏輯,它不再關心事務控制細節,把它們全部委託給了外部對象。業務代表工廠也修改一下,讓它返回兩種類型的業務代表對象:
-
- public final class ManagerFactory {
- //返回一個被封裝的對象,有事務控制能力
- public static BookStoreManager getBookStoreManagerTrans() {
- return (BookStoreManager) TransactionWrapper
- .decorate(new BookStoreManagerImpl());
- }
- //原始版本
- public static BookStoreManager getBookStoreManager() {
- return new BookStoreManagerImpl();
- }
- ......
- }
- 我們在業務代表工廠上提供了兩種不同的對象產生方法:一個用於建立被封裝的對象,它會為每次方法調用建立一個新的事務;另外一個用於建立未被封裝的版本,它用於加入到已有的事務(比如其他業務代表對象的業務方法),解決了嵌套業務代表對象的問題。
我們的設計還不夠優雅,比如我們預設所有的業務代表對象的方法調用都將被封裝在一個Transaction Context。可事實是很多方法也許並不需要與資料庫打交道,如果我們能配置哪些方法需要事務聲明,哪些不需要交易管理就更完美了。解決辦法也很簡單,一個XML設定檔來配置這些,調用時判斷即可。說到這裡,瞭解spring的大概都會意識到這不正是聲明式事務控制嗎?正是如此,事務控制就是AOP的一種服務,spring的聲明式交易管理是通過AOP實現的。AOP的實現方式包括:動態代理技術,位元組碼產生技術(如CGLIB庫),java代碼產生(早期EJB採用),修改類裝載器以及原始碼層級的代碼混合織入(aspectj)等。我們這裡就是利用了動態代理技術,只能對介面代理;對類的動態代理可以使用cglib庫。