Posted on 2007-12-20 11:42
帥子 閱讀(602)
評論(1) 編輯 收藏 所屬分類:
J2EE技術(shù)專區(qū)
事務(wù)處理是企業(yè)應(yīng)用需要解決的最主要的問題之一。J2EE通過JTA提供了完整的事務(wù)管理能力,包括多個事務(wù)性資源的管理能力。但是大部分應(yīng)用都是運行在單一的事務(wù)性資源之上(一個數(shù)據(jù)庫),他們并不需要全局性的事務(wù)服務(wù)。本地事務(wù)服務(wù)已然足夠(比如JDBC事務(wù)管理)。
本文并不討論應(yīng)該采用何種事務(wù)處理方式,主要目的是討論如何更為優(yōu)雅地設(shè)計事務(wù)服務(wù)。僅以JDBC事務(wù)處理為例。涉及到的DAO,F(xiàn)actory,Proxy,Decorator等模式概念,請閱讀相關(guān)資料。
也許你聽說過,事務(wù)處理應(yīng)該做在service層,也許你也正這樣做,但是否知道為什么這樣做?為什么不放在DAO層做事務(wù)處理。顯而易見的原因是業(yè)務(wù)層接口的每一個方法有時候都是一個業(yè)務(wù)用例(User Case),它需要調(diào)用不同的DAO對象來完成一個業(yè)務(wù)方法。比如簡單地以網(wǎng)上書店購書最后的確定定單為例,業(yè)務(wù)方法首先是調(diào)用BookDAO對象(一般是通過DAO工廠產(chǎn)生),BookDAO判斷是否還有庫存余量,取得該書的價格信息等,然后調(diào)用CustomerDAO從帳戶扣除相應(yīng)的費用以及記錄信息,然后是其他服務(wù)(通知管理員等)。簡化業(yè)務(wù)流程大概如此:
注意,我們的例子忽略了連接的處理,只要保證同一個線程內(nèi)取的是相同的連接即可(可用ThreadLocal實現(xiàn)):
首先是業(yè)務(wù)接口,針對接口,而不是針對類編程:
public interface BookStoreManager{
public boolean buyBook(String bookId,int quantity)throws SystemException;
....其他業(yè)務(wù)方法
}
接下來就是業(yè)務(wù)接口的實現(xiàn)類??業(yè)務(wù)對象:
public class BookStoreManagerImpl implements BookStoreManager{
public boolean buyBook(String bookId)throws SystemException{
Connection conn=ConnectionManager.getConnection();//獲取數(shù)據(jù)庫連接
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));
....
其他業(yè)務(wù)方法,如通知管理員,生成定單等.
...
conn.commit(); //提交事務(wù)
conn.setAutoCommit(true);
}
}catch(SQLException e){
conn.rollback(); //出現(xiàn)異常,回滾事務(wù)
con.setAutoCommit(true);
e.printStackTrace();
throws new SystemException(e);
}
return b;
}
}
然后是業(yè)務(wù)代表工廠:
public final class ManagerFactory {
public static BookStoreManager getBookStoreManager() {
return new BookStoreManagerImpl();
}
}
這樣的設(shè)計非常適合于DAO中的簡單活動,我們項目中的一個小系統(tǒng)也是采用這樣的設(shè)計方案,但是它不適合于更大規(guī)模的應(yīng)用。首先,你有沒有聞到代碼重復(fù)的 bad smell?每次都要設(shè)置AutoCommit為false,然后提交,出現(xiàn)異常回滾,包裝異常拋到上層,寫多了不煩才怪,那能不能消除呢?其次,業(yè)務(wù)代表對象現(xiàn)在知道它內(nèi)部事務(wù)管理的所有的細節(jié),這與我們設(shè)計業(yè)務(wù)代表對象的初衷不符。對于業(yè)務(wù)代表對象來說,了解一個與事務(wù)有關(guān)的業(yè)務(wù)約束是相當恰當?shù)模亲屗撠?zé)來實現(xiàn)它們就不太恰當了。再次,你是否想過嵌套業(yè)務(wù)對象的場景?業(yè)務(wù)代表對象之間的互相調(diào)用,層層嵌套,此時你又如何處理呢?你要知道按我們現(xiàn)在的方式,每個業(yè)務(wù)方法都處于各自獨立的事務(wù)上下文當中(Transaction Context),互相調(diào)用形成了嵌套事務(wù),此時你又該如何處理?也許辦法就是重新寫一遍,把不同的業(yè)務(wù)方法集中成一個巨無霸包裝在一個事務(wù)上下文中。
我們有更為優(yōu)雅的設(shè)計來解決這類問題,如果我們把Transaction Context的控制交給一個被業(yè)務(wù)代表對象、DAO和其他Component所共知的外部對象。當業(yè)務(wù)代表對象的某個方法需要事務(wù)管理時,它提示此外部對象它希望開始一個事務(wù),外部對象獲取一個連接并且開始數(shù)據(jù)庫事務(wù)。也就是將事務(wù)控制從service層抽離,當web層調(diào)用service層的某個業(yè)務(wù)代表對象時,返回的是一個經(jīng)過Transaction Context外部對象包裝(或者說代理)的業(yè)務(wù)對象。此代理對象將請求發(fā)送給原始業(yè)務(wù)代表對象,但是對其中的業(yè)務(wù)方法進行事務(wù)控制。那么,我們?nèi)绾螌崿F(xiàn)此效果呢?答案是JDK1.3引進的動態(tài)代理技術(shù)。動態(tài)代理技術(shù)只能代理接口,這也是為什么我們需要業(yè)務(wù)接口BookStoreManager的原因。
首先,我們引入這個Transaction Context外部對象,它的代碼其實很簡單,如果不了解動態(tài)代理技術(shù)的請先閱讀其他資料。
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 {
/**
* 裝飾原始的業(yè)務(wù)代表對象,返回一個與業(yè)務(wù)代表對象有相同接口的代理對象
*/
public static Object decorate(Object delegate) {
return Proxy.newProxyInstance(delegate.getClass().getClassLoader(),
delegate.getClass().getInterfaces(), new XAWrapperHandler(
delegate));
}
//動態(tài)代理技術(shù)
static final class XAWrapperHandler implements InvocationHandler {
private final Object delegate;
XAWrapperHandler(Object delegate) {
this.delegate = delegate;
}
//簡單起見,包裝業(yè)務(wù)代表對象所有的業(yè)務(wù)方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result = null;
Connection con = ConnectionManager.getConnection();
try {
//開始一個事務(wù)
con.setAutoCommit(false);
//調(diào)用原始業(yè)務(wù)對象的業(yè)務(wù)方法
result = method.invoke(delegate, args);
con.commit(); //提交事務(wù)
con.setAutoCommit(true);
} catch (Throwable t) {
//回滾
con.rollback();
con.setAutoCommit(true);
throw new SystemException(t);
}
return result;
}
}
}
正如我們所見,此對象只不過把業(yè)務(wù)對象需要事務(wù)控制的業(yè)務(wù)方法中的事務(wù)控制部分抽取出來而已。請注意,業(yè)務(wù)代表對象內(nèi)部調(diào)用自身的方法將不會開始新的事務(wù),因為這些調(diào)用不會傳給代理對象。如此,我們?nèi)コ舜碇貜?fù)的味道。此時,我們的業(yè)務(wù)代表對象修改成:
public class BookStoreManagerImpl implements BookStoreManager {
public boolean buyBook(String bookId)throws SystemException{
Connection conn=ConnectionManager.getConnection();// 獲取數(shù)據(jù)庫連接
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));
....
其他業(yè)務(wù)方法,如通知管理員,生成定單等.
...
}
}catch(SQLException e){
throws new SystemException(e);
}
return b;
}
....
}
可以看到,此時的業(yè)務(wù)代表對象專注于實現(xiàn)業(yè)務(wù)邏輯,它不再關(guān)心事務(wù)控制細節(jié),把它們?nèi)课薪o了外部對象。業(yè)務(wù)代表工廠也修改一下,讓它返回兩種類型的業(yè)務(wù)代表對象:
public final class ManagerFactory {
//返回一個被包裝的對象,有事務(wù)控制能力
public static BookStoreManager getBookStoreManagerTrans() {
return (BookStoreManager) TransactionWrapper
.decorate(new BookStoreManagerImpl());
}
//原始版本
public static BookStoreManager getBookStoreManager() {
return new BookStoreManagerImpl();
}
......
}
我們在業(yè)務(wù)代表工廠上提供了兩種不同的對象生成方法:一個用于創(chuàng)建被包裝的對象,它會為每次方法調(diào)用創(chuàng)建一個新的事務(wù);另外一個用于創(chuàng)建未被包裝的版本,它用于加入到已有的事務(wù)(比如其他業(yè)務(wù)代表對象的業(yè)務(wù)方法),解決了嵌套業(yè)務(wù)代表對象的問題。
我們的設(shè)計還不夠優(yōu)雅,比如我們默認所有的業(yè)務(wù)代表對象的方法調(diào)用都將被包裝在一個Transaction Context。可事實是很多方法也許并不需要與數(shù)據(jù)庫打交道,如果我們能配置哪些方法需要事務(wù)聲明,哪些不需要事務(wù)管理就更完美了。解決辦法也很簡單,一個XML配置文件來配置這些,調(diào)用時判斷即可。說到這里,了解spring的大概都會意識到這不正是聲明式事務(wù)控制嗎?正是如此,事務(wù)控制就是AOP的一種服務(wù),spring的聲明式事務(wù)管理是通過AOP實現(xiàn)的。AOP的實現(xiàn)方式包括:動態(tài)代理技術(shù),字節(jié)碼生成技術(shù)(如CGLIB庫),java代碼生成(早期EJB采用),修改類裝載器以及源代碼級別的代碼混合織入(aspectj)等。我們這里就是利用了動態(tài)代理技術(shù),只能對接口代理;對類的動態(tài)代理可以使用cglib庫。