在用戶修改了領(lǐng)域?qū)ο蟮闹岛螅覀冇袝r需要記錄下用戶的改動。比如對一些關(guān)鍵業(yè)務(wù)對象的改動有時往往需要發(fā)郵件通知客戶。有時用戶可能想查閱所有歷史的改動,甚至有可能會改回原先的值。
領(lǐng)域邏輯關(guān)系往往比較復(fù)雜,這時我們會使用到ORM Framework。本文以toplink為例,講述如何利用toplink編寫一個完成此功能的簡易Framework,我們暫且把它稱為ActionMemed。
我們先來看一下大體的流程:
l 我們獲得用戶修改信息通常有兩種方式,一種是被動的監(jiān)聽,另一種主動的通知。被動的監(jiān)聽就是framework訂閱所關(guān)心領(lǐng)域?qū)ο蟮男薷?,主動通知?/span>application主動的將修改之前和之后的對象通知framework。
l Framework接著從整個對象的樹結(jié)構(gòu)中找出用戶所關(guān)心的某個特定的字段或者字段的組合,生成actionRecord。ActionRecord是描述用戶對領(lǐng)域?qū)ο笮薷牡臄?shù)據(jù)結(jié)構(gòu),會包括用戶修改的原因,修改者,修改的時間,修改的字段或者組合,修改前后的值等等信息。
l 在ActionRecord生成好之后,會將它記錄到DB,發(fā)郵件通知用戶或者通過JMS通知其他Application。
有了基本的概念后,看一下整體的結(jié)構(gòu):

