級別: 中級 Ron Bodkin , 創始人, New Aspects of Software
2005 年 10 月 08 日 隨
著 Ron Bodkin 介紹如何把 AspectJ 和 JMX
組合成靈活而且模塊化的性能監視方式,就可以對散亂而糾纏不清的代碼說再見了。在這篇文章(共分兩部分)的第一部分中,Ron 用來自開放源碼項目
Glassbox Inspector
的代碼和想法幫助您構建一個監視系統,它提供的相關信息可以識別出特定問題,但是在生產環境中使用的開銷卻足夠低。  | 關于這個系列
AOP@Work系列面對的是在面向方面編程上有些基礎,想擴展或加深了解的開發人員。同 developerWorks 上的大多數文章一樣,這個系列高度實用:讀完每篇介紹新技術的文章,都可以立即投入實用。 這個系列的每個作者在面向方面編程領域都具有領袖地位或專家水平。許多作者都是系列中介紹的項目和工具的參與者。每篇文章都力圖提供一個中立的評述,以確保這里表達的觀點是公正且正確的。 如果有對每個作者文章的評論或問題,請分別與他們聯系。要對這個系列整體進行評論,可以與系列的負責人 Nicholas
Lesiecki 聯系。請參閱 參考資料 獲取關于 AOP 的更多背景資料。 |
|
現
代的 Java?
應用程序通常是采用許多第三方組件的復雜的、多線程的、分布式的系統。在這樣的系統上,很難檢測(或者分離出)性能問題或可靠性問題的根本原因,尤其是生
產中的問題。對于問題容易重現的情況來說,profiler
這類傳統工具可能有用,但是這類工具帶來的開銷造成在生產環境、甚至負載測試環境中使用它們是不現實的。 監視和檢查應用程序和故障常見的一
個備選策略是,為性能的關鍵代碼提供有關調用,記錄使用情況、計時以及錯誤情況。但是,這種方式要求在許多地方分散重復的代碼,而且要測量哪些代碼也需要
經過許多試驗和錯誤才能確定。當系統變化時,這種方式既難維護,也很難深入進去。這造成日后要求對性能需求有更好理解的時候,添加或修改應用程序的代碼變
得很困難。簡單地說,系統監視是經典的橫切關注點,因此任何非模塊化的實現都會讓它混亂。 學習這篇分兩部分的文章就會知道,面向方面編程(AOP)很自然地適合解決系統監視問題。AOP 允許定義切入點,與要監視性能的許多連接點進行匹配。然后可以編寫建議,更新性能統計,而在進入或退出任何一個連接點時,都會自動調用建議。 在本文的這半部分,我將介紹如何用 AspectJ 和 JMX 創建靈活的、面向方面的監視基礎設施。我要使用的監視基礎設施是開放源碼的 Glassbox Inspector 監視框架(請參閱 參考資料)的核心。它提供了相關的信息,可以幫助識別特定的問題,但是在生產環境中使用的開銷卻足夠小。它允許捕捉請求的總數、總時間以及最差情況性能之類的統計值,還允許深入請求中數據庫調用的信息。而它做的所有這些,僅僅是在一個中等規模的代碼基礎內完成的! 在這篇文章和下一篇文章中,我將從構建一個簡單的 Glassbox Inspector 實現開始,并逐漸添加功能。圖 1 提供了這個遞增開發過程的最終系統的概貌。請注意這個系統的設計是為了同時監視多個 Web 應用程序,并提供合并的統計結果。 圖 1. 帶有 JConsole JMX 客戶端的 Glassbox Inspector

圖
2 是監視系統架構的概貌。方面與容器內的一個或多個應用程序交互,捕捉性能數據,然后用 JMX Remote
標準把數據提出來。從架構的角度來看,Glassbox Inspector
與許多性能監視系統類似,區別在于它擁有定義良好的實現了關鍵監視功能的模塊。 圖 2. Glassbox Inspector 架構

