級(jí)別: 高級(jí) Ron Bodkin , 創(chuàng)始人, New Aspects of Software
2005 年 12 月 19 日 有
了基本的面向方面的監(jiān)視基礎(chǔ)架構(gòu)后,可以對(duì)它進(jìn)行擴(kuò)展以滿足真實(shí)世界的監(jiān)視要求。在這篇由兩部分組成的文章的第二部分,Ron Bodkin
展示了如何在 Glassbox Inspector 中添加企業(yè)監(jiān)視功能,包括監(jiān)視多個(gè)應(yīng)用程序、Web 服務(wù)和 Web
應(yīng)用程序框架。他還展示了如何跟蹤應(yīng)用程序錯(cuò)誤并在監(jiān)視代碼中包含它們,并展示了如何以編程方式部署和控制這個(gè)監(jiān)視基礎(chǔ)架構(gòu)。  | 關(guān)于這個(gè)系列
AOP@Work系列 所面向的讀者是在面向方面編程上有些基礎(chǔ),想擴(kuò)展或加深了解的開(kāi)發(fā)人員。同 developerWorks 上的大多數(shù)文章一樣,這個(gè)系列高度實(shí)用:讀完每篇介紹新技術(shù)的文章,都可以立即進(jìn)行實(shí)踐。 這個(gè)系列的每個(gè)作者在面向方面編程領(lǐng)域都具有領(lǐng)袖地位或?qū)<宜健TS多作者都是系列中介紹的項(xiàng)目和工具的參與者。每篇文章都力圖提供一個(gè)中立的評(píng)述,以確保這里表達(dá)的觀點(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 的更多背景資料。 |
|
我在本文的 第 1 部分
中給出的基本 Glassbox Inspector 只能監(jiān)視簡(jiǎn)單的、基于 Servlet 的應(yīng)用程序(如在 Duke 的 Bookstore
這個(gè)例子中),更系統(tǒng)化的框架應(yīng)包括對(duì)流行的 Web 應(yīng)用程序框架(如 Struts 和 Spring)的支持。如今的 Web 應(yīng)用程序很少使用
Servlet 直接處理請(qǐng)求。這些框架通常將所有動(dòng)態(tài)頁(yè)請(qǐng)求委派給網(wǎng)關(guān) Servlet,如 Spring 框架的 DispatcherServlet ,然后它再將請(qǐng)求處理委派給不同的控制器。與此類似,Struts ActionServlet 委派給作為控制器的 Action 子類。 在
這篇探索用 AspectJ 進(jìn)行性能監(jiān)視的文章的第二部分中,我對(duì) Glassbox Inspector
進(jìn)行擴(kuò)展,通過(guò)增加監(jiān)視器以提供關(guān)于應(yīng)用程序性能的更有意義的信息。這些監(jiān)視器跟蹤 Struts 和 Spring Web
應(yīng)用程序框架的控制器之間的交互以及 Web
服務(wù)的響應(yīng)和請(qǐng)求操作。我還擴(kuò)展了系統(tǒng)以支持多個(gè)應(yīng)用程序,并添加了一個(gè)錯(cuò)誤處理層和在運(yùn)行時(shí)方便地啟用和禁止監(jiān)視的能力。在文章的最后展示如何用裝載時(shí)
織入部署 Glassbox Inspector,以及如何測(cè)量帶來(lái)的開(kāi)銷。 為了理解本文中的例子,需要對(duì) Spring MVC、Struts 和 Apache Axis
for Web Services 有一些了解。Glassbox Inspector 監(jiān)視基礎(chǔ)架構(gòu)的完整代碼請(qǐng)參閱 下載。要下載運(yùn)行本文中的例子所需要的 Aspectj、JMX 和 Tomcat 5 請(qǐng)參閱 參考資料。 可重用的操作監(jiān)視器 為了得到范圍更廣泛的監(jiān)視功能,我將 第 1 部分的清單 5 中大多數(shù)可重用的 Servlet 監(jiān)視代碼放到 AbstractOperationMonitor 方面中。很多情況下跟蹤嵌套的請(qǐng)求很重要,包括跟蹤在框架不同級(jí)別上的控制器以及跟蹤委派的請(qǐng)求(如從轉(zhuǎn)發(fā)到嵌套請(qǐng)求)的性能。清單 1 顯示了我如何擴(kuò)展 Glassbox Inspector 以跟蹤對(duì)操作的嵌套請(qǐng)求: 清單 1. 可重用的操作監(jiān)視方面
public abstract aspect AbstractOperationMonitor extends AbstractRequestMonitor { protected abstract class OperationRequestContext extends RequestContext { /** * Find the appropriate statistics collector object for this * operation. * @param operation an instance of the operation being * monitored */ public PerfStats lookupStats() { if (getParent() != null) { // nested operation OperationStats parentStats = (OperationStats)getParent().getStats(); return parentStats.getOperationStats(getKey()); } return getTopLevelStats(getKey()); } /** * Determine the top-level statistics for a given operation * key. This also looks up the context name for the * application from the operation monitor: * @see AbstractOperationMonitor#getContextName(Object) * For a Web application, top-level statistics are normally * all Servlets, and the key is the Servlet name. * @param key An object to uniquely identify the * operation being performed. */ protected OperationStats getTopLevelStats(Object key) { OperationStats stats; synchronized(topLevelOperations) { stats = (OperationStats)topLevelOperations.get(key); if (stats == null) { stats = perfStatsFactory.createTopLevelOperationStats(key, getContextName(controller)); topLevelOperations.put(key, stats); } } return stats; } /** * @return An object that uniquely identifies the operation * being performed. */ protected abstract Object getKey(); /** The current controller object executing, if any. */ protected Object controller; };
/** * This advice stores the controller object whenever we construct a * request context. */ after(Object controller) returning (OperationRequestContext ctxt) : cflow(adviceexecution() && args(controller, ..) && this(AbstractOperationMonitor)) && call(OperationRequestContext+.new(..)) { ctxt.controller = controller; }
...
|
AbstractOperationMonitor 的第一部分?jǐn)U展了本文第 1 部分中原來(lái)的 Servlet 監(jiān)視器以查看嵌套操作的統(tǒng)計(jì)。使用嵌套操作使我們可以在分派 JSP 或者分解 Web 服務(wù)或者多方法應(yīng)用程序控制器的不同方法這樣的情況下跟蹤資源占用。原來(lái)的 lookupStats() 方法現(xiàn)在檢查父請(qǐng)求上下文。如果有父請(qǐng)求,那么它調(diào)用新方法 getOperationStats() 以獲取它。否則,它調(diào)用新方法 getTopLevelStats() ,這個(gè)方法調(diào)用一個(gè)工廠以創(chuàng)建一個(gè)新的 OperationStats 。使用工廠可以保證我的監(jiān)視基礎(chǔ)架構(gòu)不依賴于統(tǒng)計(jì)類的實(shí)現(xiàn)。
監(jiān)視多個(gè)應(yīng)用程序 在清單 1 中,我還加入了對(duì)運(yùn)行在單個(gè)應(yīng)用服務(wù)器中的多個(gè)應(yīng)用程序的監(jiān)視支持。主要是通過(guò)增加一個(gè)檢查應(yīng)用程序上下文的預(yù)查來(lái)做到這一點(diǎn)的。在檢查頂級(jí)統(tǒng)計(jì)時(shí),我調(diào)用一個(gè)監(jiān)視器模板方法 getContextName() ,以確定操作關(guān)聯(lián)的是哪一個(gè)應(yīng)用程序。下面將會(huì)看到對(duì)于 Servlet 它是如何處理的。注意,向 getContextName() 方法傳遞了控制器的一個(gè)實(shí)例,這樣就可以檢查關(guān)聯(lián)的應(yīng)用程序上下文。 在抽象操作監(jiān)視器中,還提供了具有具體建議的切點(diǎn),它定義了監(jiān)視請(qǐng)求的常用方法。對(duì)它做了擴(kuò)展以便監(jiān)視收到的請(qǐng)求,如 Struts 操作和收到的 Web 服務(wù)請(qǐng)求。清單 2 展示了一個(gè)代表性的例子: 清單 2. AbstractOperationMonitor 中的監(jiān)視模板
/** * This defaults to no join points. If a concrete aspect overrides * classControllerExec with a concrete definition, * then the monitor will track operations at matching join points * based on the class of the controller object. */ protected pointcut classControllerExec(Object controller); Object around(final Object controller) : classControllerExec(controller) { RequestContext rc = new OperationRequestContext() { public Object doExecute() { return proceed(controller); } protected Object getKey() { return controller.getClass(); } }; return rc.execute(); }
// Controller where the name of the signature at the monitored join point // determines what is being executed, for example, the method name /** * This defaults to no join points. If a concrete monitor overrides * methodSignatureControllerExec with a concrete * definition, then it will track operations at matching join points * based on the run-time class of the executing controller instance * and the method signature at the join point. */ protected pointcut methodSignatureControllerExec(Object controller); Object around(final Object controller) : methodSignatureControllerExec(controller) { RequestContext rc = new OperationRequestContext() { public Object doExecute() { return proceed(controller); } protected Object getKey() { return concatenatedKey(controller.getClass(), thisJoinPointStaticPart.getSignature().getName()); } }; return rc.execute(); }
|
classControllerExec() 的切點(diǎn)捕獲所有類控制器處理請(qǐng)求的點(diǎn),像 Servlet do 方法執(zhí)行或者普通 Struts action execute 方法,在這里響應(yīng)請(qǐng)求的對(duì)象的類確定要執(zhí)行的操作。更準(zhǔn)確地說(shuō), classControllerExec() 切點(diǎn) 定義了一個(gè)空的切點(diǎn)(它不會(huì)匹配任何連接點(diǎn))。然后它提供一個(gè)具體建議,這個(gè)建議設(shè)置工人對(duì)象并返回對(duì)于這種情況正確的鍵值。這與使用一個(gè)抽象切點(diǎn)類似,其中子方面必須覆蓋切點(diǎn)以使用建議。不過(guò)在這里,我提供了永遠(yuǎn)不匹配的默認(rèn)定義。如果 AbstractOperationMonitor 的子方面不需要監(jiān)視類控制器,那么它不覆蓋這個(gè)切點(diǎn)就行了。如果它需要監(jiān)視類控制器,那么它就提供什么時(shí)候監(jiān)視一個(gè)點(diǎn)的定義。
具體化操作監(jiān)視器 methodSignatureControllerExec() 切點(diǎn)和關(guān)聯(lián)的建議類似:它們提供具體化操作監(jiān)視方面的方法,以根據(jù)連接點(diǎn)上的簽名匹配分派到不同方法的控制器。
清單 3 展示了擴(kuò)展 AbstractOperationMonitor 以監(jiān)視 Struts 和 Spring MVC 操作的具體方面: 清單 3. 監(jiān)視 Struts 和 Spring MVC 框架
public aspect StrutsMonitor extends AbstractOperationMonitor { /** * Marker interface that allows explicitly _excluding_ classes * from this monitor: not used by default. If using Java? 5, an * annotation would be better. */ public interface NotMonitoredAction {}
/** * Matches execution of any method defined on a Struts action or * any subclass, which has signature of an action execute (or * perform) method, including methods dispatched to in a * DispatchAction or template methods with the same signature. */ public pointcut actionMethodExec() : execution(public ActionForward Action+.*(ActionMapping, ActionForm, ServletRequest+, ServletResponse+)) && !within(NotMonitoredAction);
/** * Matches execution of an action execute (or perform) method for * a Struts action. Supports the Struts 1.0 API (using the perform * method) as well as the Struts 1.1 API (using the execute method). */ public pointcut rootActionExec() : actionMethodExec() && (execution(* Action.execute(..)) || execution(* Action.perform(..))); /** @Override */ protected pointcut classControllerExec(Object controller) : rootActionExec() && this(controller);
protected pointcut dispatchActionMethodExec() : actionMethodExec() && execution(* DispatchAction+.*(..));
protected pointcut methodSignatureControllerExec(Object controller): dispatchActionMethodExec() && this(controller); }
public aspect SpringMvcMonitor extends AbstractOperationMonitor { /** * marker interface that allows explicitly _excluding_ classes * from this monitor: not used by default */ public interface NotMonitoredController {} public pointcut springControllerExec() : execution(public ModelAndView Controller+.*(HttpServletRequest, HttpServletResponse)) && !within(NotMonitoredController+);
protected pointcut classControllerExec(Object controller) : springControllerExec() && execution(* handleRequest(..)) && this(controller);
protected pointcut methodSignatureControllerExec(Object controller): springControllerExec() && execution(* MultiActionController+.*(..)) && this(controller); }
|
關(guān)于這兩個(gè)方面首先要注意的是它們非常簡(jiǎn)潔。它們只是擴(kuò)展了操作監(jiān)視器中的兩個(gè)切點(diǎn)以具體地監(jiān)視它們特定的 API。因?yàn)樗鼈兊暮?jiǎn)潔性,也可以在 XML 中定義這兩個(gè)具體工作監(jiān)視器,而不用編譯。關(guān)于 AspectJ 5 的這個(gè)功能的例子請(qǐng)參閱
清單 10。 關(guān)于 StrutsMonitor 和 SpringMvcMonitor 清單 3 中的 StrutsMonitor 方面設(shè)計(jì)為同時(shí)使用老版本和新版本的 Struts API:
Struts 1.0 操作是通過(guò)調(diào)用 perform() 而調(diào)用的,而 Struts 1.1 操作是通過(guò)調(diào)用 execute() 而調(diào)用的。我將正常操作類的具體子類作為 Class 控制器跟蹤:只有執(zhí)行對(duì)象的類才是關(guān)注的。不過(guò),如果一個(gè)類擴(kuò)展了 DispatchAction ,Struts 可以讓控制器分派給這個(gè)類中的多個(gè)方法。我通過(guò)匹配 DispatchAction 子類中所有具有 Struts 操作簽名的方法監(jiān)視在這些分派操作中執(zhí)行的各個(gè)方法。這種方式使我可以分別跟蹤每一個(gè)不同控制器方法的統(tǒng)計(jì)。 我用 iBatis JPetStore 1.3 示例應(yīng)用程序(請(qǐng)參閱 參考資料) 測(cè)試了 StrutsMonitor 。與許多應(yīng)用程序一樣,它用自己的一個(gè)小框架擴(kuò)展了 Struts:公共基本操作有一個(gè)名為 perform() 的方法,它向 helper 分派用 doPerform() 作為模板方法的操作。不過(guò),不需要跟蹤這些模板方法的執(zhí)行:類級(jí)別的控制器會(huì)識(shí)別在 execute() 方法中執(zhí)行的 Action 的特定子類,這足以區(qū)分它們了。 SpringMvcMonitor 方面與 StrutsMonitor 有一點(diǎn)很類似,它們將所有 Controller 對(duì)象作為類控制器,監(jiān)視它什么時(shí)候執(zhí)行 handleRequest() 。它還監(jiān)視 MultiActionController 或者它的任何子類中具有 Spring 控制器方法簽名的公共方法的執(zhí)行。例如,我在這段代碼中分別監(jiān)視 welcomeHandler() 和
ownerHandler() 的執(zhí)行:
public class ClinicController extends MultiActionController implements InitializingBean { ... public ModelAndView welcomeHandler(HttpServletRequest request, HttpServletResponse response) throws ServletException { return new ModelAndView("welcomeView"); } ... public ModelAndView ownerHandler(HttpServletRequest request, HttpServletResponse response) throws ServletException { Owner owner = clinic.loadOwner( RequestUtils.getIntParameter(request, "ownerId", 0)); ... model.put("owner", owner); return new ModelAndView("ownerView", "model", model); } ...
|
當(dāng)然,可以容易地?cái)U(kuò)展這種方法以處理其他的框架,包括自定義框架,甚至前面描述過(guò)的用 XML 定義的方面。 編寫可移植的方面 注意 StrutsMonitor
方面像我的大多數(shù)監(jiān)視方面一樣,設(shè)計(jì)為針對(duì)常見(jiàn)組件的多個(gè)版本。它的切點(diǎn)匹配不同版本的
Struts,它們可以引入那些在織入時(shí)(例如在裝載時(shí))還不可用的類型。只需在像字段簽名或者類型模式這樣的靜態(tài)可解析的上下文中使用類名就可得到這樣
的切點(diǎn)。如果在動(dòng)態(tài)上下文中(如 this、target 或者 args)中使用類名,那么就要求織入器能夠訪問(wèn)類型。注意 Struts 監(jiān)視器將 this 切點(diǎn)指示符綁定到 Object 的一個(gè)實(shí)例,這不需要有 Struts JAR。雖然有些別扭(因?yàn)樗チ司幾g時(shí)類型檢查的好處),但是這種方法對(duì)于部署可重用方面時(shí)要求有許多依賴庫(kù)的情況更有利。 不
過(guò),引用類或者訪問(wèn)其成員的(如通過(guò)調(diào)用一個(gè)方法)的建議或者其他可執(zhí)行代碼需要遵守 Java 代碼的常規(guī)可移植性規(guī)范。如果建議必須調(diào)用在 API
的不同版本中有變化的方法,那么可以使用 Java 反射檢查是否有不同的方法并執(zhí)行它們,例如,對(duì)一個(gè)操作方法顯式地調(diào)用 perform() 或者 execute() 。注意,如果只想在一個(gè) around 建議中的連接點(diǎn)處使用原來(lái)的 execute() 或者 perform() 方法,那么 AspectJ 會(huì)替我處理這種情況,不需要做任何特別的工作。實(shí)際上,許多 API 都提供了向后兼容性,因此可以針對(duì)要支持的老版本編譯,它在新版本中也能工作。 通
常,方面不會(huì)執(zhí)行觸發(fā)裝載一個(gè)類的代碼,除非程序的執(zhí)行總是會(huì)裝載這個(gè)類,例如,引用一個(gè)類的建議在這個(gè)類要執(zhí)行時(shí)會(huì)被觸發(fā)。保留這個(gè)屬性對(duì)于涉及可選類
的庫(kù)方面很重要。對(duì)于 Glassbox Inspector,我不想讓被監(jiān)視的應(yīng)用程序加入可能監(jiān)視的某個(gè)版本的 Struts 或者其他庫(kù)。 監(jiān)視 JSP 和應(yīng)用程序名 清單 4 展示了重構(gòu)后的本文第 1 部分中的 ServletMonitor ,它使用抽象操作監(jiān)視器作為一個(gè)基本方面。我通過(guò)擴(kuò)展 classControllerExec() 繼續(xù)按類跟蹤 Servlet。增加了通過(guò)監(jiān)視 jspService() 方法對(duì)監(jiān)視 JSP 的支持。因?yàn)槲蚁胍_定 Web 應(yīng)用程序的名字,因此設(shè)置 ServletMonitor 覆蓋 getContext() 方法以檢查 Servlet 上下文的名字。 我
到目前為止所實(shí)現(xiàn)的所有其他 Web 應(yīng)用程序最終是由 Servlet 調(diào)用的,因此只有這些控制器需要提供應(yīng)用程序上下文的一個(gè)實(shí)現(xiàn)。特別是,我的
Web 應(yīng)用程序框架監(jiān)視器是嵌套在它們的分派 Servlet 調(diào)用中的。在進(jìn)一步擴(kuò)展 Glassbox Inspector
以監(jiān)視其他操作(如收到的 Web 服務(wù)請(qǐng)求)時(shí),將需要提供操作名作為不同應(yīng)用程序的上下文。如果要擴(kuò)展框架以監(jiān)視收到的 JMS 消息或者 EJB
請(qǐng)求,那么這個(gè)框架還要為那些資源提供應(yīng)用程序上下文。 清單 4. 擴(kuò)展
ServletMonitor
public aspect ServletMonitor extends AbstractOperationMonitor { /** * Execution of any Servlet method: typically not overridden in * HttpServlets. */ public pointcut ServletService(Servlet Servlet) : execution(void Servlet.service(..)) && this(Servlet); /** Execution of any Servlet request methods. */ public pointcut httpServletDo(HttpServlet Servlet) : execution(void HttpServlet.do*(..)) && this(Servlet); /** Execution of any JSP page service method. */ public pointcut jspService(JspPage page) : execution(* _jspService(..)) && this(page); protected pointcut classControllerExec(Object controller) : (ServletService(*) || httpServletDo(*) || jspService(*)) && this(controller);
/** * Create statistics for this object: looks up Servlet context to * determine application name. */ protected String getContextName(Object controller) { Servlet Servlet = (Servlet)controller; return Servlet.getServletConfig().getServletContext(). getServletContextName(); } }
|
更新 JMX 以得到嵌套的統(tǒng)計(jì) 在 第 1 部分中,我擴(kuò)展了 Glassbox 監(jiān)視基礎(chǔ)架構(gòu)以提供嵌套的統(tǒng)計(jì),如 Servlet 請(qǐng)求的連接中的 JDBC 語(yǔ)句。在這里,我分析了如何更新 StatsJmxManagement 方面以提供這些嵌套統(tǒng)計(jì)的復(fù)合名。我還展示如何將應(yīng)用程序名加入到這個(gè)復(fù)合名中。例如,數(shù)據(jù)庫(kù)語(yǔ)句的統(tǒng)計(jì)可以用以下字符串命名: application=Spring Petclinic,operation=org.springframework.samples. petclinic.web.ClinicController,database=jdbc:hsqldb:hsql://localhost: 9001,statement=SELECT id;name from types ORDER BY name
|
這個(gè)字符串使用給定信息的所有前級(jí)統(tǒng)計(jì)的性能統(tǒng)計(jì)的描述和名字。以這種方式命名統(tǒng)計(jì)使 JMX 工具可以自然地將相關(guān)的信息組織并顯示在一起。像 JConsole 這樣的 JMX 工具使用結(jié)構(gòu)化的名字來(lái)組織常見(jiàn)的元素,更容易以層次化的方法瀏覽,如 圖 1 所示。清單 5 展示了為支持這個(gè)功能需要對(duì) StatsJmxManagement 進(jìn)行的更新; 清單 5. 更新 StatsJmxManagement 以支持嵌套的統(tǒng)計(jì)
public aspect StatsJmxManagement {
private String PerfStats.cachedOperationName; /** JMX operation name for this performance statistics bean. */ public String PerfStats.getOperationName() { // look up from cache // else ... appendOperationName(buffer); ... return operationName; } /** Add bean's JMX operation name to a StringBuffer. */ public void PerfStats.appendOperationName(StringBuffer buffer) { if (cachedOperationName != null) { buffer.append(cachedOperationName); } else { aspectOf().nameStrategy.appendOperationName(this, buffer); } }
public void PerfStats.appendName(StringBuffer buffer) { // append the appropriate name & JMX encode ... }
public StatsJmxNameStrategy getNameStrategy() {...} public void setNameStrategy(StatsJmxNameStrategy nameStrategy) { ... } private StatsJmxNameStrategy nameStrategy; }
public interface StatsJmxNameStrategy { void appendOperationName(PerfStats stats, StringBuffer buffer); }
public class GuiFriendlyStatsJmxNameStrategy extends AbstractStatsJmxNameStrategy { public void appendOperationName(PerfStats stats, StringBuffer buffer) { PerfStats parent = stats.getParent(); if (parent != null) { appendOperationName(parent, buffer); buffer.append(','); else { if (stats instanceof OperationStats) { OperationStats opStats = (OperationStats)stats; String contextName = opStats.getContextName(); if (contextName != null) { buffer.append("application=\""); int pos = buffer.length(); buffer.append(contextName); JmxManagement.jmxEncode(buffer, pos); buffer.append("\","); } } } buffer.append(stats.getDescription()); buffer.append('='); stats.appendName(buffer); } }
|
清單 5 展示了我如何設(shè)置 StatsJmxManagement 以允許不同的命名策略。為了使用像 JConsole 這樣的 GUI 工具,我列出了一個(gè) GuiFriendlyStatsJmxNameStrategy ,它返回如清單 5 和圖 1 所示的名字。除此之外,完整的代碼包括另一個(gè)可選的 CanonicalStatsJmxNameStrategy ,它遵守 JMX 建議的 MBean 命名方式,但是在我所試過(guò)的所有 JMX 工具中不能顯示。 用于構(gòu)建這些名字的基本方法是,創(chuàng)建一個(gè)字符串緩沖區(qū)并遞歸地得到父統(tǒng)計(jì)的操作名,然后在后面加上這個(gè)名字。對(duì)于頂級(jí)操作統(tǒng)計(jì),首先加入上下文(應(yīng)用程序)名。為了支持這種方法,我在 PerfStats 中增加了 getDescription() 方法。還更新了操作和資源統(tǒng)計(jì)實(shí)現(xiàn)以設(shè)置合適的值(如 operation, operation1, .., resource, and
request )。下載 Glassbox
Inspector 源代碼以查看所有細(xì)節(jié)和另一個(gè)可選的 CanonicalStatsJmxNameStrategy 。
監(jiān)視 Web 服務(wù)調(diào)用 Glassbox Inspector 框架現(xiàn)在很容易擴(kuò)展以支持新類型的請(qǐng)求(操作)以及新資源。監(jiān)視提供的(收到的)遠(yuǎn)程服務(wù)操作與監(jiān)視 Web 應(yīng)用程序框架很相似:本文的源代碼給出了監(jiān)視用 Apache Axis 實(shí)現(xiàn)的服務(wù)的一個(gè)例子。 只
需要很少的調(diào)整就可以將這個(gè) Glassbox
Inspector
擴(kuò)展為監(jiān)視應(yīng)用程序之外的遠(yuǎn)程服務(wù)的使用。隨著具有面向服務(wù)體系結(jié)構(gòu)的應(yīng)用程序越來(lái)越常見(jiàn),跟蹤外部控制的服務(wù)的可靠性及確定應(yīng)用程序問(wèn)題的相互關(guān)系越來(lái)
越重要了。將這些數(shù)據(jù)與應(yīng)用程序上下文(如執(zhí)行的 Web
操作和在其他資源上花費(fèi)的時(shí)間)相關(guān)聯(lián)是迅速找出應(yīng)用程序故障根源的重要方法。這種分析與使用 SOAP 處理程序孤立地監(jiān)視 Web
服務(wù)調(diào)用有很大不同,因?yàn)樗鼘?shí)現(xiàn)信息與外部請(qǐng)求的性能連接到一起了。 清單 6 展示了一個(gè)遠(yuǎn)程調(diào)用監(jiān)視器,它監(jiān)視通過(guò) JAX-RPC API 對(duì) Web 服務(wù)的調(diào)用以及 RMI 調(diào)用(它本身通常是遠(yuǎn)程的): 清單 6. 監(jiān)視遠(yuǎn)程調(diào)用
public aspect RemoteCallMonitor extends AbstractResourceMonitor { /** Call to remote proxy: RMI or JAX-RPC */ public pointcut remoteProxyCall(Object recipient) : call(public * Remote+.*(..) throws RemoteException) && target(recipient) && !within(glassbox.inspector..*); /** Monitor remote proxy calls based on called class and method */ Object around(final Object recipient) : remoteProxyCall(recipient) { RequestContext requestContext = new ResourceRequestContext() { public Object doExecute() { return proceed(recipient); } public PerfStats lookupStats() { String key = "jaxrpc:"+recipient.getClass().getName()+ "."+thisJoinPointStaticPart.getSignature().getName(); key = key.intern(); return lookupResourceStats(key); } }; return requestContext.execute(); } }
|
注意,只需要做很少的工作就可以支持這種功能,大多數(shù)支持包含在 AbstractRequestMonitor 中。我只是定義遠(yuǎn)程代理調(diào)用為對(duì)任何實(shí)現(xiàn)了 Remote 接口的公共方法的調(diào)用,這個(gè)方法可以拋出 RemoteException 。完成這個(gè)小小的改變后,就可以使用 Worker Object 模式(請(qǐng)參閱 第 1 部分)監(jiān)視遠(yuǎn)程調(diào)用,提供一個(gè)以類名和被調(diào)用的對(duì)象的方法名為基礎(chǔ)的名字。RemoteCallMonitor 方面使用一個(gè) helper 方法尋找頂級(jí)資源統(tǒng)計(jì),它是從 JdbcConnectionMonitor
中的工人對(duì)象中提取的,放入這個(gè)新的公共基本方面:AbstractResourceMonitor 。 自
然,可以進(jìn)一步擴(kuò)展這種方法以監(jiān)視其他 Web 服務(wù)調(diào)用以及流行框架(如 Apache Axis)上方法的執(zhí)行。我還可以使用這種技術(shù)監(jiān)視 XML
處理所花費(fèi)的時(shí)間,這對(duì)于任何有大量 XML 的應(yīng)用程序是很有用的。許多利用 Web 服務(wù)的應(yīng)用程序也可受益于這種監(jiān)視。
監(jiān)視故障 合乎邏輯的下一步是擴(kuò)展 Glassbox Inspector 系統(tǒng)以監(jiān)視應(yīng)用程序故障。我首先在系統(tǒng)退出一個(gè)監(jiān)視點(diǎn)時(shí)通過(guò)拋出一個(gè)異常(更準(zhǔn)確地說(shuō)是任何 Throwable )來(lái)記錄故障。故障記錄為跟蹤 Web 操作和數(shù)據(jù)庫(kù)連接工作提供了有用的信息:這些操作中的任何一個(gè)拋出 Throwable
在幾乎任何情況下都表示有問(wèn)題。對(duì)于 JDBC 語(yǔ)句和其他資源訪問(wèn),throwable
通常表明一個(gè)真正的應(yīng)用程序故障,但是在有些情況下,它是正常程序邏輯的一部分。例如,試圖用一個(gè)已經(jīng)有的名字注冊(cè)會(huì)在插入時(shí)觸發(fā)異常。為了照顧到這種情
況,我對(duì)每個(gè)監(jiān)視使用了一個(gè)可配置的策略,以確定給定的 throwable 是真正的錯(cuò)誤還是作為正常的退出條件接受。 清單 7 包含了使我可以配置 AbstractRequestMonitor 以確定 Throwable 是否為故障的建議和改動(dòng): 清單 7. 增加可擴(kuò)展的故障檢測(cè)
public aspect AbstractRequestMonitor {
/** * Record an error if this request exited by throwing a * Throwable. */ after(RequestContext requestContext) throwing (Throwable t) : requestExecution(requestContext) { PerfStats stats = requestContext.getStats(); if (stats != null) { if (failureDetectionStrategy.isFailure(t, thisJoinPointStaticPart)) { requestContext.recordFailure(t); } else { requestContext.recordExecution(); } } } public void setFailureDetectionStrategy(...) { ... } public FailureDetectionStrategy getFailureDetectionStrategy() { ... } protected FailureDetectionStrategy failureDetectionStrategy = new FailureDetectionStrategy() { public boolean isFailure(Throwable t, StaticPart staticPart) { return true; } }; ... }
public interface FailureDetectionStrategy { boolean isFailure(Throwable t, StaticPart staticPart); }
|
清單 7 中最后的效果是,在通過(guò)拋出異常退出一個(gè)監(jiān)視的連接點(diǎn)時(shí)記錄一個(gè)計(jì)數(shù)。注意,我肯定是要識(shí)別那些通過(guò)拋出 Error 退出的情況,因?yàn)樗鼈儙缀蹩隙ㄊ枪收希≡谶@里,可以容易地?cái)U(kuò)展這個(gè) AbstractRequestMonitor 以跟蹤部分或者所有異常、堆棧蹤跡和在發(fā)生異常時(shí)當(dāng)前對(duì)象和參數(shù)值。如果這樣做了,那么就需要有一種保證不記錄敏感信息,如密碼、信用卡號(hào)或者個(gè)人身份信息的方法。為了完成這種集成,我對(duì) AbstractRequestMonitor 做了小小的修改。細(xì)節(jié)請(qǐng)參閱 文章源代碼。 對(duì)錯(cuò)誤使用異常轉(zhuǎn)換 基于前面的討論,您可能奇怪如何能配置 Glassbox Inspector 以識(shí)別出某些 throwable 不是錯(cuò)誤。最容易的方法是傳遞一個(gè)對(duì)于給定監(jiān)視方法(如 JdbcStatementMonitor )總是返回 false 的策略。另一種簡(jiǎn)單的解決方案是檢測(cè)某種特定的異常類型,如 NumberFormatException ,并指明它不是故障。這可以結(jié)合 Exception Conversion 模式(請(qǐng)參閱 參考資料)以將某些異常轉(zhuǎn)換為有意義的層次結(jié)構(gòu)。清單 8 就是如何使用異常轉(zhuǎn)換以及故障檢測(cè)策略的一個(gè)示例: 清單 8. 跟蹤故障并使用異常轉(zhuǎn)換
/** * Softens and uses Spring's exception translator to convert from * SQLException to unchecked, meaningful exception types */ aspect DataAccessErrorHandling { declare soft: SQLException: jdbcCall();
after() throwing (SQLException e) : jdbcCall() { throw sqlExceptionTranslator.translate("dbcall", getSql(), e); }
declare precedence: ModelErrorHandling, JdbcStatementMonitor; }
/** * Conservative detection strategy: failures from SQL that often * indicate valid business conditions are not flagged as failures. */ public class DataAccessDetectionStrategy implements FailureDetectionStrategy { public boolean isFailure(Throwable t, StaticPart staticPart) { return !(t instanceof ConcurrencyFailureException) && !(t instanceof DataIntegrityViolationException); } }
|
清單 8 的第一部分顯示了一個(gè)簡(jiǎn)單的錯(cuò)誤處理方面,它軟化了來(lái)自 JDBC 調(diào)用的 SQLExceptions ,然后使用一個(gè) Spring 框架 SQLExceptionTranslator (如 SQLErrorCodeSQLExceptionTranslator )將異常轉(zhuǎn)換為有意義的(unchecked)異常。這個(gè)方面還用 AspectJ 的 declare
precedence 格式聲明優(yōu)于 JdbcStatementMonitor 。這保證了在 JdbcStatementMonitor 跟蹤返回的異常類型之前,DataAccessErrorHandling 已經(jīng)轉(zhuǎn)換了異常。 清單 8 的其他部分展示了一個(gè)示例策略,它只在幾乎確實(shí)表明有故障(如無(wú)法訪問(wèn)資源)的條件下表明故障。特別是,它排除了并發(fā)錯(cuò)誤(在設(shè)計(jì)良好的、多用戶應(yīng)用程序中會(huì)出現(xiàn))和數(shù)據(jù)完整性破壞(如通常在注冊(cè)新用戶時(shí)發(fā)生)的情況。 要對(duì)故障檢測(cè)有更多的控制,可能要有一個(gè)標(biāo)志性接口或者一個(gè)方法或者類上的注釋,以表明在正常的操作過(guò)程中,系統(tǒng)的某些部分會(huì)拋出 Exception 。這可結(jié)合使用 declare parents 或者 AspectJ 5 新的 declare annotation 格式捕獲模塊化規(guī)則,表明拋出一個(gè) Throwable 是否表明故障。在大多數(shù)情況下,我使用的簡(jiǎn)單規(guī)則對(duì)于粗粒度的監(jiān)視是合適的,但是要避免不正確的警報(bào),最好有更多的靈活性。 檢測(cè)常見(jiàn) Web 故障 應(yīng)用程序有可能在出現(xiàn)故障時(shí)并不拋出異常。一種最常見(jiàn)的情況是當(dāng)控制器處理異常并將請(qǐng)求轉(zhuǎn)發(fā)給一個(gè)錯(cuò)誤頁(yè)時(shí)。另一種情況是當(dāng)對(duì)于頁(yè)的 HTTP 響應(yīng)指明一個(gè)錯(cuò)誤時(shí)。清單 9 更新了清單 4 中的 ServletMonitor 以檢測(cè)這種情況。我還對(duì) AbstractRequestMonitor.RequestContext 工人對(duì)象做了小小的修改以設(shè)置錯(cuò)誤上下文,并在設(shè)置錯(cuò)誤上下文后記錄故障。 清單 9. 檢測(cè)常見(jiàn) Web 故障
/** Call of send error in the Servlet API */ public pointcut sendingErrorResponse(HttpServletResponse response, int sc) : target(response) && call(* sendError(..)) && args(sc, ..); /** Track a failure after sending an error response */ after(HttpServletResponse response, RequestContext requestContext, int sc) returning : sendingErrorResponse(response, sc) && inRequest(requestContext) { requestContext.setErrorContext(new Integer(sc)); }
/** Execute handle page exception in the JSP API */ public pointcut handlePageException(PageContext pageContext, Throwable t) : call(* PageContext.handlePageException(*)) && target(pageContext) && args(t);
/** Track a failure when showing an error page */ after(PageContext pageContext, RequestContext requestContext, Throwable t) returning : handlePageException(pageContext, t) && inRequest(requestContext) { requestContext.setErrorContext(t); }
|
擴(kuò)展庫(kù)方面 有些時(shí)候,當(dāng)我構(gòu)建 Glassbox Inspector 時(shí),需要讓庫(kù)方面更有可擴(kuò)展性。在下面列出了一些可用的常見(jiàn)選項(xiàng)。其中一些選項(xiàng)我已經(jīng)展示過(guò)了,它們都很有用: - 提供一個(gè)抽象切點(diǎn)以定義方面用在什么地方(例如,一個(gè)
scope() 定義)。
- 提供一個(gè)空的切點(diǎn),必須為要使用的某個(gè)建議覆蓋它(如在 清單 2 中的
AbstractOperationMonitor )。在許多情況下,少數(shù)的具體方面需要使用給定的建議,因此有一個(gè)空的默認(rèn)值更有用。像這樣的模板切點(diǎn)有一個(gè)空的默認(rèn)實(shí)現(xiàn)與模板方面有一個(gè)空的默認(rèn)實(shí)現(xiàn)很相像。
- 利用標(biāo)志性接口表明方面應(yīng)用于哪些類型。因?yàn)榻涌谑抢^承的,所以這些類型的所有子類型都包括在內(nèi)了。
- 利用 Java 5 注釋表明方面應(yīng)用于哪些類型和/或方法。這種方法使您可以細(xì)化對(duì)方面應(yīng)用領(lǐng)域的控制,可以按方法定義,以及包括類而不包括它的子類。
- 提供運(yùn)行時(shí)行為配置,有測(cè)試一種功能是否啟用的邏輯。例子請(qǐng)參閱 運(yùn)行時(shí)控制。
在
應(yīng)用程序中集成了這些擴(kuò)展后,我就完成了對(duì) Glassbox Inspector 監(jiān)視的擴(kuò)展(至少對(duì)于本文的目的 ——
本文結(jié)束的地方就是開(kāi)放源碼項(xiàng)目開(kāi)始的地方!)。我現(xiàn)在有了一個(gè)將應(yīng)用程序性能按照邏輯組織到一起的有用視圖,這樣就可以通過(guò) web
應(yīng)用程序中的請(qǐng)求用標(biāo)準(zhǔn) JMX 客戶機(jī)觀察總體性能統(tǒng)計(jì)和錯(cuò)誤。對(duì)于有疑問(wèn)的地方,我還可以深挖數(shù)據(jù)庫(kù)或者 Web 服務(wù)的性能和可靠性。圖 1
顯示了在 Spring Petclinic、iBatis JPetstore 和 Duke 的 Bookstore Web
應(yīng)用程序例子中使用的 Glassbox Inspector 監(jiān)視工具: 圖 1. 使用中的完整監(jiān)視基礎(chǔ)架構(gòu)的快照