Registry: 在TopLink上注冊ActionListener。一旦在TopLink上檢測到業(yè)務(wù)對象的改動就會調(diào)用ActionService,生成ActionRecord并調(diào)用相關(guān)的ActionRecorder。
ActionListener:TopLink的SessionListener,每次會話都會調(diào)用。我們在這里實現(xiàn)了preCommit方法,在UnitOfWork提交之前,捕捉用戶的所有修改,并從中選取出用戶所關(guān)心的對象的變動。
ActionService:當(dāng)ActionListener從TopLink中獲得到改動的對象,就會調(diào)用ActionService生成ActionRecord,并通知相關(guān)的Recorder,可能是Log到DB。如果用戶是通過主動的方式傳入新老兩個對象就不需要Listener,直接調(diào)用ActionService,將新老對象或者新對象和ValueDistiller作為參數(shù)傳入,
ValueDistiler:根據(jù)當(dāng)前的新對象,萃取出老對象。TopLink就可以根據(jù)當(dāng)前UnitOfWork中的新對象獲取原始對象。方法是:
public Object getOriginalVersionOfObject(Object workingClone)
|
Expression:ActionMemed相關(guān)的配置數(shù)據(jù),由ExpressionParser解析出來后就會cache在內(nèi)存中。這個配置可以是文件,或者DB配置。只要能描述清楚就行。文件配置我們直接利用spring bean。
ActionConstructor:Listener從TopLink ChangeSet中拿到的只是有改動的對象。而我們關(guān)心的只是對象上某個Field或者它引用的某個對象的Field,比如說Employee有PhoneNumber List,PhoneNumber有個屬性是areaCode,可能我們只關(guān)心areaCode值的更改,就只需要記錄areaCode的更改,并且通知客戶。所以我們需要根據(jù)用戶配置對新老對象進(jìn)行對比,比較是否有關(guān)注的屬性被用戶更改了。并且構(gòu)建ActionRecord。比較的方法我們可以用JXpath, Xpath的表達(dá)能力很強(qiáng),而且還可以自定義函數(shù),在自定義擴(kuò)展函數(shù)里用戶可以對字段進(jìn)行組合處理,從而生成它們自己想要記錄的值。
ActionRecorder:當(dāng)Action構(gòu)建完成后,ActionRecorder就要將它通知客戶,用JMS發(fā)給其他項目或者記錄到DB。用戶可以配置多個ActionRecorder。
MemedEventListener,讓用戶在ActionRecorder調(diào)用之前和之后做一些額外的處理。比如說用戶可能在之前對Action的數(shù)據(jù)結(jié)構(gòu)加入一些定制信息。
上面介紹了ActionMemed的流程和相關(guān)模塊的功能。其實在使用中,特別是一次修改很多業(yè)務(wù)對象的時候,處理Action時間會有點長,況且Action的處理也并不需要實時。所以Action還需要提供異步處理的功能。
將異步調(diào)用的模塊圖和先前的結(jié)構(gòu)圖進(jìn)行比較會發(fā)現(xiàn)有兩處不同:
ServiceTask: 實現(xiàn)Java Runnable接口,基本實現(xiàn)類似于先前圖中的ActionService。
ObjectCloner: 如果我們使用TopLink,在異步的情況下,用戶當(dāng)前的UnitOfWork(事務(wù))會先提交,提交之后,從TopLink中萃取的舊對象會被Merge成新對象,這時我們只能提前在UnitOfWork提交之前自己根據(jù)Expression的結(jié)構(gòu)深Copy一份出來。
ActionAsyncService: 為異步設(shè)計的ActionService,利用ValueDistiller從UnitOfwork獲得當(dāng)前對象的原始clone,構(gòu)建ServiceTask,將ServiceTask提交到ThreadPool,當(dāng)task被執(zhí)行時,就會調(diào)用ActionService,這時的ActionService重用了同步流程中的ActonService。
幾個注意點:
整個Framework的原理還是相當(dāng)簡單的,稍微值得注意的可能是下面幾個方面。
Listener如何獲取被改動的對象
TopLink會把所有改動過的對象都會被放在UnitOfWorkChangeSet中,因為在UnitOfWork提交的時候它需要將UnitOfWorkChangeSet中記下的改動提交到數(shù)據(jù)庫。然后merge到session cache。所以所有改動從UnitOfWork中都是可以拿到的。
public class ActionLogPassiveAsyncListener extends AbstractActionLogAsyncListener {
private ValueDistiller distiller;
public void preCommitUnitOfWork(SessionEvent sessionevent) {
log.debug("preCommitUnitOfWork begin.");
UnitOfWork unitOfWork = (UnitOfWork) sessionevent.getSession();
if (null == unitOfWork.getUnitOfWorkChangeSet()) {
unitOfWork.setUnitOfWorkChangeSet((oracle.toplink.internal.sessions.UnitOfWorkChangeSet) unitOfWork
.getCurrentChanges());
}
UnitOfWorkChangeSet ucs = unitOfWork.getUnitOfWorkChangeSet();
if (ucs != null && ucs.getAllChangeSets() != null) {
Set finishedObjects = new HashSet();
Map<Class<?>, List<ChangedPair>> changedPairs = new HashMap<Class<?>, List<ChangedPair>>();
for (Enumeration objectChangeSetEnum = ucs.getAllChangeSets().keys(); objectChangeSetEnum
.hasMoreElements();) {
ObjectChangeSet objectChangeSet = (ObjectChangeSet) objectChangeSetEnum.nextElement();
if (objectChangeSet == null) {
continue;
}
Object clone = objectChangeSet.getUnitOfWorkClone();
if (!finishedObjects.contains(clone)) {
for (Class focusClass : this.focusClasses) {
if ((includeSubclass ? focusClass.isAssignableFrom(clone.getClass())
: clone.getClass() == focusClass)
&& (filter == null || (filter != null && !filter.isFiltered(clone, unitOfWork)))) {
finishedObjects.add(clone);
if (objectChangeSet.hasChanges()) {
List<ChangedPair> changedPairList = changedPairs.get(focusClass);
if (null == changedPairList) {
changedPairList = new ArrayList<ChangedPair>();
changedPairs.put(focusClass, changedPairList);
}
Object originalObject = this.distiller.getOriginObject(clone);
changedPairList.add(new ChangedPair(originalObject, clone));
}
}
}
}
}
if (!changedPairs.isEmpty()) {
try {
ChangedPairMap changedPairMap = this.assembleChangedPairMap(unitOfWork, changedPairs);
this.changedPairCache.set(changedPairMap);
} catch (ActionLogException e) {
if (shouldBreakIfException) {
throw new ActionLogRuntimeException(e);
}
}
}
log.debug("preCommitUnitOfWork end.");
}
}
}
|
異步狀態(tài)下,ActionRecord要在Application事務(wù)提交之后生成
同步狀態(tài)下,ActionRecord的生成可以Join Application的transaction,這樣他們會一起成功或者失敗。但是異步情況下,就會是不同的事務(wù),兩個事務(wù)之間的關(guān)系可能是有先后順序或者互不相干。互不相干是不可能的,從業(yè)務(wù)意義上講只有Application的改動確實生效之后ActionRecord才能生成,但是將ActionRecord放在Application事務(wù)提交成功之后生成或者提交,也會面臨一個問題,就是application成功提交了,但ActionRecord的生成可能會失敗。但要知道ActionRecord失敗的幾率遠(yuǎn)比Application提交失敗的幾率要小得多,application常常會因為樂觀鎖的問題而提交失敗,但ActionRecord只可能因為DB Shutdown而丟失數(shù)據(jù)。失敗后會做詳細(xì)的備份,以便做恢復(fù)。那如何感知application事務(wù)是提交成功還是失敗了呢?TopLink的SessionEventListener有四個有用的回調(diào)方法:PreCommit,PostCommit,PostRollback,PostRelease,用戶事務(wù)提交的時候在提交之前會調(diào)用PreCommit方法,這時我們還可以從UnitOfWork中獲取新老對象,我們會把老對象深clone一份出來,將他們存放在ThreadLocal中,而在PostCommit回調(diào)的實現(xiàn)中,我們會從ThreadLocal中取出新老對象完成ActionRecord的生成,而PostRollback就可以什么都不干了。但不管是提交成功還是提交失敗Rlease方法都會被調(diào)用,UnitOfWork需要release,這里我們就會去清空ThreadLocal,以便內(nèi)存即時的垃圾回收。這樣說來即使是主動調(diào)用ActionAsyncService也會注冊一個Listener,不同的是這個Listener不需要從UnitOfWork檢測變化。
public class AbstractActionAsyncListener extends AbstractActionListener {
protected ThreadLocal<ChangedPairMap> changedPairCache = new ThreadLocal<ChangedPairMap>();
public void postCommitUnitOfWork(SessionEvent arg0) {
ChangedPairMap changedPairMap = this.changedPairCache.get();
if (changedPairMap != null) {
if (!changedPairMap.isEmpty()) {
//異步生成ActionRecord
}
}
}
public void postReleaseUnitOfWork(SessionEvent arg0) {
if (this.changedPairCache.get() != null) {
this.clearResource();
}
}
private void clearResource() {
this.changedPairCache.set(null);
}
}
|
public class ActionActiveAsyncListener extends AbstractActionAsyncListener {
private Map<Class<?>, List<ChangedPair>> changedPairs;
public void preCommitUnitOfWork(SessionEvent sessionEvent) {
UnitOfWork unitOfWork = (UnitOfWork) sessionEvent.getSession();
try {
ChangedPairMap changedPairMap = this.assembleChangedPairMap(unitOfWork, this.changedPairs);
this.changedPairCache.set(changedPairMap);
} catch (ActionLogException e) {
log.error("Assemble ChangePairMap fails! ChangedPairs: " + changedPairs.toString(), e);
if (shouldBreakIfException) {
throw new ActionRuntimeException(e);
}
}
}
}
|
ObjectCloner:
如果對象樹的結(jié)構(gòu)很龐大,深copy的性能代價不得不考慮。BeanUtils進(jìn)行深copy的性能很差。5000個對象花了我20s。首先要說的是其實不需要所有對象引用都需要深copy,只有那些用戶對關(guān)注的對象屬性才需要深copy,clone的步驟大概如下:
l 對根對象進(jìn)行淺copy
l 對用戶關(guān)心的對象屬性迭代的進(jìn)行深copy
l 如果關(guān)心的對象屬性是Collection,淺copy Collection中的每個對象并深copy對象中用戶關(guān)注的對象屬性
l 其實那些domain class,早在做ORM的時就確定下來了,所以所有domain對象反射metadata都可以事先確定,存在內(nèi)存中,這樣會大大提高性能,其實toplink也會把這些反射結(jié)構(gòu)解析出來后緩存在內(nèi)存中,直接利用toplink的clone邏輯就可以了。1000個對象深clone一把大約是120ms。