Java
管理擴展(JMX)是通過查看受管理對象的屬性來管理 Java 應用程序的標準 API。JMX Remote 標準擴展了
JMX,允許外部客戶進程管理應用程序。JMX 管理是 Java 企業容器中的標準特性。現有多個成熟的第三方 JMX 庫和工具,而且 JMX
支持在 Java 5 中也已經集成進核心 Java 運行時。Sun 公司的 Java 5 虛擬機包含 JConsole JMX 客戶端。 在繼續本文之前,應當下載 AspectJ、JMX 和 JMX Remote 的當前版本以及本文的源代碼包(請參閱 參考資料 獲得技術內容,參閱下載 獲得代碼)。如果正在使用 Java 5 虛擬機,那么內置了 JMX。請注意源代碼包包含開放源碼的 Glassbox Inspector 性能監視基礎設施 1.0 alpha 發行版的完整最終代碼。 基本的系統 我將從一個基本的面向方面的性能監視系統開始。這個系統可以捕捉處理 Web 請求的不同 servlet 的時間和計數。清單 1 顯示了一個捕捉這個性能信息的簡單方面: 清單 1. 捕捉 servlet 時間和計數的方面
/** * 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); }
|
可以看到,第一個版本相當基礎。HttpServletMonitor 定義了一個切入點,叫作 monitoredOperation ,它匹配 HttpServlet 接口上任何名稱以 do 開始的方法的執行。這些方法通常是 doGet() 和 doPost() ,但是通過匹配 doHead() 、doDelete() 、doOptions() 、doPut() 和 doTrace() ,它也可以捕捉不常用的 HTTP 請求選項。  | 管理開銷
在
這篇文章的后半部分,我將把重點放在管理監視框架開銷的技術上,但是現在,值得注意的是基本策略:在速度慢的事情發生時(像訪問 servlet
或數據庫),我要做一些在內存中的操作,這只花幾毫秒。在實踐中,對大多數應用程序的端對端響應時間只會添加微不足道的開銷。 |
|
每當其中一個操作執行的時候,系統都會執行
around 通知去監視性能。建議啟動一個秒表,然后讓原始請求繼續進行。之后,通知停止秒表并查詢與指定操作對應的性能統計對象。然后它再調用 PerfStats 接口的 recordExecution() ,記錄操作經歷的時間。這僅僅更新指定操作的總時間、最大時間(如果適用)以及執行次數。自然也可以把這種方式擴展成計算額外的統計值,并在問題可能發生的地方保存單獨的數據點。 我在方面中使用了一個哈希圖為每種操作處理程序保存累計統計值。在這個版本中,操作處理程序是 HttpServlet 的子類,所以 servlet 的類被用作鍵。我還用術語 操作
表示 Web
請求,以便把它與應用程序可能產生的其他請求(例如,數據庫請求)區分開。在這篇文章的第二部分,我將擴展這種方式,來解決更常見的在控制器中使用的基于
類或方法的跟蹤操作情況,例如 Apache Struts 的動作類或 Spring 的多動作控制器方法。
公開性能數據  | 線程安全性
Glassbox Inspector 監視系統的統計值捕捉代碼不是線程安全的。我寧愿維護(可能)略微不準確的統計值(由于多個線程很少會同時訪問一個 PerfStats
實例),也不想向程序執行添加額外的同步。如果您偏愛更高的準確性,也只要讓互斥體同步即可(例如,與方面同步)。如果正在跟蹤的累計時間超過 32
位的長度,那么同步會很重要,因為 Java 平臺不保證對 64 位數據的原子更新。但是,在毫秒的精度情況下,32 位的長度會提供 46
天的累計時間。我建議對于真實的應用,應當更加頻繁地搜集和重設統計值,所以我堅持使用 int 值。 |
|
一旦捕捉到了性能數據,讓它可以使用的方式就很多了。最簡單的方式就是把信息定期地寫入日志文件。也可以把信息裝入數據庫進行分析。由于不增加延遲、復雜性以及合計、日志及處理信息的開銷,提供到即時系統數據的直接訪問通常會更好。在下一節中我將介紹如何做到這一點。 我
想使用一個現有管理工作能夠顯示和跟蹤的標準協議,所以我將用 JMX API 來共享性能統計值。使用 JMX
意味著每個性能統計實例都會公開成一個管理 bean,從而提供詳細的性能數據。標準的 JMX 客戶端(像 Sun 公司的
JConsole)也能夠顯示這些信息。請參閱 參考資料 學習有關 JMX 的更多內容。
圖 3 是一幅 JConsole 的截屏,顯示了 Glassbox Inspector 監視 Duke 書店示例應用程序性能的情況。(請參閱 參考資料)。清單 2 顯示了實現這個特性的代碼。 圖 3. 用 Glassbox Inspector 查看操作統計值

傳統上,支持 JMX 包括用樣本代碼實現模式。在這種情況下,我將把 JMX 與 AspectJ 結合,這個結合可以讓我獨立地編寫管理邏輯。 清單 2. 實現 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 工具
有
幾個比較好的 JMX 實現庫支持遠程 JMX。Sun 公司在免費許可下提供了 JMX 和 JMX Remote
的參考實現。也有一些開放源碼的實現。MX4J 是其中比較流行的一個,它包含輔助庫和工具(像 JMX 客戶端)。Java 5 把 JMX 和
JMX 遠程支持集成進了虛擬機。Java 5 還在 javax.management 包中引入了虛擬機性能的管理 bean。Sun 的 Java 5 虛擬機包括標準的 JMX 客戶端 JConsole。 |
|
可以看出這個第一個方面是可以重用的。利用它,我能夠用 after 建議自動為任何實現 ManagedBean 接口的類登記對象實例。這與 AspectJ 標記器接口的理念類似(請參閱 參考資料):定義了實例應當通過 JMX 公開的類。但是,與真正的標記器接口不同的是,它還定義了兩個方法 。 這
個方面提供了一個設置器,定義應當用哪個 MBean
服務器管理對象。這是一個使用反轉控制(IOC)模式進行配置的示例,因此很自然地適合方面。在最終代碼的完整清單中,將會看到我用了一個簡單的輔助方面
對系統進行配置。在更大的系統中,我將用 Spring 框架這樣的 IOC 容器來配置類和方面。請參閱 參考資料 獲得關于 IOC 和 Spring 框架的更多信息,并獲得關于使用 Spring 配置方面的介紹。 清單 3. 公開負責 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 方面,它具體地定義了哪個對象應當公開管理 bean。它描述了一個接口 PerfStatsMBean ,這個接口定義了用于任何性能統計實現的管理接口。其中包括計數、總時間、最大時間的統計值,還有重設操作,這個接口是 PerfStats 接口的子集。 PerfStatsMBean 本身擴展了 ManagedBean ,所以它的任何實現都會自動被 JmxManagement 方面登記成進行管理。我采用 AspectJ 的 declare parents 格式讓 PerfStats 接口擴展了一個特殊的管理接口 PerfStatsMBean 。結果是 JMX
Dynamic MBean 技術會管理這些對象,與使用 JMX 的標準 MBean 相比,我更喜歡這種方式。
使用標準 MBean 會要求定義一個管理接口,接口名稱基于每個性能統計的實現類,例如 PerfStatsImplMBean 。后來,當我向 Glassbox Inspector 添加 PerfStats 的子類時,情況變糟了,因為我被要求創建對應的接口(例如 OperationPerfStatsImpl )。標準 MBean 的約定使得接口依賴于實現,而且代表這個系統的繼承層次出現不必要的重復。  | 部署這些方面
這
篇文章中使用的方面只能應用到它們監視的每個應用程序上,不能應用到第三方庫或容器代碼上。所以,如果要把它們集成到生產系統中,可以把它們編譯到應用程
序中,或者編織到已經編譯的應用程序中,或者使用裝入時編織(這是這種用例下我偏愛的方式)。在這篇文章的第二部分,您將學到有關裝入時編程的更多內容。 |
|
這個方面剩下的部分負責用 JMX 創建正確的 MBean 和對象名稱。我重用了來自 Spring 框架的 JMX 工具 InterfaceBasedMBeanInfoAssembler ,用它可以更容易地創建 JMX DynamicMBean(用 PerfStatsMBean 接口管理
PerfStats 實例)。在這個階段,我只公開了 PerfStats 實現。這個方面還用受管理 bean 類上的類型間聲明定義了輔助方法。如果這些類中的任何一個的子類需要覆蓋默認行為,那么可以通過覆蓋這個方法實現。 您可能想知道為什么我用方面進行管理而不是直接把支持添加到 PerfStatsImpl 的實現類中。雖然把管理添加到這個類中不會把代碼分散,但是它會把性能監視系統的實現與 JMX 混雜在一起。所以,如果我想把這個系統用在一個 沒有 JMX 的系統中,就要被迫包含 JMX 的庫,還要禁止有關服務。而且,當擴展系統的管理功能時,我還要公開更多的類用 JMX 進行管理。使用方面可以讓系統的管理策略保持模塊化。
數據庫請求監視 分
布式調用是應用程序性能低和出錯誤的一個常見源頭。多數基于 Web
的應用程序要做相當數量的數據庫工作,所以對查詢和其他數據庫請求進行監視就成為性能監視中特別重要的領域。常見的問題包括編寫得有毛病的查詢、遺漏了索
引以及每個操作中過量的數據庫請求。在這一節,我將對監視系統進行擴展,跟蹤數據庫中與操作相關的活動。  | 分布式調用
在
這一節,我介紹了一種處理數據庫分布式調用的方式。雖然數據庫通常位于不同的機器上,但我的技術也適用于本地數據庫。我的方式也可以自然地擴展到其他分布
式資源上,包括遠程對象調用。在這篇文章的第二部分中,我將介紹如何用 SOAP 把這項技術應用到 Web 服務調用上。 |
|
開
始時,我將監視數據庫的連接次數和數據庫語句的執行。為了有效地支持這個要求,我需要歸納性能監視信息,并允許跟蹤嵌套在一個操作中的性能。我想把性能的
公共元素提取到一個抽象基類。每個基類負責跟蹤某項操作前后的性能,還需要更新系統范圍內這條信息的性能統計值。這樣我就能跟蹤嵌套的 servlet
請求,對于在 Web 應用程序中支持對控制器的跟蹤,這也會很重要(在第二部分討論)。 因為我想根據請求更新數據庫的性能,所以我將采用 composite pattern
跟蹤由其他統計值持有的統計值。這樣,操作(例如
servelt)的統計值就持有每個數據庫的性能統計。數據庫的統計值持有有關連接次數的信息,并聚合每個單獨語句的額外統計值。圖 4
顯示整體設計是如何結合在一起的。清單 4 擁有新的基監視方面,它支持對不同的請求進行監視。 圖 4. 一般化后的監視設計

清單 4. 基監視方面
/** 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; } } }
|
不出所料,對于如何存儲共享的性能統計值和基方面的每請求狀態,有許多選擇。例如,我可以用帶有更底層機制的單體(例如 ThreadLocal )持有一堆統計值和上下文。但是,我選用了工人對象(Worker Object)模式(請參閱 參考資料),
因為它支持更加模塊化、更簡潔的表達。雖然這會帶來一些額外的開銷,但是分配單一對象并執行建議所需要的額外時間,比起為 Web
和數據庫請求提供服務來說,通常是微不足道的。換句話說,我可以在不增加開銷的情況下,在監視代碼中做一些處理工作,因為它運行的頻繁相對很低,而且比起
在通過網絡發送信息和等候磁盤 I/O 上花費的時間來說,通常就微不足道了。對于 profiler 來說,這可能是個糟糕的設計,因為在
profiler 中可能想要跟蹤每個請求中的許多操作(和方法)的數據。但是,我是在做請求的統計匯總,所以這個選擇是合理的。 在上面的基方面中,我把當前被監視請求的中間狀態保存在匿名內部類中。這個工人對象用來包裝被監視請求的執行。工人對象 RequestContext 是在基類中定義的,提供的 final execute 方法定義了對請求進行監視的流程。execute 方法委托抽象的模板方法 doExecute() 負責繼續處理原始的連接點。在 doExecute() 方法中也適合在根據上下文信息(例如正在連接的數據源)繼續處理被監視的連接點之前設置統計值,并在連接點返回之后關聯返回的值(例如數據庫連接)。 每個監視方面還負責提供抽象方法 lookupStats() 的實現,用來確定為指定請求更新哪個統計對象。lookupStats() 需要根據被監視的連接點訪問信息。一般來說,捕捉的上下文對于每個監視方面都應當各不相同。例如,在 HttpServletMonitor
中,需要的上下文就是目前執行操作對象的類。對于 JDBC
連接,需要的上下文就是得到的數據源。因為要求根據上下文而不同,所以設置工人對象的建議最好是包含在每個子方面中,而不是在抽象的基方面中。這種安排更
清楚,它支持類型檢測,而且也比在基類中編寫一個建議,再把 JoinPoint 傳遞給所有孩子執行得更好。
servlet 請求跟蹤 AbstractRequestMonitor 確實包含一個具體的 after 建議,負責跟蹤請求上下文的雙親上下文。這就讓我可以把嵌套請求的操作統計值與它們雙親的統計值關聯起來(例如,哪個 servlet 請求造成了這個數據庫訪問)。對于示例監視系統來說,我明確地 需要 嵌套的工人對象,而 不想 把自己限制在只能處理頂級請求上。例如,所有的 Duke 書店 servlet 都把調用 BannerServlet 作為顯示頁面的一部分。所以能把這些調用的次數分開是有用的,如清單 5 所示。在這里,我沒有顯示在操作統計值中查詢嵌套統計值的支持代碼(可以在本文的源代碼中看到它)。在第二部分,我將重新回到這個主題,介紹如何更新 JMX 支持來顯示像這樣的嵌套統計值。
清單 5. 更新的 servlet 監視
清單 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 顯示了修訂后進行 serverlet 請求跟蹤的監視建議。余下的全部代碼與 清單 1 相同:或者推入基方面 AbstractRequestMonitor 方面,或者保持一致。
JDBC 監視 設置好性能監視框架后,我現在準備跟蹤數據庫的連接次數以及數據庫語句的時間。而且,我還希望能夠把數據庫語句和實際連接的數據庫關聯起來(在 lookupStats() 方法中)。為了做到這一點,我創建了兩個跟蹤 JDBC 語句和連接信息的方面:
JdbcConnectionMonitor 和
JdbcStatementMonitor 。 這些方面的一個關鍵職責是跟蹤對象引用的鏈。我想根據我用來連接數
據庫的 URI 跟蹤請求,或者至少根據數據庫名稱來跟蹤。這就要求跟蹤用來獲得連接的數據源。我還想進一步根據 SQL
字符串跟蹤預備語句(在執行之前就已經準備就緒)。最后,我需要跟蹤與正在執行的語句關聯的 JDBC 連接。您會注意到:JDBC 語句 確實 為它們的連接提供了存取器;但是,應用程序服務器和 Web 應用程序框架頻繁地使用修飾器模式包裝 JDBC 連接。我想確保自己能夠把語句與我擁有句柄的連接關聯起來,而不是與包裝的連接關聯起來。 JdbcConnectionMonitor 負責測量數據庫連接的性能統計值,它也把連接與它們來自數據源或連接 URL 的元數據(例如 JDBC URL 或數據庫名稱)關聯在一起。JdbcStatementMonitor 負責測量執行語句的性能統計值,跟蹤用來取得語句的連接,跟蹤與預備(和可調用)語句關聯的 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 跟蹤數據庫連接的方面。它用一個圖來關聯數據庫名稱和每個 JDBC 連接。 在
jdbcConnectionMonitor 內部 在清單 6 顯示的 JdbcConnectionMonitor 內部,我定義了切入點,捕捉連接數據庫的兩種不同方式:通過數據源或直接通過 JDBC URL。連接監視器包含針對每種情況的監視建議,兩種情況都設置一個工人對象。doExecute() 方法啟動時處理原始連接,然后把返回的連接傳遞給兩個輔助方法中名為 addConnection 的一個。在兩種情況下,被建議的切入點會排除來自另一個連接的連接調用(例如,如果要連接到數據源,會造成建立 JDBC 連接)。 數據源的 addConnection() 委托輔助方法 getNameForDataSource() 從數據源確定數據庫的名稱。DataSource 接口不提供任何這類機制,但是幾乎每個實現都提供了 getDatabaseName() 方法。getNameForDataSource() 用反射來嘗試完成這項工作和其他少數常見(和不太常見)的方法,為數據庫源提供一個有用的標識。addConnection() 方法然后委托給 addConnection() 方法,這個方法用字符串參數作為名稱。 被委托的 addConnection() 方法從父請求的上下文中檢索可以操作的統計值,并根據與指定連接關聯的數據庫名稱(或其他描述字符串)查詢數據庫的統計值。然后它把這條信息保存在請求上下文對象的 dbStats 字段中,更新關于獲得連接的性能信息。這樣就可以跟蹤連接數據庫需要的時間(通常這實際是從池中得到連接所需要的時間)。addConnection() 方法也更新到數據庫名稱的連接的連接圖。隨后在執行 JDBC 語句更新對應請求的統計值時,會使用這個圖。JdbcConnectionMonitor 還提供了一個輔助方法 getDatabaseName() ,它從連接圖中查詢字符串名稱找到連接。 弱標識圖和方面 JDBC 監視方面使用 弱標識 哈希圖。這些圖持有 弱 引用,允許連接這樣的被跟蹤對象在只有方面引用它們的時候,被垃圾收集掉。這一點很重要,因為單體的方面通常 不會 被垃圾收集。如果引用不弱,那么應用程序會有內存泄漏。方面用 標識 圖來避免調用連接或語句的hashCode 或 equals 方法。這很重要,因為我想跟蹤連接和語句,而不理會它們的狀態:我不想遇到來自 hashCode
方法的異常,也不想在對象的內部狀態已經改變時(例如關閉時),指望對象的哈希碼保持不變。我在處理動態的基于代理的 JDBC 對象(就像來自
iBatis 的那些對象)時遇到了這個問題:在連接已經關閉之后調用對象上的方法就會拋出異常。在完成操作之后還想記錄統計值時會造成錯誤。 從這里可以學到的教訓是:把對第三方代碼的假設最小化。使用標識圖是避免對接受建議的代碼的實現邏輯進行猜測的好方法。在這種情況下,我使用了來自 DCL Java 工具的 WeakIdentityHashMap 開放源碼實現(請參閱 參考資料)。
跟蹤連接或語句的元數據信息讓我可以跨越請求,針對連接或語句把統計值分組。這意味著可以只根據對象實例進行跟蹤,而不需要使用對象等價性來跟蹤這些
JDBC 對象。另一個要記住的教訓是:不同的對象經常用不同的修飾器包裝(越來越多地采用動態代理) JDBC
對象。所以假設要處理的是這類接口的簡單而原始的實現,可不是一個好主意! jdbcStatementMonitor 內部 清單 7 顯示了 JdbcStatementMonitor 方面。這個方面有兩個主要職責:跟蹤與創建和準備語句有關的信息,然后監視 JDBC 語句執行的性能統計值。 清單 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 維護兩個弱標識圖:statementCreators 和 statementSql 。第一個圖跟蹤用來創建語句的連接。正如前面提示過的,我不想依賴這條語句的 getConnection 方法,因為它會引用一個包裝過的連接,而我沒有這個連接的元數據。請注意 callCreateStatement 切入點,我建議它去監視 JDBC 語句的執行。這個建議匹配的方法調用是在 JDBC 連接上定義的,而且會返回 Statement 或任何子類。這個建議可以匹配 JDBC 中 12 種不同的可以創建或準備語句的方式,而且是為了適應 JDBC API 未來的擴展而設計的。
statementSql
圖跟蹤指定語句執行的 SQL 字符串。這個圖用兩種不同的方式更新。在創建預備語句(包括可調用語句)時,在創建時捕捉到 SQL
字符串參數。對于動態 SQL 語句,SQL
字符串參數在監視建議使用它之前,從語句執行調用中被捕捉。(建議的先后次序在這里沒影響;雖然是在執行完成之后才用建議查詢統計值,但字符串是在執行發
生之前捕捉的。)
語句的性能監視由一個 around 建議處理,它在執行 JDBC 語句的時候設置工人對象。執行 JDBC 語句的 statementExec 切入點會捕捉 JDBC Statement (包括子類)實例上名稱以 execute 開始的任何方法的調用,方法是在 JDBC API 中定義的(也就是說,在任何名稱以
java.sql 開始的包中)。 工人對象上的 lookupStats() 方法使用雙親(servlet)的統計上下文來查詢指定連接的數據庫統計值,然后查詢指定 SQL 字符串的 JDBC 語句統計值。直接的語句執行方法包括:SQL 語句中在 where 子句之后剝離數據的附加邏輯。這就避免了暴露敏感數據的風險,而且也允許把常見語句分組。更復雜的方式就是剝離查詢參數而已。但是,多數應用程序使用預備語句而不是動態 SQL 語句,所以我不想深入這一部分。
跟蹤 JDBC 信息 在結束之前,關于監視方面如何解決跟蹤 JDBC 信息的挑戰,請靜想一分鐘。JdbcConnectionMonitor 讓我把數據庫的文本描述(例如 JDBC URL)與用來訪問數據庫的連接關聯起來。同樣,JdbcStatementMonitor 中的 statementSql 映射跟蹤 SQL 字符串(甚至是用于預備語句的字符串),從而確保可以用有意義的名稱,把執行的查詢分成有意義的組。最后,JdbcStatementMonitor 中的 statementCreators 映射讓我把語句與我擁有句柄(而不是包裝過)的連接關聯。這種方式整合了多個建議,在把方面應用到現實問題時,更新內部狀態非常有用。在許多情況下,需要跟蹤來自 一系列 切入點的上下文信息,在單一公開上下文的 AspectJ 切入點中無法捕捉到這個信息。在出現這種情況時,一個切入點的跟蹤狀態可以在后一個切入點中使用這項技術就會非常有幫助。 這個信息可用之后,JdbcStatementMonitor 就能夠很自然地監視性能了。在語句執行切入點上的實際建議只是遵循標準方法 ,創建工人對象繼續處理原始的計算。lookupStats() 方法使用這三個不同的映射來查詢與這條語句關聯的連接和 SQL。然后它用它的雙親請求,根據連接的描述找到正確的數據庫統計值,并根據 SQL 鍵字符串找到語句統計值。lookupStats() 是防御性的,也就是說它在應用程序的使用違背預期的時候,會檢查 null 值。在這篇文章的第二部分,我將介紹如何用 AOP 系統地保證監視代碼不會在被監視的應用程序中造成問題。
第 1 部分結束語 迄今為止,我構建了一個核心的監視基礎設施,可以系統地跟蹤應用程序的性能、測量 servlet 操作中的數據庫活動。監視代碼可以自然地插入 JMX 接口來公開結果,如圖 5 所示。代碼已經能夠監視重要的應用程序邏輯,您也已經看到了擴展和更新監視方式有多容易。 圖 5. 監視數據庫結果

雖
然這里提供的代碼相當簡單,但卻是對傳統方式的巨大修改。AspectJ
模塊化的方式讓我可以精確且一致地處理監視功能。比起在整個示例應用程序中用分散的調用更新統計值和跟蹤上下文,這是一個重大的改進。即使使用對象來封裝
統計跟蹤,傳統的方式對于每個用戶操作和每個資源訪問,也都需要多個調用。實現這樣的一致性會很繁瑣,也很難一次實現,更不用說維護了。 在
這篇文章的第二部分中,我將把重點放在開發和部署基于 AOP 的性能監視系統的編程問題上。我將介紹如何用 AspectJ 5
的裝入時編織來監視運行在 Apache Tomcat
中的多個應用程序,包括在第三方庫中進行監視。我將介紹如何測量監視的開銷,如何選擇性地在運行時啟用監視,如何測量裝入時編織的性能和內存影響。我還會
介紹如何用方面防止監視代碼中的錯誤造成應用程序錯誤。最后,我將擴展 Glassbox Inspector,讓它支持 Web 服務和常見的
Web 應用程序框架(例如 Struts 和 Spring )并跟蹤應用程序錯誤。歡迎繼續閱讀! 致謝
感謝 Ramnivas Laddad、Will Edwards、Matt Hutton、David Pickering、Rob
Harrop、Alex Vasseur、Paul Sutter、Mik Kersten 和 Eugene Kuleshov
審閱本文并給出非常深刻的評價。
下載 描述 | 名字 | 大小 | 下載方法 |
---|
Sample code | j-aopwork10-source.zip | 75 KB |
FTP |
參考資料 學習
獲得產品和技術
討論
關于作者  |
| Ron
Bodkin 是 New Aspects of Software
的創始人,該公司提供應用程序開發和架構方面的咨詢和培訓,側重于性能管理和有效地使用面向方面編程。Ron 以前在 Xerox PARC 為
AspectJ 小組工作,在那里他領導了第一個 AOP 實現項目并負責客戶的培訓,他還是 C-bridge 的創始人和
CTO,這家咨詢機構采用 Java 的框架、XML 和其他 Internet 技術提供企業應用程序。Ron
經常為各種會議和客戶進行演講和提供教程,包括在 Software Development、The Colorado Software
Summit、
TheServerSide Symposium、EclipseCon、StarWest、Software Test &
Performance、OOPSLA 和 AOSD 上做報告。最近,他一直為 Glassbox 集團工作,用 AspectJ 和 JMX
開發應用程序性能管理和分析產品。可以通過 Ron 的郵箱 ron.bodkin@newaspects.com 與他聯系。 |
|