事務處理是企業應用需要解決的最主要的問題之一。j2ee通過jta提供了完整的事務管理能力,包括多個事務性資源的管理能力。但是大部分應用都是運作在單一的事務性資源之上(一個資料庫),他們并不需要全局性的事務服務。本地事務服務已然足夠(比如jdbc事務管理)。
本文并不讨論應該采用何種事務處理方式,主要目的是讨論如何更為優雅地設計事務服務。僅以jdbc事務處理為例。涉及到的dao,factory,proxy,decorator等模式概念,請閱讀相關資料。
也許你聽說過,事務處理應該做在service層,也許你也正這樣做,但是否知道為什麼這樣做?為什麼不放在dao層做事務處理。顯而易見的原因是業務層接口的每一個方法有時候都是一個業務用例(user case),它需要調用不同的dao對象來完成一個業務方法。比如簡單地以網上書店購書最後的确定定單為例,業務方法首先是調用bookdao對象(一般是通過dao工廠産生),bookdao判斷是否還有庫存餘量,取得該書的價格資訊等,然後調用customerdao從帳戶扣除相應的費用以及記錄資訊,然後是其他服務(通知管理者等)。簡化業務流程大概如此:
首先是業務接口,針對接口,而不是針對類程式設計:
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();
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 static bookstoremanager getbookstoremanagertrans() {
return (bookstoremanager) transactionwrapper
.decorate(new bookstoremanagerimpl());
//原始版本
我們在業務代表工廠上提供了兩種不同的對象生成方法:一個用于建立被包裝的對象,它會為每次方法調用建立一個新的事務;另外一個用于建立未被包裝的版本,它用于加入到已有的事務(比如其他業務代表對象的業務方法),解決了嵌套業務代表對象的問題。
我們的設計還不夠優雅,比如我們預設所有的業務代表對象的方法調用都将被包裝在一個transaction context。可事實是很多方法也許并不需要與資料庫打交道,如果我們能配置哪些方法需要事務聲明,哪些不需要事務管理就更完美了。解決辦法也很簡單,一個xml配置檔案來配置這些,調用時判斷即可。說到這裡,了解spring的大概都會意識到這不正是聲明式事務控制嗎?正是如此,事務控制就是aop的一種服務,spring的聲明式事務管理是通過aop實作的。aop的實作方式包括:動态代理技術,位元組碼生成技術(如cglib庫),java代碼生成(早期ejb采用),修改類裝載器以及源代碼級别的代碼混合織入(aspectj)等。我們這裡就是利用了動态代理技術,隻能對接口代理;對類的動态代理可以使用cglib庫。
文章轉自莊周夢蝶 ,原文釋出時間2007-02-06