錯(cuò)誤處理 在這之后,我將重點(diǎn)放到如何更容易地部署擴(kuò)展的
Glassbox Inspector 監(jiān)視基礎(chǔ)架構(gòu)上。邁向用于企業(yè)管理的基礎(chǔ)架構(gòu)的第一步是擴(kuò)展 Glassbox
Inspector 來(lái)實(shí)現(xiàn)錯(cuò)誤隔離。傳統(tǒng)上,開(kāi)發(fā)人員盡可能保證像性能監(jiān)視這樣的功能不會(huì)產(chǎn)生 影響被監(jiān)視應(yīng)用程序的錯(cuò)誤。不過(guò),監(jiān)視代碼中未預(yù)料到的行為(如未預(yù)計(jì)到的 null 值、拋出異常的代理等)有時(shí)會(huì)在監(jiān)視代碼自身中產(chǎn)生錯(cuò)誤。在這些情況下,隔離 錯(cuò)誤的能力可提高監(jiān)視實(shí)現(xiàn)的健壯性。系統(tǒng)初始化時(shí)出現(xiàn)的問(wèn)題可能造成錯(cuò)誤隔離方面不能緩沖的錯(cuò)誤,但是這種問(wèn)題通常是立即發(fā)生的,并容易在開(kāi)發(fā)時(shí)捕獲,使得它的風(fēng)險(xiǎn)降低了很多。 我的基本錯(cuò)誤隔離策略是,捕獲并處理所有可能從建議執(zhí)行連接點(diǎn)拋出的 Throwable ,這樣它們就不會(huì)影響底層的應(yīng)用程序。注意,建議執(zhí)行切點(diǎn)匹配一些建議執(zhí)行的連接點(diǎn),因此這個(gè)方面將影響其他方面的行為。這種策略對(duì)于 before 和 after 建議都可以很好地工作,但是 around 建議實(shí)現(xiàn)錯(cuò)誤隔離要復(fù)雜一點(diǎn)兒。如果在達(dá)到 proceed 語(yǔ)句之前拋出異常,那么我會(huì)仍然繼續(xù)原來(lái)的連接點(diǎn)。相反,如果異常是在 proceed
語(yǔ)句處 拋出的,那么我會(huì)讓異常通過(guò)而不會(huì)“隔離”它(即不吞掉它)。 因?yàn)樵诮ㄗh中沒(méi)有調(diào)用 proceed
的連接點(diǎn),因此不能編寫像我需要的那樣完全一般化的建議。不過(guò),可以用 Glassbox Inspector 的結(jié)構(gòu)提供它所使用的那種
around 建議的錯(cuò)誤隔離。在 Glassbox Inspector 中惟一的 around 建議總是構(gòu)造一個(gè)工人對(duì)象,然后對(duì)它調(diào)用 execute() 模板方法,這個(gè)方法調(diào)用 doExecute() 以完成原來(lái)的連接點(diǎn)。因此我將處理由監(jiān)視器中(AbstractRequestMonitor 或者它的任何子類)的 helper 方法或者由在 execute 或者 doExecute() 中的調(diào)用返回的所有異常。清單 10 顯示了我是如何擴(kuò)展 Glassbox Inspector 以處理錯(cuò)誤的: 清單 10. Glassbox Inspector 中的錯(cuò)誤處理
public aspect ErrorHandling { public pointcut scope() : within(glassbox.inspector..*) && !within(ErrorHandling+);
public pointcut inMonitor() : within(AbstractRequestMonitor+) || within(RequestContext+);
public pointcut voidReturnAdviceExecution() : adviceexecution() && if(((AdviceSignature)thisJoinPointStaticPart.getSignature()). getReturnType() == void.class);
protected pointcut monitorHelperExec() : inMonitor() && execution(* *(..)) && !execution(* execute(..)) && !execution(* doExecute(..)); protected pointcut monitorExecuteCall() : (inMonitor() && withincode(* execute(..)) || withincode(* doExecute(..))) && (call(* *(..)) && !call(* doExecute(..)) || call(new(..))); public pointcut handlingScope() : scope() && voidReturnAdviceExecution() || monitorHelperExec() || monitorExecuteCall();
/** * This advice ensures that errors in the monitoring code will not * poison the underlying application code. */ Object around() : handlingScope() { try { return proceed(); } catch (Throwable e) { handleError(e, thisJoinPointStaticPart); return null; } }
public synchronized void handleError(Throwable t, StaticPart joinPointStaticPart) { // log 1 in 1000 but don't rethrow ... }
/** * Count of errors: a weak map so we don't leak memory after apps * have been disposed of. */ private Map/* <JoinPoint.StaticPart, MutableInteger> */errorCount = new WeakHashMap(); ... }
|
關(guān)于錯(cuò)誤隔離層 從清單 10 中可以看到我首先定義了范圍的切點(diǎn),以限定 ErrorHandling 中的建議只應(yīng)用于 Glassbox Inspector 中的代碼,而不應(yīng)用到 ErrorHandling 方面本身。然后,我定義了 inMonitor() 切點(diǎn)以定義詞句上處于請(qǐng)求監(jiān)視器中的代碼(包括所有工人對(duì)象)。我定義了 voidReturnAdviceExecution() 切點(diǎn),通過(guò)對(duì)建議執(zhí)行的簽名使用一個(gè)基于 if 的測(cè)試以匹配建議執(zhí)行。before 和 after 建議總是返回 void ,而 Glassbox Inspector around 建議不會(huì)這樣做。對(duì)于我的系統(tǒng),這相當(dāng)于匹配 before 和 after 建議而不是 around 建議的建議執(zhí)行。 下一步,我定義 monitorHelperExec() 以匹配監(jiān)視器中 helper 方法的執(zhí)行(即所有不是 execute() 或者 doExecute() 的調(diào)用)。我定義了 monitorExecuteCalls() 為監(jiān)視器中所有在
execute() 或者 doExecute() 方法中的調(diào)用,而不是對(duì) doExecute() 本身的調(diào)用。這些切點(diǎn)結(jié)合在 handlingScope() 中以定義我要處理異常的連接點(diǎn)。 最終的效果是,在所有 before 或者 after 建議和監(jiān)視 around 建議處處理異常以避免錯(cuò)誤影響應(yīng)用程序代碼,但是又不會(huì)吞掉應(yīng)用程序異常。為了處理異常,我捕獲所有由進(jìn)程拋出的 Throwable ,然后委派給 helper 方法以在第一次遇到時(shí)記錄它們。然后建議返回,并且不拋出異常。Glassbox Inspector 還使用 AspectJ 的 declare
soft 格式向編譯器表明它在建議中處理了 checked 異常。 總之,我可以創(chuàng)建一個(gè)非常有效的錯(cuò)誤隔離層。在 Glassbox Inspector 中增加這一功能所需要的修改開(kāi)銷很小,從后面的性能測(cè)量中就可以看出來(lái)。
運(yùn)行時(shí)控制 構(gòu)
建了一個(gè)有用的監(jiān)視基礎(chǔ)架構(gòu)后,現(xiàn)在我想在不重新啟動(dòng)服務(wù)器的條件下增加對(duì)所運(yùn)行的監(jiān)視的運(yùn)行時(shí)控制。運(yùn)行時(shí)控制基礎(chǔ)架構(gòu)還讓 Glassbox
Inspector 的用戶增加開(kāi)銷更大的監(jiān)視器以在系統(tǒng)出問(wèn)題時(shí)捕獲更詳細(xì)的數(shù)據(jù)。可以在運(yùn)行時(shí)動(dòng)態(tài)地啟用或者禁止整個(gè)監(jiān)視,也可以通過(guò)增加一個(gè) if 測(cè)試來(lái)檢查每一個(gè)建議的切點(diǎn)的簡(jiǎn)單標(biāo)志,從而分別啟用或者禁止個(gè)別建議。為了提供控制的常用框架,我使用清單 11 中的表達(dá)來(lái)啟用和禁止每一個(gè)方面的建議: 清單 11. 在運(yùn)行時(shí)啟用和禁止建議
public aspect AbstractRequestMonitor { ... protected pointcut scope() : if(true); protected pointcut monitorEnabled() : isMonitorEnabled() && scope(); protected abstract pointcut isMonitorEnabled();
public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } protected boolean enabled = true; }
public aspect JdbcStatementMonitor extends AbstractResourceMonitor { ... /** Monitor performance for executing a JDBC statement. */ Object around(final Statement statement) : statementExec(statement) && monitorEnabled() { ... } ... protected pointcut isMonitorEnabled() : if(aspectOf().isEnabled()); }
|
很自然地,我使用 JMX 提供必要的運(yùn)行時(shí)控制,如清單 12 所示: 清單 12. 啟用監(jiān)視器的 JMX 管理
public aspect MonitorJmxManagement { /** * Management interface for monitors allows enabling and disabling * at runtime. */ public interface RequestMonitorMBean extends ManagedBean { public boolean isEnabled(); public void setEnabled(boolean enabled); }
/** * Make the {@link AbstractRequestMonitor} aspect implement * {@link RequestMonitorMBean}, so all instances can be managed */ declare parents: AbstractRequestMonitor implements MonitorMBean;
public String AbstractRequestMonitor.getOperationName() { return "control=monitor,type="+getClass().getName(); } public MBeanInfoAssembler AbstractRequestMonitor.getAssembler() { if (assembler == null) { initAssembler(); } return assembler; } ... }
|
MonitorJmxManagement 方面定義了監(jiān)視器的管理接口,它只包括一個(gè)屬性
enabled。 通過(guò) JMX 管理監(jiān)視器所需要的其他支持來(lái)自在 第一部分 中討論的 JmxManagement 方面。我還將一些常用代碼從 StatsJmxManagement 中轉(zhuǎn)移到這個(gè)共享類中。圖 2 顯示了用 JMX 在運(yùn)行時(shí)控制監(jiān)視的例子:
圖 2. 在運(yùn)行時(shí)控制監(jiān)視

