走自己的路

          路漫漫其修遠兮,吾將上下而求索

            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            50 隨筆 :: 4 文章 :: 118 評論 :: 0 Trackbacks
           

          在用戶修改了領(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)心的某個特定的字段或者字段的組合,生成actionRecordActionRecord是描述用戶對領(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

          ActionListenerTopLinkSessionListener,每次會話都會調(diào)用。我們在這里實現(xiàn)了preCommit方法,在UnitOfWork提交之前,捕捉用戶的所有修改,并從中選取出用戶所關(guān)心的對象的變動。

          ActionService:ActionListenerTopLink中獲得到改動的對象,就會調(diào)用ActionService生成ActionRecord,并通知相關(guān)的Recorder,可能是LogDB。如果用戶是通過主動的方式傳入新老兩個對象就不需要Listener,直接調(diào)用ActionService,將新老對象或者新對象和ValueDistiller作為參數(shù)傳入,

          ValueDistiler:根據(jù)當前的新對象,萃取出老對象。TopLink就可以根據(jù)當前UnitOfWork中的新對象獲取原始對象。方法是:

          public Object getOriginalVersionOfObject(Object workingClone)

           

          ExpressionActionMemed相關(guān)的配置數(shù)據(jù),由ExpressionParser解析出來后就會cache在內(nèi)存中。這個配置可以是文件,或者DB配置。只要能描述清楚就行。文件配置我們直接利用spring bean

          ActionConstructor:ListenerTopLink ChangeSet中拿到的只是有改動的對象。而我們關(guān)心的只是對象上某個Field或者它引用的某個對象的Field,比如說EmployeePhoneNumber ListPhoneNumber有個屬性是areaCode,可能我們只關(guān)心areaCode值的更改,就只需要記錄areaCode的更改,并且通知客戶。所以我們需要根據(jù)用戶配置對新老對象進行對比,比較是否有關(guān)注的屬性被用戶更改了。并且構(gòu)建ActionRecord。比較的方法我們可以用JXpath Xpath的表達能力很強,而且還可以自定義函數(shù),在自定義擴展函數(shù)里用戶可以對字段進行組合處理,從而生成它們自己想要記錄的值。

          ActionRecorder:當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)圖進行比較會發(fā)現(xiàn)有兩處不同:

          ServiceTask: 實現(xiàn)Java Runnable接口,基本實現(xiàn)類似于先前圖中的ActionService

           

          ObjectCloner: 如果我們使用TopLink,在異步的情況下,用戶當前的UnitOfWork(事務(wù))會先提交,提交之后,從TopLink中萃取的舊對象會被Merge成新對象,這時我們只能提前在UnitOfWork提交之前自己根據(jù)Expression的結(jié)構(gòu)深Copy一份出來。

           

          ActionAsyncService: 為異步設(shè)計的ActionService,利用ValueDistillerUnitOfwork獲得當前對象的原始clone,構(gòu)建ServiceTask,將ServiceTask提交到ThreadPool,當task被執(zhí)行時,就會調(diào)用ActionService,這時的ActionService重用了同步流程中的ActonService

           

          幾個注意點:

          整個Framework的原理還是相當簡單的,稍微值得注意的可能是下面幾個方面。

          Listener如何獲取被改動的對象

          TopLink會把所有改動過的對象都會被放在UnitOfWorkChangeSet中,因為在UnitOfWork提交的時候它需要將UnitOfWorkChangeSet中記下的改動提交到數(shù)據(jù)庫。然后mergesession 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 Applicationtransaction,這樣他們會一起成功或者失敗。但是異步情況下,就會是不同的事務(wù),兩個事務(wù)之間的關(guān)系可能是有先后順序或者互不相干。互不相干是不可能的,從業(yè)務(wù)意義上講只有Application的改動確實生效之后ActionRecord才能生成,但是將ActionRecord放在Application事務(wù)提交成功之后生成或者提交,也會面臨一個問題,就是application成功提交了,但ActionRecord的生成可能會失敗。但要知道ActionRecord失敗的幾率遠比Application提交失敗的幾率要小得多,application常常會因為樂觀鎖的問題而提交失敗,但ActionRecord只可能因為DB Shutdown而丟失數(shù)據(jù)。失敗后會做詳細的備份,以便做恢復(fù)。那如何感知application事務(wù)是提交成功還是失敗了呢?TopLinkSessionEventListener有四個有用的回調(diào)方法:PreCommitPostCommitPostRollbackPostRelease,用戶事務(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進行深copy的性能很差。5000個對象花了我20s。首先要說的是其實不需要所有對象引用都需要深copy,只有那些用戶對關(guān)注的對象屬性才需要深copyclone的步驟大概如下:

          l           對根對象進行淺copy

          l           對用戶關(guā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)存中,直接利用toplinkclone邏輯就可以了。1000個對象深clone一把大約是120ms




          評論

          # re: 偵聽和處理用戶對業(yè)務(wù)對象改動的簡易框架 2009-12-01 12:13 創(chuàng)意產(chǎn)品
          非常好的文章,謝謝樓主分享!  回復(fù)  更多評論
            

          # re: 偵聽和處理用戶對業(yè)務(wù)對象改動的簡易框架 2010-03-01 10:47 游客
          很好。公司的team也開發(fā)了記錄ActionLog的組件。不過在獲取改變集和比較對象上代價比較高。正好參考  回復(fù)  更多評論
            

          主站蜘蛛池模板: 墨竹工卡县| 什邡市| 万山特区| 衡水市| 永善县| 晋江市| 浦北县| 宣汉县| 渭南市| 突泉县| 临西县| 固安县| 普陀区| 抚远县| 永年县| 巴彦淖尔市| 旬邑县| 延津县| 新沂市| 汽车| 沂水县| 南陵县| 工布江达县| 高唐县| 蓝田县| 柏乡县| 邹平县| 田阳县| 比如县| 丽水市| 黄平县| 开鲁县| 上蔡县| 洞口县| 荆门市| 泗水县| 泰宁县| 澳门| 公安县| 扶风县| 鄂托克旗|