Java Votary

            BlogJava :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            48 隨筆 :: 1 文章 :: 80 評(píng)論 :: 0 Trackbacks

          AOP@Work: 用 AspectJ 進(jìn)行性能監(jiān)視,第 1 部分

          用 AspectJ 和 JMX 深入觀(guān)察 Glassbox Inspector

          developerWorks
          文檔選項(xiàng)
          將此頁(yè)作為電子郵件發(fā)送

          將此頁(yè)作為電子郵件發(fā)送

          未顯示需要 JavaScript 的文檔選項(xiàng)

          樣例代碼


          對(duì)此頁(yè)的評(píng)價(jià)

          幫助我們改進(jìn)這些內(nèi)容


          級(jí)別: 中級(jí)

          Ron Bodkin , 創(chuàng)始人, New Aspects of Software

          2005 年 10 月 08 日

          隨 著 Ron Bodkin 介紹如何把 AspectJ 和 JMX 組合成靈活而且模塊化的性能監(jiān)視方式,就可以對(duì)散亂而糾纏不清的代碼說(shuō)再見(jiàn)了。在這篇文章(共分兩部分)的第一部分中,Ron 用來(lái)自開(kāi)放源碼項(xiàng)目 Glassbox Inspector 的代碼和想法幫助您構(gòu)建一個(gè)監(jiān)視系統(tǒng),它提供的相關(guān)信息可以識(shí)別出特定問(wèn)題,但是在生產(chǎn)環(huán)境中使用的開(kāi)銷(xiāo)卻足夠低。
          關(guān)于這個(gè)系列

          AOP@Work系列面對(duì)的是在面向方面編程上有些基礎(chǔ),想擴(kuò)展或加深了解的開(kāi)發(fā)人員。同 developerWorks 上的大多數(shù)文章一樣,這個(gè)系列高度實(shí)用:讀完每篇介紹新技術(shù)的文章,都可以立即投入實(shí)用。

          這個(gè)系列的每個(gè)作者在面向方面編程領(lǐng)域都具有領(lǐng)袖地位或?qū)<宜健TS多作者都是系列中介紹的項(xiàng)目和工具的參與者。每篇文章都力圖提供一個(gè)中立的評(píng)述,以確保這里表達(dá)的觀(guān)點(diǎn)是公正且正確的。

          如果有對(duì)每個(gè)作者文章的評(píng)論或問(wèn)題,請(qǐng)分別與他們聯(lián)系。要對(duì)這個(gè)系列整體進(jìn)行評(píng)論,可以與系列的負(fù)責(zé)人 Nicholas Lesiecki 聯(lián)系。請(qǐng)參閱 參考資料 獲取關(guān)于 AOP 的更多背景資料。

          現(xiàn) 代的 Java? 應(yīng)用程序通常是采用許多第三方組件的復(fù)雜的、多線(xiàn)程的、分布式的系統(tǒng)。在這樣的系統(tǒng)上,很難檢測(cè)(或者分離出)性能問(wèn)題或可靠性問(wèn)題的根本原因,尤其是生 產(chǎn)中的問(wèn)題。對(duì)于問(wèn)題容易重現(xiàn)的情況來(lái)說(shuō),profiler 這類(lèi)傳統(tǒng)工具可能有用,但是這類(lèi)工具帶來(lái)的開(kāi)銷(xiāo)造成在生產(chǎn)環(huán)境、甚至負(fù)載測(cè)試環(huán)境中使用它們是不現(xiàn)實(shí)的。

          監(jiān)視和檢查應(yīng)用程序和故障常見(jiàn)的一 個(gè)備選策略是,為性能的關(guān)鍵代碼提供有關(guān)調(diào)用,記錄使用情況、計(jì)時(shí)以及錯(cuò)誤情況。但是,這種方式要求在許多地方分散重復(fù)的代碼,而且要測(cè)量哪些代碼也需要 經(jīng)過(guò)許多試驗(yàn)和錯(cuò)誤才能確定。當(dāng)系統(tǒng)變化時(shí),這種方式既難維護(hù),也很難深入進(jìn)去。這造成日后要求對(duì)性能需求有更好理解的時(shí)候,添加或修改應(yīng)用程序的代碼變 得很困難。簡(jiǎn)單地說(shuō),系統(tǒng)監(jiān)視是經(jīng)典的橫切關(guān)注點(diǎn),因此任何非模塊化的實(shí)現(xiàn)都會(huì)讓它混亂。

          學(xué)習(xí)這篇分兩部分的文章就會(huì)知道,面向方面編程(AOP)很自然地適合解決系統(tǒng)監(jiān)視問(wèn)題。AOP 允許定義切入點(diǎn),與要監(jiān)視性能的許多連接點(diǎn)進(jìn)行匹配。然后可以編寫(xiě)建議,更新性能統(tǒng)計(jì),而在進(jìn)入或退出任何一個(gè)連接點(diǎn)時(shí),都會(huì)自動(dòng)調(diào)用建議。

          在本文的這半部分,我將介紹如何用 AspectJ 和 JMX 創(chuàng)建靈活的、面向方面的監(jiān)視基礎(chǔ)設(shè)施。我要使用的監(jiān)視基礎(chǔ)設(shè)施是開(kāi)放源碼的 Glassbox Inspector 監(jiān)視框架(請(qǐng)參閱 參考資料)的核心。它提供了相關(guān)的信息,可以幫助識(shí)別特定的問(wèn)題,但是在生產(chǎn)環(huán)境中使用的開(kāi)銷(xiāo)卻足夠小。它允許捕捉請(qǐng)求的總數(shù)、總時(shí)間以及最差情況性能之類(lèi)的統(tǒng)計(jì)值,還允許深入請(qǐng)求中數(shù)據(jù)庫(kù)調(diào)用的信息。而它做的所有這些,僅僅是在一個(gè)中等規(guī)模的代碼基礎(chǔ)內(nèi)完成的!

          在這篇文章和下一篇文章中,我將從構(gòu)建一個(gè)簡(jiǎn)單的 Glassbox Inspector 實(shí)現(xiàn)開(kāi)始,并逐漸添加功能。圖 1 提供了這個(gè)遞增開(kāi)發(fā)過(guò)程的最終系統(tǒng)的概貌。請(qǐng)注意這個(gè)系統(tǒng)的設(shè)計(jì)是為了同時(shí)監(jiān)視多個(gè) Web 應(yīng)用程序,并提供合并的統(tǒng)計(jì)結(jié)果。


          圖 1. 帶有 JConsole JMX 客戶(hù)端的 Glassbox Inspector
          Glassbox Inspector 監(jiān)視多個(gè)應(yīng)用程序

          圖 2 是監(jiān)視系統(tǒng)架構(gòu)的概貌。方面與容器內(nèi)的一個(gè)或多個(gè)應(yīng)用程序交互,捕捉性能數(shù)據(jù),然后用 JMX Remote 標(biāo)準(zhǔn)把數(shù)據(jù)提出來(lái)。從架構(gòu)的角度來(lái)看,Glassbox Inspector 與許多性能監(jiān)視系統(tǒng)類(lèi)似,區(qū)別在于它擁有定義良好的實(shí)現(xiàn)了關(guān)鍵監(jiān)視功能的模塊。


          圖 2. Glassbox Inspector 架構(gòu)
          Glassbox Inspector 架構(gòu)

          Java 管理擴(kuò)展(JMX)是通過(guò)查看受管理對(duì)象的屬性來(lái)管理 Java 應(yīng)用程序的標(biāo)準(zhǔn) API。JMX Remote 標(biāo)準(zhǔn)擴(kuò)展了 JMX,允許外部客戶(hù)進(jìn)程管理應(yīng)用程序。JMX 管理是 Java 企業(yè)容器中的標(biāo)準(zhǔn)特性。現(xiàn)有多個(gè)成熟的第三方 JMX 庫(kù)和工具,而且 JMX 支持在 Java 5 中也已經(jīng)集成進(jìn)核心 Java 運(yùn)行時(shí)。Sun 公司的 Java 5 虛擬機(jī)包含 JConsole JMX 客戶(hù)端。

          在繼續(xù)本文之前,應(yīng)當(dāng)下載 AspectJ、JMX 和 JMX Remote 的當(dāng)前版本以及本文的源代碼包(請(qǐng)參閱 參考資料 獲得技術(shù)內(nèi)容,參閱下載 獲得代碼)。如果正在使用 Java 5 虛擬機(jī),那么內(nèi)置了 JMX。請(qǐng)注意源代碼包包含開(kāi)放源碼的 Glassbox Inspector 性能監(jiān)視基礎(chǔ)設(shè)施 1.0 alpha 發(fā)行版的完整最終代碼。

          基本的系統(tǒng)

          我將從一個(gè)基本的面向方面的性能監(jiān)視系統(tǒng)開(kāi)始。這個(gè)系統(tǒng)可以捕捉處理 Web 請(qǐng)求的不同 servlet 的時(shí)間和計(jì)數(shù)。清單 1 顯示了一個(gè)捕捉這個(gè)性能信息的簡(jiǎn)單方面:


          清單 1. 捕捉 servlet 時(shí)間和計(jì)數(shù)的方面


          /**
          * Monitors performance timing and execution counts for
          * <code>HttpServlet</code> operations
          */
          public aspect HttpServletMonitor {

          /** Execution of any Servlet request methods. */
          public pointcut monitoredOperation(Object operation) :
          execution(void HttpServlet.do*(..)) && this(operation);

          /** Advice that records statistics for each monitored operation. */
          void around(Object operation) : monitoredOperation(operation) {
          long start = getTime();

          proceed(operation);

          PerfStats stats = lookupStats(operation);
          stats.recordExecution(getTime(), start);
          }

          /**
          * Find the appropriate statistics collector object for this
          * operation.
          *
          * @param operation
          * the instance of the operation being monitored
          */
          protected PerfStats lookupStats(Object operation) {
          Class keyClass = operation.getClass();
          synchronized(operations) {
          stats = (PerfStats)operations.get(keyClass);
          if (stats == null) {
          stats = perfStatsFactory.
          createTopLevelOperationStats(HttpServlet.class,
          keyClass);
          operations.put(keyClass, stats);
          }
          }
          return stats;
          }

          /**
          * Helper method to collect time in milliseconds. Could plug in
          * nanotimer.
          */
          public long getTime() {
          return System.currentTimeMillis();
          }

          public void setPerfStatsFactory(PerfStatsFactory
          perfStatsFactory) {
          this.perfStatsFactory = perfStatsFactory;
          }

          public PerfStatsFactory getPerfStatsFactory() {
          return perfStatsFactory;
          }

          /** Track top-level operations. */
          private Map/*<Class,PerfStats>*/ operations =
          new WeakIdentityHashMap();
          private PerfStatsFactory perfStatsFactory;
          }

          /**
          * Holds summary performance statistics for a
          * given topic of interest
          * (e.g., a subclass of Servlet).
          */
          public interface PerfStats {
          /**
          * Record that a single execution occurred.
          *
          * @param start time in milliseconds
          * @param end time in milliseconds
          */
          void recordExecution(long start, long end);

          /**
          * Reset these statistics back to zero. Useful to track statistics
          * during an interval.
          */
          void reset();

          /**
          * @return total accumulated time in milliseconds from all
          * executions (since last reset).
          */
          int getAccumulatedTime();

          /**
          * @return the largest time for any single execution, in
          * milliseconds (since last reset).
          */
          int getMaxTime();

          /**
          * @return the number of executions recorded (since last reset).
          */
          int getCount();
          }

          /**
          * Implementation of the
          *
          * @link PerfStats interface.
          */
          public class PerfStatsImpl implements PerfStats {
          private int accumulatedTime=0L;
          private int maxTime=0L;
          private int count=0;

          public void recordExecution(long start, long end) {
          int time = (int)(getTime()-start);
          accumulatedTime += time;
          maxTime = Math.max(time, maxTime);
          count++;
          }

          public void reset() {
          accumulatedTime=0L;
          maxTime=0L;
          count=0;
          }

          int getAccumulatedTime() { return accumulatedTime; }
          int getMaxTime() { return maxTime; }
          int getCount() { return count; }
          }

          public interface PerfStatsFactory {
          PerfStats
          createTopLevelOperationStats(Object type, Object key);
          }

          可以看到,第一個(gè)版本相當(dāng)基礎(chǔ)。HttpServletMonitor 定義了一個(gè)切入點(diǎn),叫作 monitoredOperation,它匹配 HttpServlet 接口上任何名稱(chēng)以 do 開(kāi)始的方法的執(zhí)行。這些方法通常是 doGet()doPost(),但是通過(guò)匹配 doHead()、doDelete()doOptions()、doPut()doTrace(),它也可以捕捉不常用的 HTTP 請(qǐng)求選項(xiàng)。

          管理開(kāi)銷(xiāo)

          在 這篇文章的后半部分,我將把重點(diǎn)放在管理監(jiān)視框架開(kāi)銷(xiāo)的技術(shù)上,但是現(xiàn)在,值得注意的是基本策略:在速度慢的事情發(fā)生時(shí)(像訪(fǎng)問(wèn) servlet 或數(shù)據(jù)庫(kù)),我要做一些在內(nèi)存中的操作,這只花幾毫秒。在實(shí)踐中,對(duì)大多數(shù)應(yīng)用程序的端對(duì)端響應(yīng)時(shí)間只會(huì)添加微不足道的開(kāi)銷(xiāo)。

          每當(dāng)其中一個(gè)操作執(zhí)行的時(shí)候,系統(tǒng)都會(huì)執(zhí)行 around 通知去監(jiān)視性能。建議啟動(dòng)一個(gè)秒表,然后讓原始請(qǐng)求繼續(xù)進(jìn)行。之后,通知停止秒表并查詢(xún)與指定操作對(duì)應(yīng)的性能統(tǒng)計(jì)對(duì)象。然后它再調(diào)用 PerfStats 接口的 recordExecution(),記錄操作經(jīng)歷的時(shí)間。這僅僅更新指定操作的總時(shí)間、最大時(shí)間(如果適用)以及執(zhí)行次數(shù)。自然也可以把這種方式擴(kuò)展成計(jì)算額外的統(tǒng)計(jì)值,并在問(wèn)題可能發(fā)生的地方保存單獨(dú)的數(shù)據(jù)點(diǎn)。

          我在方面中使用了一個(gè)哈希圖為每種操作處理程序保存累計(jì)統(tǒng)計(jì)值。在這個(gè)版本中,操作處理程序是 HttpServlet 的子類(lèi),所以 servlet 的類(lèi)被用作鍵。我還用術(shù)語(yǔ) 操作 表示 Web 請(qǐng)求,以便把它與應(yīng)用程序可能產(chǎn)生的其他請(qǐng)求(例如,數(shù)據(jù)庫(kù)請(qǐng)求)區(qū)分開(kāi)。在這篇文章的第二部分,我將擴(kuò)展這種方式,來(lái)解決更常見(jiàn)的在控制器中使用的基于 類(lèi)或方法的跟蹤操作情況,例如 Apache Struts 的動(dòng)作類(lèi)或 Spring 的多動(dòng)作控制器方法。



          回頁(yè)首


          公開(kāi)性能數(shù)據(jù)

          線(xiàn)程安全性

          Glassbox Inspector 監(jiān)視系統(tǒng)的統(tǒng)計(jì)值捕捉代碼不是線(xiàn)程安全的。我寧愿維護(hù)(可能)略微不準(zhǔn)確的統(tǒng)計(jì)值(由于多個(gè)線(xiàn)程很少會(huì)同時(shí)訪(fǎng)問(wèn)一個(gè) PerfStats 實(shí)例),也不想向程序執(zhí)行添加額外的同步。如果您偏愛(ài)更高的準(zhǔn)確性,也只要讓互斥體同步即可(例如,與方面同步)。如果正在跟蹤的累計(jì)時(shí)間超過(guò) 32 位的長(zhǎng)度,那么同步會(huì)很重要,因?yàn)?Java 平臺(tái)不保證對(duì) 64 位數(shù)據(jù)的原子更新。但是,在毫秒的精度情況下,32 位的長(zhǎng)度會(huì)提供 46 天的累計(jì)時(shí)間。我建議對(duì)于真實(shí)的應(yīng)用,應(yīng)當(dāng)更加頻繁地搜集和重設(shè)統(tǒng)計(jì)值,所以我堅(jiān)持使用 int 值。

          一旦捕捉到了性能數(shù)據(jù),讓它可以使用的方式就很多了。最簡(jiǎn)單的方式就是把信息定期地寫(xiě)入日志文件。也可以把信息裝入數(shù)據(jù)庫(kù)進(jìn)行分析。由于不增加延遲、復(fù)雜性以及合計(jì)、日志及處理信息的開(kāi)銷(xiāo),提供到即時(shí)系統(tǒng)數(shù)據(jù)的直接訪(fǎng)問(wèn)通常會(huì)更好。在下一節(jié)中我將介紹如何做到這一點(diǎn)。

          我 想使用一個(gè)現(xiàn)有管理工作能夠顯示和跟蹤的標(biāo)準(zhǔn)協(xié)議,所以我將用 JMX API 來(lái)共享性能統(tǒng)計(jì)值。使用 JMX 意味著每個(gè)性能統(tǒng)計(jì)實(shí)例都會(huì)公開(kāi)成一個(gè)管理 bean,從而提供詳細(xì)的性能數(shù)據(jù)。標(biāo)準(zhǔn)的 JMX 客戶(hù)端(像 Sun 公司的 JConsole)也能夠顯示這些信息。請(qǐng)參閱 參考資料 學(xué)習(xí)有關(guān) JMX 的更多內(nèi)容。


          圖 3 是一幅 JConsole 的截屏,顯示了 Glassbox Inspector 監(jiān)視 Duke 書(shū)店示例應(yīng)用程序性能的情況。(請(qǐng)參閱 參考資料)。清單 2 顯示了實(shí)現(xiàn)這個(gè)特性的代碼。


          圖 3. 用 Glassbox Inspector 查看操作統(tǒng)計(jì)值
          用 Glassbox Inspector 查看操作統(tǒng)計(jì)值

          傳統(tǒng)上,支持 JMX 包括用樣本代碼實(shí)現(xiàn)模式。在這種情況下,我將把 JMX 與 AspectJ 結(jié)合,這個(gè)結(jié)合可以讓我獨(dú)立地編寫(xiě)管理邏輯。


          清單 2. 實(shí)現(xiàn) JMX 管理特性


          /** Reusable aspect that automatically registers
          * beans for management
          */
          public aspect JmxManagement {

          /** Defines classes to be managed and
          * defines basic management operation
          */
          public interface ManagedBean {
          /** Define a JMX operation name for this bean.
          * Not to be confused with a Web request operation.
          */
          String getOperationName();
          /** Returns the underlying JMX MBean that
          * provides management
          * information for this bean (POJO).
          */
          Object getMBean();
          }

          /** After constructing an instance of
          * <code>ManagedBean</code>, register it
          */
          after() returning (ManagedBean bean):
          call(ManagedBean+.new(..)) {
          String keyName = bean.getOperationName();
          ObjectName objectName =
          new
          ObjectName("glassbox.inspector:" + keyName);

          Object mBean = bean.getMBean();
          if (mBean != null) {
          server.registerMBean(mBean, objectName);
          }
          }

          /**
          * Utility method to encode a JMX key name,
          * escaping illegal characters.
          * @param jmxName unescaped string buffer of form
          * JMX keyname=key
          * @param attrPos position of key in String
          */
          public static StringBuffer
          jmxEncode(StringBuffer jmxName, int attrPos) {
          for (int i=attrPos; i<jmxName.length(); i++) {
          if (jmxName.charAt(i)==',' ) {
          jmxName.setCharAt(i, ';');
          } else if (jmxName.charAt(i)=='?'
          || jmxName.charAt(i)=='*' ||
          jmxName.charAt(i)=='\\' ) {
          jmxName.insert(i, '\\');
          i++;
          } else if (jmxName.charAt(i)=='\n') {
          jmxName.insert(i, '\\');
          i++;
          jmxName.setCharAt(i, 'n');
          }
          }
          return jmxName;
          }

          /** Defines the MBeanServer with which beans
          * are auto-registered.
          */
          private MBeanServer server;

          public void setMBeanServer(MBeanServer server) {
          this.server = server;
          }

          public MBeanServer getMBeanServer() {
          return server;
          }
          }

          JMX 工具

          有 幾個(gè)比較好的 JMX 實(shí)現(xiàn)庫(kù)支持遠(yuǎn)程 JMX。Sun 公司在免費(fèi)許可下提供了 JMX 和 JMX Remote 的參考實(shí)現(xiàn)。也有一些開(kāi)放源碼的實(shí)現(xiàn)。MX4J 是其中比較流行的一個(gè),它包含輔助庫(kù)和工具(像 JMX 客戶(hù)端)。Java 5 把 JMX 和 JMX 遠(yuǎn)程支持集成進(jìn)了虛擬機(jī)。Java 5 還在 javax.management 包中引入了虛擬機(jī)性能的管理 bean。Sun 的 Java 5 虛擬機(jī)包括標(biāo)準(zhǔn)的 JMX 客戶(hù)端 JConsole。

          可以看出這個(gè)第一個(gè)方面是可以重用的。利用它,我能夠用 after 建議自動(dòng)為任何實(shí)現(xiàn) ManagedBean 接口的類(lèi)登記對(duì)象實(shí)例。這與 AspectJ 標(biāo)記器接口的理念類(lèi)似(請(qǐng)參閱 參考資料):定義了實(shí)例應(yīng)當(dāng)通過(guò) JMX 公開(kāi)的類(lèi)。但是,與真正的標(biāo)記器接口不同的是,它還定義了兩個(gè)方法 。

          這 個(gè)方面提供了一個(gè)設(shè)置器,定義應(yīng)當(dāng)用哪個(gè) MBean 服務(wù)器管理對(duì)象。這是一個(gè)使用反轉(zhuǎn)控制(IOC)模式進(jìn)行配置的示例,因此很自然地適合方面。在最終代碼的完整清單中,將會(huì)看到我用了一個(gè)簡(jiǎn)單的輔助方面 對(duì)系統(tǒng)進(jìn)行配置。在更大的系統(tǒng)中,我將用 Spring 框架這樣的 IOC 容器來(lái)配置類(lèi)和方面。請(qǐng)參閱 參考資料 獲得關(guān)于 IOC 和 Spring 框架的更多信息,并獲得關(guān)于使用 Spring 配置方面的介紹。


          清單 3. 公開(kāi)負(fù)責(zé) JMX 管理的 bean

          /** Applies JMX management to performance statistics beans. */
          public aspect StatsJmxManagement {
          /** Management interface for performance statistics.
          * A subset of @link PerfStats
          */
          public interface PerfStatsMBean extends ManagedBean {
          int getAccumulatedTime();
          int getMaxTime();
          int getCount();
          void reset();
          }

          /**
          * Make the @link PerfStats interface
          * implement @link PerfStatsMBean,
          * so all instances can be managed
          */
          declare parents: PerfStats implements PerfStatsMBean;

          /** Creates a JMX MBean to represent this PerfStats instance. */
          public DynamicMBean PerfStats.getMBean() {
          try {
          RequiredModelMBean mBean = new RequiredModelMBean();
          mBean.setModelMBeanInfo
          (assembler.getMBeanInfo(this, getOperationName()));
          mBean.setManagedResource(this,
          "ObjectReference");
          return mBean;
          } catch (Exception e) {
          /* This is safe because @link ErrorHandling
          * will resolve it. This is described later!
          */
          throw new
          AspectConfigurationException("can't
          register bean ", e);
          }
          }

          /** Determine JMX operation name for this
          * performance statistics bean.
          */
          public String PerfStats.getOperationName() {
          StringBuffer keyStr =
          new StringBuffer("operation=\"");
          int pos = keyStr.length();

          if (key instanceof Class) {
          keyStr.append(((Class)key).getName());
          } else {
          keyStr.append(key.toString());
          }

          JmxManagement.jmxEncode(keyStr, pos);

          keyStr.append("\"");
          return keyStr.toString();
          }

          private static Class[] managedInterfaces =
          { PerfStatsMBean.class };
          /**
          * Spring JMX utility MBean Info Assembler.
          * Allows @link PerfStatsMBean to serve
          * as the management interface of all performance
          * statistics implementors.
          */
          static InterfaceBasedMBeanInfoAssembler assembler;
          static {
          assembler = new InterfaceBasedMBeanInfoAssembler();
          assembler.setManagedInterfaces(managedInterfaces);
          }
          }

          清單 3 包含 StatsJmxManagement 方面,它具體地定義了哪個(gè)對(duì)象應(yīng)當(dāng)公開(kāi)管理 bean。它描述了一個(gè)接口 PerfStatsMBean,這個(gè)接口定義了用于任何性能統(tǒng)計(jì)實(shí)現(xiàn)的管理接口。其中包括計(jì)數(shù)、總時(shí)間、最大時(shí)間的統(tǒng)計(jì)值,還有重設(shè)操作,這個(gè)接口是 PerfStats 接口的子集。

          PerfStatsMBean 本身擴(kuò)展了 ManagedBean,所以它的任何實(shí)現(xiàn)都會(huì)自動(dòng)被 JmxManagement 方面登記成進(jìn)行管理。我采用 AspectJ 的 declare parents 格式讓 PerfStats 接口擴(kuò)展了一個(gè)特殊的管理接口 PerfStatsMBean。結(jié)果是 JMX Dynamic MBean 技術(shù)會(huì)管理這些對(duì)象,與使用 JMX 的標(biāo)準(zhǔn) MBean 相比,我更喜歡這種方式。

          使用標(biāo)準(zhǔn) MBean 會(huì)要求定義一個(gè)管理接口,接口名稱(chēng)基于每個(gè)性能統(tǒng)計(jì)的實(shí)現(xiàn)類(lèi),例如 PerfStatsImplMBean。后來(lái),當(dāng)我向 Glassbox Inspector 添加 PerfStats 的子類(lèi)時(shí),情況變?cè)懔?,因?yàn)槲冶灰髣?chuàng)建對(duì)應(yīng)的接口(例如 OperationPerfStatsImpl)。標(biāo)準(zhǔn) MBean 的約定使得接口依賴(lài)于實(shí)現(xiàn),而且代表這個(gè)系統(tǒng)的繼承層次出現(xiàn)不必要的重復(fù)。

          部署這些方面

          這 篇文章中使用的方面只能應(yīng)用到它們監(jiān)視的每個(gè)應(yīng)用程序上,不能應(yīng)用到第三方庫(kù)或容器代碼上。所以,如果要把它們集成到生產(chǎn)系統(tǒng)中,可以把它們編譯到應(yīng)用程 序中,或者編織到已經(jīng)編譯的應(yīng)用程序中,或者使用裝入時(shí)編織(這是這種用例下我偏愛(ài)的方式)。在這篇文章的第二部分,您將學(xué)到有關(guān)裝入時(shí)編程的更多內(nèi)容。

          這個(gè)方面剩下的部分負(fù)責(zé)用 JMX 創(chuàng)建正確的 MBean 和對(duì)象名稱(chēng)。我重用了來(lái)自 Spring 框架的 JMX 工具 InterfaceBasedMBeanInfoAssembler,用它可以更容易地創(chuàng)建 JMX DynamicMBean(用 PerfStatsMBean 接口管理 PerfStats 實(shí)例)。在這個(gè)階段,我只公開(kāi)了 PerfStats 實(shí)現(xiàn)。這個(gè)方面還用受管理 bean 類(lèi)上的類(lèi)型間聲明定義了輔助方法。如果這些類(lèi)中的任何一個(gè)的子類(lèi)需要覆蓋默認(rèn)行為,那么可以通過(guò)覆蓋這個(gè)方法實(shí)現(xiàn)。

          您可能想知道為什么我用方面進(jìn)行管理而不是直接把支持添加到 PerfStatsImpl 的實(shí)現(xiàn)類(lèi)中。雖然把管理添加到這個(gè)類(lèi)中不會(huì)把代碼分散,但是它會(huì)把性能監(jiān)視系統(tǒng)的實(shí)現(xiàn)與 JMX 混雜在一起。所以,如果我想把這個(gè)系統(tǒng)用在一個(gè) 沒(méi)有 JMX 的系統(tǒng)中,就要被迫包含 JMX 的庫(kù),還要禁止有關(guān)服務(wù)。而且,當(dāng)擴(kuò)展系統(tǒng)的管理功能時(shí),我還要公開(kāi)更多的類(lèi)用 JMX 進(jìn)行管理。使用方面可以讓系統(tǒng)的管理策略保持模塊化。



          回頁(yè)首


          數(shù)據(jù)庫(kù)請(qǐng)求監(jiān)視

          分 布式調(diào)用是應(yīng)用程序性能低和出錯(cuò)誤的一個(gè)常見(jiàn)源頭。多數(shù)基于 Web 的應(yīng)用程序要做相當(dāng)數(shù)量的數(shù)據(jù)庫(kù)工作,所以對(duì)查詢(xún)和其他數(shù)據(jù)庫(kù)請(qǐng)求進(jìn)行監(jiān)視就成為性能監(jiān)視中特別重要的領(lǐng)域。常見(jiàn)的問(wèn)題包括編寫(xiě)得有毛病的查詢(xún)、遺漏了索 引以及每個(gè)操作中過(guò)量的數(shù)據(jù)庫(kù)請(qǐng)求。在這一節(jié),我將對(duì)監(jiān)視系統(tǒng)進(jìn)行擴(kuò)展,跟蹤數(shù)據(jù)庫(kù)中與操作相關(guān)的活動(dòng)。

          分布式調(diào)用

          在 這一節(jié),我介紹了一種處理數(shù)據(jù)庫(kù)分布式調(diào)用的方式。雖然數(shù)據(jù)庫(kù)通常位于不同的機(jī)器上,但我的技術(shù)也適用于本地?cái)?shù)據(jù)庫(kù)。我的方式也可以自然地?cái)U(kuò)展到其他分布 式資源上,包括遠(yuǎn)程對(duì)象調(diào)用。在這篇文章的第二部分中,我將介紹如何用 SOAP 把這項(xiàng)技術(shù)應(yīng)用到 Web 服務(wù)調(diào)用上。

          開(kāi) 始時(shí),我將監(jiān)視數(shù)據(jù)庫(kù)的連接次數(shù)和數(shù)據(jù)庫(kù)語(yǔ)句的執(zhí)行。為了有效地支持這個(gè)要求,我需要?dú)w納性能監(jiān)視信息,并允許跟蹤嵌套在一個(gè)操作中的性能。我想把性能的 公共元素提取到一個(gè)抽象基類(lèi)。每個(gè)基類(lèi)負(fù)責(zé)跟蹤某項(xiàng)操作前后的性能,還需要更新系統(tǒng)范圍內(nèi)這條信息的性能統(tǒng)計(jì)值。這樣我就能跟蹤嵌套的 servlet 請(qǐng)求,對(duì)于在 Web 應(yīng)用程序中支持對(duì)控制器的跟蹤,這也會(huì)很重要(在第二部分討論)。

          因?yàn)槲蚁敫鶕?jù)請(qǐng)求更新數(shù)據(jù)庫(kù)的性能,所以我將采用 composite pattern 跟蹤由其他統(tǒng)計(jì)值持有的統(tǒng)計(jì)值。這樣,操作(例如 servelt)的統(tǒng)計(jì)值就持有每個(gè)數(shù)據(jù)庫(kù)的性能統(tǒng)計(jì)。數(shù)據(jù)庫(kù)的統(tǒng)計(jì)值持有有關(guān)連接次數(shù)的信息,并聚合每個(gè)單獨(dú)語(yǔ)句的額外統(tǒng)計(jì)值。圖 4 顯示整體設(shè)計(jì)是如何結(jié)合在一起的。清單 4 擁有新的基監(jiān)視方面,它支持對(duì)不同的請(qǐng)求進(jìn)行監(jiān)視。


          圖 4. 一般化后的監(jiān)視設(shè)計(jì)
          修訂后的監(jiān)視設(shè)計(jì)

          清單 4. 基監(jiān)視方面

          /** Base aspect for monitoring functionality.
          * Uses the worker object pattern.
          */
          public abstract aspect AbstractRequestMonitor {

          /** Matches execution of the worker object
          * for a monitored request.
          */
          public pointcut
          requestExecution(RequestContext requestContext) :
          execution(* RequestContext.execute(..))
          && this(requestContext);

          /** In the control flow of a monitored request,
          * i.e., of the execution of a worker object.
          */
          public pointcut inRequest(RequestContext requestContext) :
          cflow(requestExecution(requestContext));

          /** establish parent relationships
          * for request context objects.
          */
          // use of call is cleaner since constructors are called
          // once but executed many times
          after(RequestContext parentContext)
          returning (RequestContext childContext) :
          call(RequestContext+.new(..)) &&
          inRequest(parentContext) {
          childContext.setParent(parentContext);
          }

          public long getTime() {
          return System.currentTimeMillis();
          }

          /** Worker object that holds context information
          * for a monitored request.
          */
          public abstract class RequestContext {
          /** Containing request context, if any.
          * Maintained by @link AbstractRequestMonitor
          */
          protected RequestContext parent = null;
          /** Associated performance statistics.
          * Used to cache results of @link #lookupStats()
          */
          protected PerfStats stats;
          /** Start time for monitored request. */
          protected long startTime;

          /**
          * Record execution and elapsed time
          * for each monitored request.
          * Relies on @link #doExecute() to proceed
          * with original request.
          */
          public final Object execute() {
          startTime = getTime();

          Object result = doExecute();

          PerfStats stats = getStats();
          if (stats != null) {
          stats.recordExecution(startTime, getTime());
          }

          return result;
          }

          /** template method: proceed with original request */
          public abstract Object doExecute();

          /** template method: determines appropriate performance
          * statistics for this request
          */
          protected abstract PerfStats lookupStats();

          /** returns performance statistics for this method */
          public PerfStats getStats() {
          if (stats == null) {
          stats = lookupStats(); // get from cache if available
          }
          return stats;
          }

          public RequestContext getParent() {
          return parent;
          }

          public void setParent(RequestContext parent) {
          this.parent = parent;
          }
          }
          }

          不出所料,對(duì)于如何存儲(chǔ)共享的性能統(tǒng)計(jì)值和基方面的每請(qǐng)求狀態(tài),有許多選擇。例如,我可以用帶有更底層機(jī)制的單體(例如 ThreadLocal)持有一堆統(tǒng)計(jì)值和上下文。但是,我選用了工人對(duì)象(Worker Object)模式(請(qǐng)參閱 參考資料), 因?yàn)樗С指幽K化、更簡(jiǎn)潔的表達(dá)。雖然這會(huì)帶來(lái)一些額外的開(kāi)銷(xiāo),但是分配單一對(duì)象并執(zhí)行建議所需要的額外時(shí)間,比起為 Web 和數(shù)據(jù)庫(kù)請(qǐng)求提供服務(wù)來(lái)說(shuō),通常是微不足道的。換句話(huà)說(shuō),我可以在不增加開(kāi)銷(xiāo)的情況下,在監(jiān)視代碼中做一些處理工作,因?yàn)樗\(yùn)行的頻繁相對(duì)很低,而且比起 在通過(guò)網(wǎng)絡(luò)發(fā)送信息和等候磁盤(pán) I/O 上花費(fèi)的時(shí)間來(lái)說(shuō),通常就微不足道了。對(duì)于 profiler 來(lái)說(shuō),這可能是個(gè)糟糕的設(shè)計(jì),因?yàn)樵? profiler 中可能想要跟蹤每個(gè)請(qǐng)求中的許多操作(和方法)的數(shù)據(jù)。但是,我是在做請(qǐng)求的統(tǒng)計(jì)匯總,所以這個(gè)選擇是合理的。

          在上面的基方面中,我把當(dāng)前被監(jiān)視請(qǐng)求的中間狀態(tài)保存在匿名內(nèi)部類(lèi)中。這個(gè)工人對(duì)象用來(lái)包裝被監(jiān)視請(qǐng)求的執(zhí)行。工人對(duì)象 RequestContext 是在基類(lèi)中定義的,提供的 final execute 方法定義了對(duì)請(qǐng)求進(jìn)行監(jiān)視的流程。execute 方法委托抽象的模板方法 doExecute() 負(fù)責(zé)繼續(xù)處理原始的連接點(diǎn)。在 doExecute() 方法中也適合在根據(jù)上下文信息(例如正在連接的數(shù)據(jù)源)繼續(xù)處理被監(jiān)視的連接點(diǎn)之前設(shè)置統(tǒng)計(jì)值,并在連接點(diǎn)返回之后關(guān)聯(lián)返回的值(例如數(shù)據(jù)庫(kù)連接)。

          每個(gè)監(jiān)視方面還負(fù)責(zé)提供抽象方法 lookupStats() 的實(shí)現(xiàn),用來(lái)確定為指定請(qǐng)求更新哪個(gè)統(tǒng)計(jì)對(duì)象。lookupStats() 需要根據(jù)被監(jiān)視的連接點(diǎn)訪(fǎng)問(wèn)信息。一般來(lái)說(shuō),捕捉的上下文對(duì)于每個(gè)監(jiān)視方面都應(yīng)當(dāng)各不相同。例如,在 HttpServletMonitor 中,需要的上下文就是目前執(zhí)行操作對(duì)象的類(lèi)。對(duì)于 JDBC 連接,需要的上下文就是得到的數(shù)據(jù)源。因?yàn)橐蟾鶕?jù)上下文而不同,所以設(shè)置工人對(duì)象的建議最好是包含在每個(gè)子方面中,而不是在抽象的基方面中。這種安排更 清楚,它支持類(lèi)型檢測(cè),而且也比在基類(lèi)中編寫(xiě)一個(gè)建議,再把 JoinPoint 傳遞給所有孩子執(zhí)行得更好。



          回頁(yè)首


          servlet 請(qǐng)求跟蹤

          AbstractRequestMonitor 確實(shí)包含一個(gè)具體的 after 建議,負(fù)責(zé)跟蹤請(qǐng)求上下文的雙親上下文。這就讓我可以把嵌套請(qǐng)求的操作統(tǒng)計(jì)值與它們雙親的統(tǒng)計(jì)值關(guān)聯(lián)起來(lái)(例如,哪個(gè) servlet 請(qǐng)求造成了這個(gè)數(shù)據(jù)庫(kù)訪(fǎng)問(wèn))。對(duì)于示例監(jiān)視系統(tǒng)來(lái)說(shuō),我明確地 需要 嵌套的工人對(duì)象,而 不想 把自己限制在只能處理頂級(jí)請(qǐng)求上。例如,所有的 Duke 書(shū)店 servlet 都把調(diào)用 BannerServlet 作為顯示頁(yè)面的一部分。所以能把這些調(diào)用的次數(shù)分開(kāi)是有用的,如清單 5 所示。在這里,我沒(méi)有顯示在操作統(tǒng)計(jì)值中查詢(xún)嵌套統(tǒng)計(jì)值的支持代碼(可以在本文的源代碼中看到它)。在第二部分,我將重新回到這個(gè)主題,介紹如何更新 JMX 支持來(lái)顯示像這樣的嵌套統(tǒng)計(jì)值。


          清單 5. 更新的 servlet 監(jiān)視

          清單 5 should now read
          public aspect HttpServletMonitor extends AbstractRequestMonitor {

          /** Monitor Servlet requests using the worker object pattern */
          Object around(final Object operation) :
          monitoredOperation(operation) {
          RequestContext requestContext = new RequestContext() {
          public Object doExecute() {
          return proceed(operation);
          }

          public PerfStats lookupStats() {
          if (getParent() != null) {
          // nested operation
          OperationStats parentStats =
          (OperationStats)getParent().getStats();
          return
          parentStats.getOperationStats(operation.getClass());
          }
          return lookupStats(operation.getClass());
          }
          };
          return requestContext.execute();
          }
          ...

          清單 5 顯示了修訂后進(jìn)行 serverlet 請(qǐng)求跟蹤的監(jiān)視建議。余下的全部代碼與 清單 1 相同:或者推入基方面 AbstractRequestMonitor 方面,或者保持一致。



          回頁(yè)首


          JDBC 監(jiān)視

          設(shè)置好性能監(jiān)視框架后,我現(xiàn)在準(zhǔn)備跟蹤數(shù)據(jù)庫(kù)的連接次數(shù)以及數(shù)據(jù)庫(kù)語(yǔ)句的時(shí)間。而且,我還希望能夠把數(shù)據(jù)庫(kù)語(yǔ)句和實(shí)際連接的數(shù)據(jù)庫(kù)關(guān)聯(lián)起來(lái)(在 lookupStats() 方法中)。為了做到這一點(diǎn),我創(chuàng)建了兩個(gè)跟蹤 JDBC 語(yǔ)句和連接信息的方面: JdbcConnectionMonitorJdbcStatementMonitor

          這些方面的一個(gè)關(guān)鍵職責(zé)是跟蹤對(duì)象引用的鏈。我想根據(jù)我用來(lái)連接數(shù) 據(jù)庫(kù)的 URI 跟蹤請(qǐng)求,或者至少根據(jù)數(shù)據(jù)庫(kù)名稱(chēng)來(lái)跟蹤。這就要求跟蹤用來(lái)獲得連接的數(shù)據(jù)源。我還想進(jìn)一步根據(jù) SQL 字符串跟蹤預(yù)備語(yǔ)句(在執(zhí)行之前就已經(jīng)準(zhǔn)備就緒)。最后,我需要跟蹤與正在執(zhí)行的語(yǔ)句關(guān)聯(lián)的 JDBC 連接。您會(huì)注意到:JDBC 語(yǔ)句 確實(shí) 為它們的連接提供了存取器;但是,應(yīng)用程序服務(wù)器和 Web 應(yīng)用程序框架頻繁地使用修飾器模式包裝 JDBC 連接。我想確保自己能夠把語(yǔ)句與我擁有句柄的連接關(guān)聯(lián)起來(lái),而不是與包裝的連接關(guān)聯(lián)起來(lái)。

          JdbcConnectionMonitor 負(fù)責(zé)測(cè)量數(shù)據(jù)庫(kù)連接的性能統(tǒng)計(jì)值,它也把連接與它們來(lái)自數(shù)據(jù)源或連接 URL 的元數(shù)據(jù)(例如 JDBC URL 或數(shù)據(jù)庫(kù)名稱(chēng))關(guān)聯(lián)在一起。JdbcStatementMonitor 負(fù)責(zé)測(cè)量執(zhí)行語(yǔ)句的性能統(tǒng)計(jì)值,跟蹤用來(lái)取得語(yǔ)句的連接,跟蹤與預(yù)備(和可調(diào)用)語(yǔ)句關(guān)聯(lián)的 SQL 字符串。清單 6 顯示了 JdbcConnectionMonitor 方面。


          清單 6. JdbcConnectionMonitor 方面

          /**
          * Monitor performance for JDBC connections,
          * and track database connection information associated with them.
          */
          public aspect JdbcConnectionMonitor extends AbstractRequestMonitor {

          /** A call to establish a connection using a
          * <code>DataSource</code>
          */
          public pointcut dataSourceConnectionCall(DataSource dataSource) :
          call(Connection+ DataSource.getConnection(..))
          && target(dataSource);

          /** A call to establish a connection using a URL string */
          public pointcut directConnectionCall(String url) :
          (call(Connection+ Driver.connect(..)) || call(Connection+
          DriverManager.getConnection(..))) &&
          args(url, ..);

          /** A database connection call nested beneath another one
          * (common with proxies).
          */
          public pointcut nestedConnectionCall() :
          cflowbelow(dataSourceConnectionCall(*) ||
          directConnectionCall(*));

          /** Monitor data source connections using
          * the worker object pattern
          */
          Connection around(final DataSource dataSource) :
          dataSourceConnectionCall(dataSource)
          && !nestedConnectionCall() {
          RequestContext requestContext =
          new ConnectionRequestContext() {
          public Object doExecute() {
          accessingConnection(dataSource);
          // set up stats early in case needed

          Connection connection = proceed(dataSource);

          return addConnection(connection);
          }

          };
          return (Connection)requestContext.execute();
          }

          /** Monitor url connections using the worker object pattern */
          Connection around(final String url) : directConnectionCall(url)
          && !nestedConnectionCall() {
          RequestContext requestContext =
          new ConnectionRequestContext() {
          public Object doExecute() {
          accessingConnection(url);

          Connection connection = proceed(url);

          return addConnection(connection);
          }
          };
          return (Connection)requestContext.execute();
          }

          /** Get stored name associated with this data source. */
          public String getDatabaseName(Connection connection) {
          synchronized (connections) {
          return (String)connections.get(connection);
          }
          }

          /** Use common accessors to return meaningful name
          * for the resource accessed by this data source.
          */
          public String getNameForDataSource(DataSource ds) {
          // methods used to get names are listed in descending
          // preference order
          String possibleNames[] =
          { "getDatabaseName",
          "getDatabasename",
          "getUrl", "getURL",
          "getDataSourceName",
          "getDescription" };
          String name = null;
          for (int i=0; name == null &&
          i<possibleNames.length; i++) {
          try {
          Method method =
          ds.getClass().getMethod(possibleNames[i], null);
          name = (String)method.invoke(ds, null);
          } catch (Exception e) {
          // keep trying
          }
          }
          return (name != null) ? name : "unknown";
          }

          /** Holds JDBC connection-specific context information:
          * a database name and statistics
          */
          protected abstract class ConnectionRequestContext
          extends RequestContext {
          private ResourceStats dbStats;

          /** set up context statistics for accessing
          * this data source
          */
          protected void
          accessingConnection(final DataSource dataSource) {
          addConnection(getNameForDataSource(dataSource),
          connection);
          }

          /** set up context statistics for accessing this database */
          protected void accessingConnection(String databaseName) {
          this.databaseName = databaseName;

          // might be null if there is database access
          // caused from a request I'm not tracking...
          if (getParent() != null) {
          OperationStats opStats =
          (OperationStats)getParent().getStats();
          dbStats = opStats.getDatabaseStats(databaseName);
          }
          }

          /** record the database name for this database connection */
          protected Connection
          addConnection(final Connection connection) {
          synchronized(connections) {
          connections.put(connection, databaseName);
          }
          return connection;
          }

          protected PerfStats lookupStats() {
          return dbStats;
          }
          };

          /** Associates connections with their database names */
          private Map/*<Connection,String>*/ connections =
          new WeakIdentityHashMap();

          }

          清單 6 顯示了利用 AspectJ 和 JDBC API 跟蹤數(shù)據(jù)庫(kù)連接的方面。它用一個(gè)圖來(lái)關(guān)聯(lián)數(shù)據(jù)庫(kù)名稱(chēng)和每個(gè) JDBC 連接。

          在 jdbcConnectionMonitor 內(nèi)部

          在清單 6 顯示的 JdbcConnectionMonitor 內(nèi)部,我定義了切入點(diǎn),捕捉連接數(shù)據(jù)庫(kù)的兩種不同方式:通過(guò)數(shù)據(jù)源或直接通過(guò) JDBC URL。連接監(jiān)視器包含針對(duì)每種情況的監(jiān)視建議,兩種情況都設(shè)置一個(gè)工人對(duì)象。doExecute() 方法啟動(dòng)時(shí)處理原始連接,然后把返回的連接傳遞給兩個(gè)輔助方法中名為 addConnection 的一個(gè)。在兩種情況下,被建議的切入點(diǎn)會(huì)排除來(lái)自另一個(gè)連接的連接調(diào)用(例如,如果要連接到數(shù)據(jù)源,會(huì)造成建立 JDBC 連接)。

          數(shù)據(jù)源的 addConnection() 委托輔助方法 getNameForDataSource() 從數(shù)據(jù)源確定數(shù)據(jù)庫(kù)的名稱(chēng)。DataSource 接口不提供任何這類(lèi)機(jī)制,但是幾乎每個(gè)實(shí)現(xiàn)都提供了 getDatabaseName() 方法。getNameForDataSource() 用反射來(lái)嘗試完成這項(xiàng)工作和其他少數(shù)常見(jiàn)(和不太常見(jiàn))的方法,為數(shù)據(jù)庫(kù)源提供一個(gè)有用的標(biāo)識(shí)。addConnection() 方法然后委托給 addConnection() 方法,這個(gè)方法用字符串參數(shù)作為名稱(chēng)。

          被委托的 addConnection() 方法從父請(qǐng)求的上下文中檢索可以操作的統(tǒng)計(jì)值,并根據(jù)與指定連接關(guān)聯(lián)的數(shù)據(jù)庫(kù)名稱(chēng)(或其他描述字符串)查詢(xún)數(shù)據(jù)庫(kù)的統(tǒng)計(jì)值。然后它把這條信息保存在請(qǐng)求上下文對(duì)象的 dbStats 字段中,更新關(guān)于獲得連接的性能信息。這樣就可以跟蹤連接數(shù)據(jù)庫(kù)需要的時(shí)間(通常這實(shí)際是從池中得到連接所需要的時(shí)間)。addConnection() 方法也更新到數(shù)據(jù)庫(kù)名稱(chēng)的連接的連接圖。隨后在執(zhí)行 JDBC 語(yǔ)句更新對(duì)應(yīng)請(qǐng)求的統(tǒng)計(jì)值時(shí),會(huì)使用這個(gè)圖。JdbcConnectionMonitor 還提供了一個(gè)輔助方法 getDatabaseName(),它從連接圖中查詢(xún)字符串名稱(chēng)找到連接。

          弱標(biāo)識(shí)圖和方面

          JDBC 監(jiān)視方面使用 弱標(biāo)識(shí) 哈希圖。這些圖持有 引用,允許連接這樣的被跟蹤對(duì)象在只有方面引用它們的時(shí)候,被垃圾收集掉。這一點(diǎn)很重要,因?yàn)閱误w的方面通常 不會(huì) 被垃圾收集。如果引用不弱,那么應(yīng)用程序會(huì)有內(nèi)存泄漏。方面用 標(biāo)識(shí) 圖來(lái)避免調(diào)用連接或語(yǔ)句的hashCodeequals 方法。這很重要,因?yàn)槲蚁敫欉B接和語(yǔ)句,而不理會(huì)它們的狀態(tài):我不想遇到來(lái)自 hashCode 方法的異常,也不想在對(duì)象的內(nèi)部狀態(tài)已經(jīng)改變時(shí)(例如關(guān)閉時(shí)),指望對(duì)象的哈希碼保持不變。我在處理動(dòng)態(tài)的基于代理的 JDBC 對(duì)象(就像來(lái)自 iBatis 的那些對(duì)象)時(shí)遇到了這個(gè)問(wèn)題:在連接已經(jīng)關(guān)閉之后調(diào)用對(duì)象上的方法就會(huì)拋出異常。在完成操作之后還想記錄統(tǒng)計(jì)值時(shí)會(huì)造成錯(cuò)誤。

          從這里可以學(xué)到的教訓(xùn)是:把對(duì)第三方代碼的假設(shè)最小化。使用標(biāo)識(shí)圖是避免對(duì)接受建議的代碼的實(shí)現(xiàn)邏輯進(jìn)行猜測(cè)的好方法。在這種情況下,我使用了來(lái)自 DCL Java 工具的 WeakIdentityHashMap 開(kāi)放源碼實(shí)現(xiàn)(請(qǐng)參閱 參考資料)。 跟蹤連接或語(yǔ)句的元數(shù)據(jù)信息讓我可以跨越請(qǐng)求,針對(duì)連接或語(yǔ)句把統(tǒng)計(jì)值分組。這意味著可以只根據(jù)對(duì)象實(shí)例進(jìn)行跟蹤,而不需要使用對(duì)象等價(jià)性來(lái)跟蹤這些 JDBC 對(duì)象。另一個(gè)要記住的教訓(xùn)是:不同的對(duì)象經(jīng)常用不同的修飾器包裝(越來(lái)越多地采用動(dòng)態(tài)代理) JDBC 對(duì)象。所以假設(shè)要處理的是這類(lèi)接口的簡(jiǎn)單而原始的實(shí)現(xiàn),可不是一個(gè)好主意!

          jdbcStatementMonitor 內(nèi)部

          清單 7 顯示了 JdbcStatementMonitor 方面。這個(gè)方面有兩個(gè)主要職責(zé):跟蹤與創(chuàng)建和準(zhǔn)備語(yǔ)句有關(guān)的信息,然后監(jiān)視 JDBC 語(yǔ)句執(zhí)行的性能統(tǒng)計(jì)值。


          清單 7. JdbcStatementMonitor 方面

          /**
          * Monitor performance for executing JDBC statements,
          * and track the connections used to create them,
          * and the SQL used to prepare them (if appropriate).
          */
          public aspect JdbcStatementMonitor extends AbstractRequestMonitor {

          /** Matches any execution of a JDBC statement */
          public pointcut statementExec(Statement statement) :
          call(* java.sql..*.execute*(..)) &&
          target(statement);

          /**
          * Store the sanitized SQL for dynamic statements.
          */
          before(Statement statement, String sql,
          RequestContext parentContext):
          statementExec(statement) && args(sql, ..)
          && inRequest(parentContext) {
          sql = stripAfterWhere(sql);
          setUpStatement(statement, sql, parentContext);
          }

          /** Monitor performance for executing a JDBC statement. */
          Object around(final Statement statement) :
          statementExec(statement) {
          RequestContext requestContext =
          new StatementRequestContext() {
          public Object doExecute() {
          return proceed(statement);
          }
          };
          return requestContext.execute();
          }

          /**
          * Call to create a Statement.
          * @param connection the connection called to
          * create the statement, which is bound to
          * track the statement's origin
          */
          public pointcut callCreateStatement(Connection connection):
          call(Statement+ Connection.*(..))
          && target(connection);

          /**
          * Track origin of statements, to properly
          * associate statistics even in
          * the presence of wrapped connections
          */
          after(Connection connection) returning (Statement statement):
          callCreateStatement(connection) {
          synchronized (JdbcStatementMonitor.this) {
          statementCreators.put(statement, connection);
          }
          }

          /**
          * A call to prepare a statement.
          * @param sql The SQL string prepared by the statement.
          */
          public pointcut callCreatePreparedStatement(String sql):
          call(PreparedStatement+ Connection.*(String, ..))
          && args(sql, ..);

          /** Track SQL used to prepare a prepared statement */
          after(String sql) returning (PreparedStatement statement):
          callCreatePreparedStatement(sql) {
          setUpStatement(statement, sql);
          }

          protected abstract class StatementRequestContext
          extends RequestContext {
          /**
          * Find statistics for this statement, looking for its
          * SQL string in the parent request's statistics context
          */
          protected PerfStats lookupStats() {
          if (getParent() != null) {
          Connection connection = null;
          String sql = null;

          synchronized (JdbcStatementMonitor.this) {
          connection =
          (Connection) statementCreators.get(statement);
          sql = (String) statementSql.get(statement);
          }

          if (connection != null) {
          String databaseName =
          JdbcConnectionMonitor.aspectOf().
          getDatabaseName(connection);
          if (databaseName != null && sql != null) {
          OperationStats opStats =
          (OperationStats) getParent().getStats();
          if (opStats != null) {
          ResourceStats dbStats =
          opStats.getDatabaseStats(databaseName);

          return dbStats.getRequestStats(sql);
          }
          }
          }
          }
          return null;
          }
          }

          /**
          * To group sensibly and to avoid recording sensitive data,
          * I don't record the where clause (only used for dynamic
          * SQL since parameters aren't included
          * in prepared statements)
          * @return subset of passed SQL up to the where clause
          */
          public static String stripAfterWhere(String sql) {
          for (int i=0; i<sql.length()-4; i++) {
          if (sql.charAt(i)=='w' || sql.charAt(i)==
          'W') {
          if (sql.substring(i+1, i+5).equalsIgnoreCase(
          "here"))
          {
          sql = sql.substring(0, i);
          }
          }
          }
          return sql;
          }

          private synchronized void
          setUpStatement(Statement statement, String sql) {
          statementSql.put(statement, sql);
          }

          /** associate statements with the connections
          * called to create them
          */
          private Map/*<Statement,Connection>*/ statementCreators =
          new WeakIdentityHashMap();

          /** associate statements with the
          * underlying string they execute
          */
          private Map/*<Statement,String>*/ statementSql =
          new WeakIdentityHashMap();
          }

          JdbcStatementMonitor 維護(hù)兩個(gè)弱標(biāo)識(shí)圖:statementCreatorsstatementSql。第一個(gè)圖跟蹤用來(lái)創(chuàng)建語(yǔ)句的連接。正如前面提示過(guò)的,我不想依賴(lài)這條語(yǔ)句的 getConnection 方法,因?yàn)樗鼤?huì)引用一個(gè)包裝過(guò)的連接,而我沒(méi)有這個(gè)連接的元數(shù)據(jù)。請(qǐng)注意 callCreateStatement 切入點(diǎn),我建議它去監(jiān)視 JDBC 語(yǔ)句的執(zhí)行。這個(gè)建議匹配的方法調(diào)用是在 JDBC 連接上定義的,而且會(huì)返回 Statement 或任何子類(lèi)。這個(gè)建議可以匹配 JDBC 中 12 種不同的可以創(chuàng)建或準(zhǔn)備語(yǔ)句的方式,而且是為了適應(yīng) JDBC API 未來(lái)的擴(kuò)展而設(shè)計(jì)的。

          statementSql 圖跟蹤指定語(yǔ)句執(zhí)行的 SQL 字符串。這個(gè)圖用兩種不同的方式更新。在創(chuàng)建預(yù)備語(yǔ)句(包括可調(diào)用語(yǔ)句)時(shí),在創(chuàng)建時(shí)捕捉到 SQL 字符串參數(shù)。對(duì)于動(dòng)態(tài) SQL 語(yǔ)句,SQL 字符串參數(shù)在監(jiān)視建議使用它之前,從語(yǔ)句執(zhí)行調(diào)用中被捕捉。(建議的先后次序在這里沒(méi)影響;雖然是在執(zhí)行完成之后才用建議查詢(xún)統(tǒng)計(jì)值,但字符串是在執(zhí)行發(fā) 生之前捕捉的。)

          語(yǔ)句的性能監(jiān)視由一個(gè) around 建議處理,它在執(zhí)行 JDBC 語(yǔ)句的時(shí)候設(shè)置工人對(duì)象。執(zhí)行 JDBC 語(yǔ)句的 statementExec 切入點(diǎn)會(huì)捕捉 JDBC Statement(包括子類(lèi))實(shí)例上名稱(chēng)以 execute 開(kāi)始的任何方法的調(diào)用,方法是在 JDBC API 中定義的(也就是說(shuō),在任何名稱(chēng)以 java.sql 開(kāi)始的包中)。

          工人對(duì)象上的 lookupStats() 方法使用雙親(servlet)的統(tǒng)計(jì)上下文來(lái)查詢(xún)指定連接的數(shù)據(jù)庫(kù)統(tǒng)計(jì)值,然后查詢(xún)指定 SQL 字符串的 JDBC 語(yǔ)句統(tǒng)計(jì)值。直接的語(yǔ)句執(zhí)行方法包括:SQL 語(yǔ)句中在 where 子句之后剝離數(shù)據(jù)的附加邏輯。這就避免了暴露敏感數(shù)據(jù)的風(fēng)險(xiǎn),而且也允許把常見(jiàn)語(yǔ)句分組。更復(fù)雜的方式就是剝離查詢(xún)參數(shù)而已。但是,多數(shù)應(yīng)用程序使用預(yù)備語(yǔ)句而不是動(dòng)態(tài) SQL 語(yǔ)句,所以我不想深入這一部分。



          回頁(yè)首


          跟蹤 JDBC 信息

          在結(jié)束之前,關(guān)于監(jiān)視方面如何解決跟蹤 JDBC 信息的挑戰(zhàn),請(qǐng)靜想一分鐘。JdbcConnectionMonitor 讓我把數(shù)據(jù)庫(kù)的文本描述(例如 JDBC URL)與用來(lái)訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)的連接關(guān)聯(lián)起來(lái)。同樣,JdbcStatementMonitor 中的 statementSql 映射跟蹤 SQL 字符串(甚至是用于預(yù)備語(yǔ)句的字符串),從而確??梢杂糜幸饬x的名稱(chēng),把執(zhí)行的查詢(xún)分成有意義的組。最后,JdbcStatementMonitor 中的 statementCreators 映射讓我把語(yǔ)句與我擁有句柄(而不是包裝過(guò))的連接關(guān)聯(lián)。這種方式整合了多個(gè)建議,在把方面應(yīng)用到現(xiàn)實(shí)問(wèn)題時(shí),更新內(nèi)部狀態(tài)非常有用。在許多情況下,需要跟蹤來(lái)自 一系列 切入點(diǎn)的上下文信息,在單一公開(kāi)上下文的 AspectJ 切入點(diǎn)中無(wú)法捕捉到這個(gè)信息。在出現(xiàn)這種情況時(shí),一個(gè)切入點(diǎn)的跟蹤狀態(tài)可以在后一個(gè)切入點(diǎn)中使用這項(xiàng)技術(shù)就會(huì)非常有幫助。

          這個(gè)信息可用之后,JdbcStatementMonitor 就能夠很自然地監(jiān)視性能了。在語(yǔ)句執(zhí)行切入點(diǎn)上的實(shí)際建議只是遵循標(biāo)準(zhǔn)方法 ,創(chuàng)建工人對(duì)象繼續(xù)處理原始的計(jì)算。lookupStats() 方法使用這三個(gè)不同的映射來(lái)查詢(xún)與這條語(yǔ)句關(guān)聯(lián)的連接和 SQL。然后它用它的雙親請(qǐng)求,根據(jù)連接的描述找到正確的數(shù)據(jù)庫(kù)統(tǒng)計(jì)值,并根據(jù) SQL 鍵字符串找到語(yǔ)句統(tǒng)計(jì)值。lookupStats() 是防御性的,也就是說(shuō)它在應(yīng)用程序的使用違背預(yù)期的時(shí)候,會(huì)檢查 null 值。在這篇文章的第二部分,我將介紹如何用 AOP 系統(tǒng)地保證監(jiān)視代碼不會(huì)在被監(jiān)視的應(yīng)用程序中造成問(wèn)題。



          回頁(yè)首


          第 1 部分結(jié)束語(yǔ)

          迄今為止,我構(gòu)建了一個(gè)核心的監(jiān)視基礎(chǔ)設(shè)施,可以系統(tǒng)地跟蹤應(yīng)用程序的性能、測(cè)量 servlet 操作中的數(shù)據(jù)庫(kù)活動(dòng)。監(jiān)視代碼可以自然地插入 JMX 接口來(lái)公開(kāi)結(jié)果,如圖 5 所示。代碼已經(jīng)能夠監(jiān)視重要的應(yīng)用程序邏輯,您也已經(jīng)看到了擴(kuò)展和更新監(jiān)視方式有多容易。


          圖 5. 監(jiān)視數(shù)據(jù)庫(kù)結(jié)果
          監(jiān)視數(shù)據(jù)庫(kù)結(jié)果

          雖 然這里提供的代碼相當(dāng)簡(jiǎn)單,但卻是對(duì)傳統(tǒng)方式的巨大修改。AspectJ 模塊化的方式讓我可以精確且一致地處理監(jiān)視功能。比起在整個(gè)示例應(yīng)用程序中用分散的調(diào)用更新統(tǒng)計(jì)值和跟蹤上下文,這是一個(gè)重大的改進(jìn)。即使使用對(duì)象來(lái)封裝 統(tǒng)計(jì)跟蹤,傳統(tǒng)的方式對(duì)于每個(gè)用戶(hù)操作和每個(gè)資源訪(fǎng)問(wèn),也都需要多個(gè)調(diào)用。實(shí)現(xiàn)這樣的一致性會(huì)很繁瑣,也很難一次實(shí)現(xiàn),更不用說(shuō)維護(hù)了。

          在 這篇文章的第二部分中,我將把重點(diǎn)放在開(kāi)發(fā)和部署基于 AOP 的性能監(jiān)視系統(tǒng)的編程問(wèn)題上。我將介紹如何用 AspectJ 5 的裝入時(shí)編織來(lái)監(jiān)視運(yùn)行在 Apache Tomcat 中的多個(gè)應(yīng)用程序,包括在第三方庫(kù)中進(jìn)行監(jiān)視。我將介紹如何測(cè)量監(jiān)視的開(kāi)銷(xiāo),如何選擇性地在運(yùn)行時(shí)啟用監(jiān)視,如何測(cè)量裝入時(shí)編織的性能和內(nèi)存影響。我還會(huì) 介紹如何用方面防止監(jiān)視代碼中的錯(cuò)誤造成應(yīng)用程序錯(cuò)誤。最后,我將擴(kuò)展 Glassbox Inspector,讓它支持 Web 服務(wù)和常見(jiàn)的 Web 應(yīng)用程序框架(例如 Struts 和 Spring )并跟蹤應(yīng)用程序錯(cuò)誤。歡迎繼續(xù)閱讀!

          致謝

          感謝 Ramnivas Laddad、Will Edwards、Matt Hutton、David Pickering、Rob Harrop、Alex Vasseur、Paul Sutter、Mik Kersten 和 Eugene Kuleshov 審閱本文并給出非常深刻的評(píng)價(jià)。


          回頁(yè)首


          下載

          描述名字大小 下載方法
          Sample codej-aopwork10-source.zip75 KB  FTP
          關(guān)于下載方法的信息獲取 Adobe? Reader?


          回頁(yè)首


          參考資料

          學(xué)習(xí)

          獲得產(chǎn)品和技術(shù)

          討論


          回頁(yè)首


          關(guān)于作者


          Ron Bodkin 是 New Aspects of Software 的創(chuàng)始人,該公司提供應(yīng)用程序開(kāi)發(fā)和架構(gòu)方面的咨詢(xún)和培訓(xùn),側(cè)重于性能管理和有效地使用面向方面編程。Ron 以前在 Xerox PARC 為 AspectJ 小組工作,在那里他領(lǐng)導(dǎo)了第一個(gè) AOP 實(shí)現(xiàn)項(xiàng)目并負(fù)責(zé)客戶(hù)的培訓(xùn),他還是 C-bridge 的創(chuàng)始人和 CTO,這家咨詢(xún)機(jī)構(gòu)采用 Java 的框架、XML 和其他 Internet 技術(shù)提供企業(yè)應(yīng)用程序。Ron 經(jīng)常為各種會(huì)議和客戶(hù)進(jìn)行演講和提供教程,包括在 Software Development、The Colorado Software Summit、 TheServerSide Symposium、EclipseCon、StarWest、Software Test & Performance、OOPSLA 和 AOSD 上做報(bào)告。最近,他一直為 Glassbox 集團(tuán)工作,用 AspectJ 和 JMX 開(kāi)發(fā)應(yīng)用程序性能管理和分析產(chǎn)品??梢酝ㄟ^(guò) Ron 的郵箱 ron.bodkin@newaspects.com 與他聯(lián)系。

          posted on 2005-12-19 21:52 Dion 閱讀(1108) 評(píng)論(0)  編輯  收藏 所屬分類(lèi): 企業(yè)架構(gòu)模式
          主站蜘蛛池模板: 赤峰市| 岢岚县| 景德镇市| 孝感市| 永胜县| 石渠县| 大姚县| 庆阳市| 汉源县| 大英县| 铜梁县| 临安市| 秦安县| 钟山县| 旌德县| 嵊泗县| 宿迁市| 伽师县| 团风县| 武夷山市| 方山县| 沙洋县| 赫章县| 昌黎县| 阳新县| 电白县| 离岛区| 鹤岗市| 惠安县| 伊川县| 临沭县| 丰都县| 溆浦县| 罗田县| 常州市| 许昌市| 塘沽区| 保山市| 海南省| 云霄县| 陈巴尔虎旗|