部署 Glassbox Inspector 到
此為止,我已經(jīng)可以為應(yīng)用程序監(jiān)視部署這個(gè) Glassbox Inspector
了。我讓它盡可能只應(yīng)用到應(yīng)用程序代碼上。不過(guò),在許多情況下,將方面應(yīng)用到庫(kù)代碼是有利的。對(duì)于我工作的 Petclinic 和
JPetstore 示例應(yīng)用程序來(lái)說(shuō),JDBC 訪問(wèn)是由 Spring 和 iBatis 庫(kù)代碼分別處理的,在 Petclinic
中,代碼控制器邏輯是由 Spring
庫(kù)處理的。因此讓監(jiān)視方面影響應(yīng)用程序所使用的一些庫(kù)代碼是有好處的。有時(shí),可以通過(guò)對(duì)來(lái)自應(yīng)用程序代碼的調(diào)用進(jìn)行建議,或者在可以被建議的應(yīng)用程序代碼
中增加顯式鉤子(如繼承在庫(kù)類中定義的方法),從而避免對(duì)庫(kù)代碼的執(zhí)行進(jìn)行建議。 我可以選擇兩種基本方法將監(jiān)視織入到應(yīng)用程序中:在編譯時(shí)或者在裝載時(shí)。如果使用編譯時(shí)織入,那么處理二進(jìn)制 jar(例如,使用 ajc -inpath
命令行工具進(jìn)行織入)容易得多,不用從源代碼重新編譯庫(kù)。我在過(guò)去使用這種方法,它對(duì)應(yīng)用程序啟動(dòng)提供了最好的性能,并且只有很少的內(nèi)存開(kāi)銷。不過(guò),它會(huì)
顯著增加編譯環(huán)境的復(fù)雜性。一個(gè)問(wèn)題是要求 AspectJ 織入器能夠解析對(duì)包含在被織入的 jar 中的第三方類的引用。對(duì)于
Spring,這意味著包括 Hibernate、多種開(kāi)放源碼緩沖管理器和 Servlet API 這些 JAR。  | VM 中的 AOP 支持
BEA JRockIt 最近提供了一種原型 Java VM,它支持方面。這種集成帶來(lái)了 AOP 的高性能和后綁定(late-binding)支持,這是這兩個(gè)方面最好的事情。我期待著這種技術(shù)的成熟和其他 Java VM 創(chuàng)造者的貢獻(xiàn)。 |
|
對(duì)于 AspectJ 5 的另一種可行的方法是裝載時(shí)織入。使用這種方法可以分別編譯監(jiān)視方面,并加入一個(gè)小的部署描述符以定義這個(gè)方面用在什么地方。 裝載時(shí)織入 清
單 13 展示了部署描述符 aop.xml 的一個(gè)例子,它描述了裝載時(shí)織入應(yīng)用在什么地方。AspectJ 5 裝載時(shí)織入代理在系統(tǒng)內(nèi)所有類
ClassLoader 都可訪問(wèn)的一個(gè)目錄或者 jar 中尋找所有 META-INF/aop.xml 資源。這些方面可以被任何 Java
ClassLoader 代理(如 Java 5 JVMTI -javaagent 、JRockIt JVM 代理或者甚至
WebSphere 或者 WebLogic ClassLoader 插件程序)自動(dòng)裝載,以將方面應(yīng)用到系統(tǒng)中所有代碼或者部分代碼上。這使我可以對(duì)一個(gè)應(yīng)用程序或者一個(gè)服務(wù)器進(jìn)行監(jiān)視,而不用事先建立它,也不用改變編譯過(guò)程。 注
意(例如)Spring 框架中引用不在類路徑中的類的代碼永遠(yuǎn)也不裝載,因此這種方法不需要增加額外的 jar。在 2005 年 11
月撰寫本文的時(shí)候,AspectJ 5 Milestone 4 已經(jīng)發(fā)布,AspectJ 5
的最終版本預(yù)計(jì)在年底發(fā)布。裝載時(shí)織入在保持大型系統(tǒng)中的模塊獨(dú)立性方面非常有用,它為方面提供了 Java 類裝載為對(duì)象提供的同樣的運(yùn)行時(shí)解決方案。 清單 13. 裝載時(shí)織入配置
<aspectj> <weaver> <exclude within="org.springframework.jmx..*"/> <!-- don't reweave --> <exclude within="glassbox.inspector..*"/> </weaver> <aspects> <aspect name="glassbox.inspector.monitor.operation.AxisOperationMonitor"/> <aspect name="glassbox.inspector.monitor.operation.SpringMvcMonitor"/> <aspect ... <aspect name="glassbox.inspector.monitor.resource.JdbcStatementMonitor"/> <concrete-aspect name="glassbox.inspector.monitor.operation.CustomMvcMonitor " extends="glassbox.inspector.monitor.operation.TemplOperationMonitor"> <pointcut name="classControllerExecTarget" expression="execution(* com.ibatis.struts.BaseBean..*(..)) && cflow(execution(* com.ibatis.struts.BeanAction.execute(..)))"/> </concrete-aspect> </aspects> </aspectj>
|
清單 3 中的 aop.xml 文件首先定義 AspectJ 織入器在什么地方應(yīng)該和不應(yīng)該應(yīng)用方面。在默認(rèn)情況下,它影響所有通過(guò) ClassLoader 裝載的代碼。我排除了系統(tǒng)的某些部分,在那些地方不需要裝載時(shí)織入以加快啟動(dòng)時(shí)間。 AOP 配置文件然后列出要使用的所有方面。目前,這個(gè)方面清單必須手工維護(hù),這通常是容易出錯(cuò)的地方。幸運(yùn)的是,AspectJ 5 的最終版本計(jì)劃包含在編譯方面時(shí)生成這些清單的工具。 最后注意,這個(gè)示例文件包括一個(gè) XML 定義的方面 CustomMVCMonitor 。
這個(gè)方面擴(kuò)展了一個(gè)抽象方面以監(jiān)視一個(gè)特定于應(yīng)用程序的框架。基本上,它只定義了一個(gè)針對(duì)要監(jiān)視的操作的切點(diǎn)。這表明了如何在不編譯任何代碼的情況下擴(kuò)展
Glassbox
Inspector 以監(jiān)視自定義代碼。還可以通過(guò)將監(jiān)視方面從 aop.xml
文件中刪除而不讓它們運(yùn)行。如果以這種方式取消它們的部署,可以降低開(kāi)銷,但是不能在不重新啟動(dòng)應(yīng)用程序(或者服務(wù)器)的條件下重新啟用它們。這種技術(shù)還
可以用于限制監(jiān)視。例如,可以定義一個(gè)更狹窄的切點(diǎn)以監(jiān)視在一個(gè) XML 定義的方面中的 Struts 操作,并排除標(biāo)準(zhǔn)的 StrutsMonitor 。 運(yùn)行帶裝載時(shí)織入的 Tomcat 5.5 我將展示在 Tomcat 5.5 應(yīng)用服務(wù)器中部署 Glassbox Inspector。這個(gè)服務(wù)器默認(rèn)實(shí)現(xiàn)了 Java 5,因此我使用
Java 5 的 -javaagent 啟動(dòng)參數(shù)調(diào)用 AspectJ 5 裝載時(shí)織入。(請(qǐng)參閱 參考資料 以了解關(guān)于對(duì)更早版本的 Java 語(yǔ)言使用 AspectJ 裝載時(shí)織入的知識(shí)。)要使用這個(gè)功能,只需要對(duì)啟動(dòng)命令增加一個(gè)新的 java 選項(xiàng)標(biāo)志。這通常是通過(guò)編譯一個(gè) shell 腳本完成的。例如,可以在 Windows 計(jì)算機(jī)中的 setclasspath.bat 的開(kāi)始處增加以下一行: set JAVA_OPTS=-javaagent:%CATALINA_HOME%\common\lib\aspectjweaver.jar -Xmx256m
|
可以對(duì)于 Linux 或者其他 Unix 操作系統(tǒng)的 setclasspath.sh 腳本做類似的修改。 這
一設(shè)置就可以在 Tomcat VM 中允許裝載時(shí)織入。要監(jiān)視 Web
應(yīng)用程序,可以像通常那樣編譯它,并在以后在 WEB-INF/lib 目錄中增加
glassboxInspector.jar 文件及其依賴文件。AspectJ 織入器然后尋找我部署的 aop.xml
文件,并保證它定義的方面在應(yīng)用程序裝載時(shí)被織入。另一種方法是,將 glassboxInspector.jar 和文件加到 Tomcat 的
shared/lib 文件夾中。這會(huì)增加對(duì)服務(wù)器中所有 web 應(yīng)用程序的監(jiān)視,而不需要為每一個(gè)應(yīng)用程序增加這個(gè)
jar。這種解決方案類似于為應(yīng)用服務(wù)器中的一個(gè)應(yīng)用程序或者所有應(yīng)用程序增加任何其他庫(kù)功能。我甚至可以將這個(gè)監(jiān)視器放到系統(tǒng)類路徑中或者
Tomcat 的 common/lib 文件夾中。這樣就可以監(jiān)視 Tomcat 的內(nèi)部,不過(guò)我看不出這有什么必要,而且這樣做會(huì)增加啟動(dòng)時(shí)間。 我選擇一次性地在整個(gè)服務(wù)器上部署監(jiān)視。一般來(lái)說(shuō),應(yīng)用服務(wù)器是作為整體管理的,最好對(duì)服務(wù)器上的所有應(yīng)用程序有一致性的管理和監(jiān)視能力。
性能和內(nèi)存使用 裝
載時(shí)織入的簡(jiǎn)單性的代價(jià)是啟動(dòng)應(yīng)用程序需要更多的時(shí)間。我一直與 Alexandre Vasseur 和 Matthew Webster
進(jìn)行努力,盡量在 AspectJ 5
發(fā)布之前優(yōu)化裝載時(shí)織入的性能。開(kāi)銷已經(jīng)不算大了,我們找到了一些可以加入的優(yōu)化。我預(yù)計(jì)裝載時(shí)織入性能在下一年仍然會(huì)繼續(xù)進(jìn)一步優(yōu)化。同樣,這些測(cè)試是
對(duì) Glassbox
Inspector 的 alpha 版進(jìn)行的。我預(yù)計(jì)在調(diào)優(yōu) Glassbox Inspector 和 AspectJ
裝載時(shí)織入后,開(kāi)銷會(huì)顯著降低。我計(jì)劃優(yōu)化 Glassbox Inspector 以使用
java.util.concurrent 庫(kù),使用的方面可以根據(jù)系統(tǒng)運(yùn)行的是哪種 Java VM,使用多個(gè)實(shí)現(xiàn)中最有效的一種。 我
對(duì) Windows XP 工作站上 2005 年 10 月開(kāi)發(fā)版本的 Aspect J(1.5.0 M4
之后)的服務(wù)器啟動(dòng)時(shí)間和每個(gè)請(qǐng)求的平均時(shí)間做了一些簡(jiǎn)單的測(cè)量。這個(gè)系統(tǒng)是 Inspector 的開(kāi)發(fā)版本(alpha 1
之后)。在我的測(cè)試中,在 JRockIt 1.5.0_03 Java VM 上運(yùn)行 Tomcat 5.5.9,并自動(dòng)啟動(dòng) iBatis 1.3
JPetstore、Spring 1.2.1 Petclinic 和 Duke 的 Bookstore 應(yīng)用程序。 當(dāng)系統(tǒng)啟動(dòng)后,端
到端的響應(yīng)時(shí)間開(kāi)銷很小。50 位用戶同時(shí)運(yùn)行在同一臺(tái)計(jì)算機(jī)中時(shí),平均服務(wù)器響應(yīng)時(shí)間增加了 10 毫秒。不過(guò),帶裝載時(shí)織入的啟動(dòng)用了約 15
秒,包括織入時(shí)間。對(duì)應(yīng)地,不帶織入的啟動(dòng)用了大約 5 秒。最后,使用裝載時(shí)織入顯著增加了內(nèi)存開(kāi)銷:有織入與沒(méi)織入時(shí)相比,Java
進(jìn)程使用的內(nèi)存增加了 100%。 表 1 顯示了當(dāng)啟用 Glassbox Inspector 并帶裝載時(shí)織入時(shí)、部署但禁用了裝載時(shí)織入時(shí)以及沒(méi)有部署時(shí),每個(gè)請(qǐng)求的開(kāi)銷、凈啟動(dòng)時(shí)間和內(nèi)存使用: 表 1. 帶裝載時(shí)織入時(shí)的開(kāi)銷監(jiān)視 | 平均端到端 響應(yīng)時(shí)間(毫秒) | 服務(wù)器啟動(dòng) 時(shí)間(毫秒) | 啟動(dòng)進(jìn)程 內(nèi)存使用(MB) |
---|
啟用 | 20 | 15 | 135 | 禁用(在運(yùn)行時(shí)) | 10 | 130 | 未部署 | 10 | 5 | 65 |
結(jié)束語(yǔ) 在
這篇由兩部分組成的文章中,我向您展示了如何用 AspectJ
處理復(fù)雜的橫切問(wèn)題(在這里是應(yīng)用程序監(jiān)視),并逐步構(gòu)造解決方案。即使您在下一個(gè)監(jiān)視解決方案中不選擇使用面向方面的方法,這里所講的許多內(nèi)容和技巧也
會(huì)證明是很有價(jià)值的。在我的其他 AspectJ 項(xiàng)目中發(fā)現(xiàn)它們顯然是很有用的。 就是說(shuō),我希望本文提供了使用面向方面的代碼進(jìn)行監(jiān)視的
好例子。與傳統(tǒng)的分散而且糾纏的測(cè)試代碼相比,我編寫的方面非常容易擴(kuò)展和改編,它們提供了獲得關(guān)于生產(chǎn)應(yīng)用程序的數(shù)據(jù)的更好方法。如果沒(méi)有方面,那么實(shí)
現(xiàn)在這里描述的復(fù)雜功能已經(jīng)非常困難,更不用說(shuō)隨著對(duì)生產(chǎn)應(yīng)用程序的了解加深而改寫它了。 對(duì) Glassbox Inspector 監(jiān)視基礎(chǔ)架構(gòu)的開(kāi)發(fā)和分析不會(huì)隨著本文而結(jié)束。這個(gè)項(xiàng)目正在尋求多個(gè)領(lǐng)域的貢獻(xiàn),如: - 捕獲關(guān)于所發(fā)生情況的更多上下文(例如,堆棧跟蹤和參數(shù)),特別是像故障或者不正常響應(yīng)時(shí)間這樣的反常結(jié)果。
- 監(jiān)視更多操作、資源,如 JMS 和 EJB,并擴(kuò)展 Inspector 對(duì)監(jiān)視 XML 文檔處理的基本支持。
- 處理分布式監(jiān)視,以便跨集群應(yīng)用程序跟蹤信息,并跨分布式調(diào)用對(duì)信息進(jìn)行關(guān)聯(lián)。
- 利用 Java 5 管理信息,如 CPU 時(shí)間或者特定于線程的統(tǒng)計(jì)。
- 使用應(yīng)用服務(wù)器 JMX 統(tǒng)計(jì),如線程池。
- 捕獲歷史和趨勢(shì),帶持久化存儲(chǔ)和報(bào)告。
- 使用 JMX 提供報(bào)警,并提供統(tǒng)計(jì)匯總。累積 JMX 統(tǒng)計(jì)中關(guān)于嵌套的監(jiān)視器的關(guān)鍵信息會(huì)很有用。
- 監(jiān)視頂級(jí)資源信息(例如,調(diào)用服務(wù)或者連接到數(shù)據(jù)庫(kù)所花費(fèi)的總時(shí)間)。這可以提供更好的匯總數(shù)據(jù)。
- 提供不同程度的統(tǒng)計(jì)匯總(例如,一個(gè)請(qǐng)求所花時(shí)間的柱狀圖)。
- 適應(yīng)性地發(fā)現(xiàn)要跟蹤的相關(guān)參數(shù)(例如,對(duì)于未知的數(shù)據(jù)查詢或者 Servlet 請(qǐng)求)。
- 對(duì)于高層數(shù)據(jù)庫(kù)和服務(wù)訪問(wèn)框架提供資源監(jiān)視(如 Hibernate、TopLink、EJB 3 Persistence Managers、JAX-WS 等)。
- 允許取樣以改變(跨整個(gè)請(qǐng)求)所捕獲的數(shù)據(jù)量。
- 監(jiān)視對(duì)沒(méi)有綁定到 Servlet 的 Web 應(yīng)用程序的請(qǐng)求錯(cuò)誤,如 404 錯(cuò)誤。這是對(duì) Servlet 過(guò)濾器進(jìn)行建議的好例子,包括增加一個(gè)空的 Servlet 過(guò)濾器以匹配對(duì)一個(gè)應(yīng)用程序的所有請(qǐng)求。
- 監(jiān)視業(yè)務(wù)事件,如客戶購(gòu)買或者放棄購(gòu)物車。
現(xiàn)在該您了。我鼓勵(lì)您下載 Glassbox
Inspector 并試驗(yàn)它。如果您能為這個(gè)項(xiàng)目出力,不管是提供反饋意見(jiàn)、開(kāi)發(fā)其他的監(jiān)視器、沿上面建議的方向擴(kuò)展系統(tǒng),還是為該項(xiàng)目建立一個(gè)新的方向,我都會(huì)很高興。Glassbox
Inspector 的完整源代碼請(qǐng)參閱 下載。請(qǐng)參閱 參考資料 以了解更多關(guān)于這里討論的主題的內(nèi)容。 致謝 感
謝 Eugene Kuleshov、Nicholas Lesiecki、Eamonn McManus、Srinivas
Narayanan、Ramnivas Laddad、Will Edwards、Matt Hutton、David Pickering、Rob
Harrop、Alex Vasseur、Paul Sutter 和 Mik Kersten 對(duì)本文進(jìn)行審校并給予有見(jiàn)地的意見(jiàn)。
下載 描述 | 名字 | 大小 | 下載方法 |
---|
Source code | j-aopwork12source.zip | 72KB |
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),側(cè)重于性能管理和有效地使用面向方面編程。Ron 以前在 Xerox PARC 為
AspectJ 小組工作,在那里他領(lǐng)導(dǎo)了第一個(gè) AOP 實(shí)現(xiàn)項(xiàng)目并負(fù)責(zé)客戶的培訓(xùn),他還是 C-bridge 的創(chuàng)始人和
CTO,這家咨詢機(jī)構(gòu)采用 Java 的框架、XML 和其他互聯(lián)網(wǎng)技術(shù)提供企業(yè)應(yīng)用程序。Ron 經(jīng)常為各種會(huì)議和客戶進(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)系。
|
|