級(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

圖
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)

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)作控制器方法。
公開(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ì)值

傳統(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)的管理策略保持模塊化。
數(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ì)

清單 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í)行得更好。
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 方面,或者保持一致。
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ǔ)句和連接信息的方面:
JdbcConnectionMonitor 和
JdbcStatementMonitor 。 這些方面的一個(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ǔ)句的hashCode 或 equals 方法。這很重要,因?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í)圖:statementCreators 和 statementSql 。第一個(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ǔ)句,所以我不想深入這一部分。
跟蹤 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)題。
第 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é)果

雖
然這里提供的代碼相當(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à)。
下載 描述 | 名字 | 大小 | 下載方法 |
---|
Sample code | j-aopwork10-source.zip | 75 KB |
FTP |
參考資料 學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
討論
關(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)系。 |
|