有了基本的面向方面的监视基础架构后,可以对它进行扩展以满足真实世界的监视要求。在这篇由两部分组成的文章的第二部分,Ron Bodkin 展示了如何在 Glassbox Inspector 中添加企业监视功能,包括监视多个应用程序、Web 服务和 Web 应用程序框架。他还展示了如何跟踪应用程序错误并在监视代码中包含它们,并展示了如何以编程方式部署和控制这个监视基础架构。
![]() |
|
我在本文的 第 1 部分 中给出的基本 Glassbox Inspector 只能监视简单的、基于 Servlet 的应用程序(如在 Duke 的 Bookstore 这个例子中),更系统化的框架应包括对流行的 Web 应用程序框架(如 Struts 和 Spring)的支持。如今的 Web 应用程序很少使用 Servlet 直接处理请求。这些框架通常将所有动态页请求委派给网关 Servlet,如 Spring 框架的 DispatcherServlet
,然后它再将请求处理委派给不同的控制器。与此类似,Struts ActionServlet
委派给作为控制器的 Action
子类。
在这篇探索用 AspectJ 进行性能监视的文章的第二部分中,我对 Glassbox Inspector 进行扩展,通过增加监视器以提供关于应用程序性能的更有意义的信息。这些监视器跟踪 Struts 和 Spring Web 应用程序框架的控制器之间的交互以及 Web 服务的响应和请求操作。我还扩展了系统以支持多个应用程序,并添加了一个错误处理层和在运行时方便地启用和禁止监视的能力。在文章的最后展示如何用装载时 织入部署 Glassbox Inspector,以及如何测量带来的开销。
为了理解本文中的例子,需要对 Spring MVC、Struts 和 Apache Axis for Web Services 有一些了解。Glassbox Inspector 监视基础架构的完整代码请参阅 下载。要下载运行本文中的例子所需要的 Aspectj、JMX 和 Tomcat 5 请参阅 参考资料。
为了得到范围更广泛的监视功能,我将 第 1 部分的清单 5 中大多数可重用的 Servlet 监视代码放到 AbstractOperationMonitor
方面中。很多情况下跟踪嵌套的请求很重要,包括跟踪在框架不同级别上的控制器以及跟踪委派的请求(如从转发到嵌套请求)的性能。清单 1 显示了我如何扩展 Glassbox Inspector 以跟踪对操作的嵌套请求:
清单 1. 可重用的操作监视方面
|
AbstractOperationMonitor
的第一部分扩展了本文第 1 部分中原来的 Servlet 监视器以查看嵌套操作的统计。使用嵌套操作使我们可以在分派 JSP 或者分解 Web 服务或者多方法应用程序控制器的不同方法这样的情况下跟踪资源占用。原来的 lookupStats()
方法现在检查父请求上下文。如果有父请求,那么它调用新方法 getOperationStats()
以获取它。否则,它调用新方法 getTopLevelStats()
,这个方法调用一个工厂以创建一个新的 OperationStats
。使用工厂可以保证我的监视基础架构不依赖于统计类的实现。
![]() ![]() |
![]()
|
在清单 1 中,我还加入了对运行在单个应用服务器中的多个应用程序的监视支持。主要是通过增加一个检查应用程序上下文的预查来做到这一点的。在检查顶级统计时,我调用一个监视器模板方法 getContextName()
,以确定操作关联的是哪一个应用程序。下面将会看到对于 Servlet 它是如何处理的。注意,向 getContextName()
方法传递了控制器的一个实例,这样就可以检查关联的应用程序上下文。
在抽象操作监视器中,还提供了具有具体建议的切点,它定义了监视请求的常用方法。对它做了扩展以便监视收到的请求,如 Struts 操作和收到的 Web 服务请求。清单 2 展示了一个代表性的例子:
清单 2. AbstractOperationMonitor 中的监视模板
|
classControllerExec()
的切点捕获所有类控制器处理请求的点,像 Servlet do 方法执行或者普通 Struts action execute 方法,在这里响应请求的对象的类确定要执行的操作。更准确地说, classControllerExec()
切点 定义了一个空的切点(它不会匹配任何连接点)。然后它提供一个具体建议,这个建议设置工人对象并返回对于这种情况正确的键值。这与使用一个抽象切点类似,其中子方面必须覆盖切点以使用建议。不过在这里,我提供了永远不匹配的默认定义。如果 AbstractOperationMonitor
的子方面不需要监视类控制器,那么它不覆盖这个切点就行了。如果它需要监视类控制器,那么它就提供什么时候监视一个点的定义。
![]() ![]() |
![]()
|
methodSignatureControllerExec()
切点和关联的建议类似:它们提供具体化操作监视方面的方法,以根据连接点上的签名匹配分派到不同方法的控制器。
清单 3 展示了扩展 AbstractOperationMonitor
以监视 Struts 和 Spring MVC 操作的具体方面:
清单 3. 监视 Struts 和 Spring MVC 框架
|
关于这两个方面首先要注意的是它们非常简洁。它们只是扩展了操作监视器中的两个切点以具体地监视它们特定的 API。因为它们的简洁性,也可以在 XML 中定义这两个具体工作监视器,而不用编译。关于 AspectJ 5 的这个功能的例子请参阅 清单 10。
关于 StrutsMonitor 和 SpringMvcMonitor
清单 3 中的 StrutsMonitor
方面设计为同时使用老版本和新版本的 Struts API: Struts 1.0 操作是通过调用 perform()
而调用的,而 Struts 1.1 操作是通过调用 execute()
而调用的。我将正常操作类的具体子类作为 Class
控制器跟踪:只有执行对象的类才是关注的。不过,如果一个类扩展了 DispatchAction
,Struts 可以让控制器分派给这个类中的多个方法。我通过匹配 DispatchAction
子类中所有具有 Struts 操作签名的方法监视在这些分派操作中执行的各个方法。这种方式使我可以分别跟踪每一个不同控制器方法的统计。
我用 iBatis JPetStore 1.3 示例应用程序(请参阅 参考资料) 测试了 StrutsMonitor
。与许多应用程序一样,它用自己的一个小框架扩展了 Struts:公共基本操作有一个名为 perform()
的方法,它向 helper 分派用 doPerform()
作为模板方法的操作。不过,不需要跟踪这些模板方法的执行:类级别的控制器会识别在 execute()
方法中执行的 Action
的特定子类,这足以区分它们了。
SpringMvcMonitor
方面与 StrutsMonitor
有一点很类似,它们将所有 Controller
对象作为类控制器,监视它什么时候执行 handleRequest()
。它还监视 MultiActionController
或者它的任何子类中具有 Spring 控制器方法签名的公共方法的执行。例如,我在这段代码中分别监视 welcomeHandler()
和 ownerHandler()
的执行:
|
当然,可以容易地扩展这种方法以处理其他的框架,包括自定义框架,甚至前面描述过的用 XML 定义的方面。
注意 StrutsMonitor
方面像我的大多数监视方面一样,设计为针对常见组件的多个版本。它的切点匹配不同版本的 Struts,它们可以引入那些在织入时(例如在装载时)还不可用的类型。只需在像字段签名或者类型模式这样的静态可解析的上下文中使用类名就可得到这样 的切点。如果在动态上下文中(如 this、target 或者 args)中使用类名,那么就要求织入器能够访问类型。注意 Struts 监视器将 this
切点指示符绑定到 Object
的一个实例,这不需要有 Struts JAR。虽然有些别扭(因为它失去了编译时类型检查的好处),但是这种方法对于部署可重用方面时要求有许多依赖库的情况更有利。
不过,引用类或者访问其成员的(如通过调用一个方法)的建议或者其他可执行代码需要遵守 Java 代码的常规可移植性规范。如果建议必须调用在 API 的不同版本中有变化的方法,那么可以使用 Java 反射检查是否有不同的方法并执行它们,例如,对一个操作方法显式地调用 perform()
或者 execute()
。注意,如果只想在一个 around 建议中的连接点处使用原来的 execute()
或者 perform()
方法,那么 AspectJ 会替我处理这种情况,不需要做任何特别的工作。实际上,许多 API 都提供了向后兼容性,因此可以针对要支持的老版本编译,它在新版本中也能工作。
通常,方面不会执行触发装载一个类的代码,除非程序的执行总是会装载这个类,例如,引用一个类的建议在这个类要执行时会被触发。保留这个属性对于涉 及可选类的库方面很重要。对于 Glassbox Inspector,我不想让被监视的应用程序加入可能监视的某个版本的 Struts 或者其他库。
清单 4 展示了重构后的本文第 1 部分中的 ServletMonitor
,它使用抽象操作监视器作为一个基本方面。我通过扩展 classControllerExec()
继续按类跟踪 Servlet。增加了通过监视 jspService()
方法对监视 JSP 的支持。因为我想要确定 Web 应用程序的名字,因此设置 ServletMonitor
覆盖 getContext()
方法以检查 Servlet 上下文的名字。
我到目前为止所实现的所有其他 Web 应用程序最终是由 Servlet 调用的,因此只有这些控制器需要提供应用程序上下文的一个实现。特别是,我的 Web 应用程序框架监视器是嵌套在它们的分派 Servlet 调用中的。在进一步扩展 Glassbox Inspector 以监视其他操作(如收到的 Web 服务请求)时,将需要提供操作名作为不同应用程序的上下文。如果要扩展框架以监视收到的 JMS 消息或者 EJB 请求,那么这个框架还要为那些资源提供应用程序上下文。
清单 4. 扩展 ServletMonitor
|
![]() ![]() |
![]()
|
在 第 1 部分中,我扩展了 Glassbox 监视基础架构以提供嵌套的统计,如 Servlet 请求的连接中的 JDBC 语句。在这里,我分析了如何更新 StatsJmxManagement
方面以提供这些嵌套统计的复合名。我还展示如何将应用程序名加入到这个复合名中。例如,数据库语句的统计可以用以下字符串命名:
|
这个字符串使用给定信息的所有前级统计的性能统计的描述和名字。以这种方式命名统计使 JMX 工具可以自然地将相关的信息组织并显示在一起。像 JConsole 这样的 JMX 工具使用结构化的名字来组织常见的元素,更容易以层次化的方法浏览,如 图 1 所示。清单 5 展示了为支持这个功能需要对 StatsJmxManagement
进行的更新;
清单 5. 更新 StatsJmxManagement 以支持嵌套的统计
|
清单 5 展示了我如何设置 StatsJmxManagement
以允许不同的命名策略。为了使用像 JConsole 这样的 GUI 工具,我列出了一个 GuiFriendlyStatsJmxNameStrategy
,它返回如清单 5 和图 1 所示的名字。除此之外,完整的代码包括另一个可选的 CanonicalStatsJmxNameStrategy
,它遵守 JMX 建议的 MBean 命名方式,但是在我所试过的所有 JMX 工具中不能显示。
用于构建这些名字的基本方法是,创建一个字符串缓冲区并递归地得到父统计的操作名,然后在后面加上这个名字。对于顶级操作统计,首先加入上下文(应用程序)名。为了支持这种方法,我在 PerfStats
中增加了 getDescription()
方法。还更新了操作和资源统计实现以设置合适的值(如 operation, operation1, .., resource, and request
)。下载 Glassbox Inspector 源代码以查看所有细节和另一个可选的 CanonicalStatsJmxNameStrategy
。
![]() ![]() |
![]()
|
Glassbox Inspector 框架现在很容易扩展以支持新类型的请求(操作)以及新资源。监视提供的(收到的)远程服务操作与监视 Web 应用程序框架很相似:本文的源代码给出了监视用 Apache Axis 实现的服务的一个例子。
只需要很少的调整就可以将这个 Glassbox Inspector 扩展为监视应用程序之外的远程服务的使用。随着具有面向服务体系结构的应用程序越来越常见,跟踪外部控制的服务的可靠性及确定应用程序问题的相互关系越来 越重要了。将这些数据与应用程序上下文(如执行的 Web 操作和在其他资源上花费的时间)相关联是迅速找出应用程序故障根源的重要方法。这种分析与使用 SOAP 处理程序孤立地监视 Web 服务调用有很大不同,因为它将实现信息与外部请求的性能连接到一起了。
清单 6 展示了一个远程调用监视器,它监视通过 JAX-RPC API 对 Web 服务的调用以及 RMI 调用(它本身通常是远程的):
清单 6. 监视远程调用
|
注意,只需要做很少的工作就可以支持这种功能,大多数支持包含在 AbstractRequestMonitor
中。我只是定义远程代理调用为对任何实现了 Remote
接口的公共方法的调用,这个方法可以抛出 RemoteException
。完成这个小小的改变后,就可以使用 Worker Object 模式(请参阅 第 1 部分)监视远程调用,提供一个以类名和被调用的对象的方法名为基础的名字。RemoteCallMonitor
方面使用一个 helper 方法寻找顶级资源统计,它是从 JdbcConnectionMonitor
中的工人对象中提取的,放入这个新的公共基本方面:AbstractResourceMonitor
。
自然,可以进一步扩展这种方法以监视其他 Web 服务调用以及流行框架(如 Apache Axis)上方法的执行。我还可以使用这种技术监视 XML 处理所花费的时间,这对于任何有大量 XML 的应用程序是很有用的。许多利用 Web 服务的应用程序也可受益于这种监视。
![]() ![]() |
![]()
|
合乎逻辑的下一步是扩展 Glassbox Inspector 系统以监视应用程序故障。我首先在系统退出一个监视点时通过抛出一个异常(更准确地说是任何 Throwable
)来记录故障。故障记录为跟踪 Web 操作和数据库连接工作提供了有用的信息:这些操作中的任何一个抛出 Throwable
在几乎任何情况下都表示有问题。对于 JDBC 语句和其他资源访问,throwable 通常表明一个真正的应用程序故障,但是在有些情况下,它是正常程序逻辑的一部分。例如,试图用一个已经有的名字注册会在插入时触发异常。为了照顾到这种情 况,我对每个监视使用了一个可配置的策略,以确定给定的 throwable 是真正的错误还是作为正常的退出条件接受。
清单 7 包含了使我可以配置 AbstractRequestMonitor
以确定 Throwable
是否为故障的建议和改动:
清单 7. 增加可扩展的故障检测
|
清单 7 中最后的效果是,在通过抛出异常退出一个监视的连接点时记录一个计数。注意,我肯定是要识别那些通过抛出 Error
退出的情况,因为它们几乎肯定是故障!在这里,可以容易地扩展这个 AbstractRequestMonitor
以跟踪部分或者所有异常、堆栈踪迹和在发生异常时当前对象和参数值。如果这样做了,那么就需要有一种保证不记录敏感信息,如密码、信用卡号或者个人身份信息的方法。为了完成这种集成,我对 AbstractRequestMonitor
做了小小的修改。细节请参阅 文章源代码。
基于前面的讨论,您可能奇怪如何能配置 Glassbox Inspector 以识别出某些 throwable 不是错误。最容易的方法是传递一个对于给定监视方法(如 JdbcStatementMonitor
)总是返回 false 的策略。另一种简单的解决方案是检测某种特定的异常类型,如 NumberFormatException
,并指明它不是故障。这可以结合 Exception Conversion 模式(请参阅 参考资料)以将某些异常转换为有意义的层次结构。清单 8 就是如何使用异常转换以及故障检测策略的一个示例:
清单 8. 跟踪故障并使用异常转换
|
清单 8 的第一部分显示了一个简单的错误处理方面,它软化了来自 JDBC 调用的 SQLExceptions
,然后使用一个 Spring 框架 SQLExceptionTranslator
(如 SQLErrorCodeSQLExceptionTranslator
)将异常转换为有意义的(unchecked)异常。这个方面还用 AspectJ 的 declare precedence 格式声明优于 JdbcStatementMonitor
。这保证了在 JdbcStatementMonitor
跟踪返回的异常类型之前,DataAccessErrorHandling
已经转换了异常。
清单 8 的其他部分展示了一个示例策略,它只在几乎确实表明有故障(如无法访问资源)的条件下表明故障。特别是,它排除了并发错误(在设计良好的、多用户应用程序中会出现)和数据完整性破坏(如通常在注册新用户时发生)的情况。
要对故障检测有更多的控制,可能要有一个标志性接口或者一个方法或者类上的注释,以表明在正常的操作过程中,系统的某些部分会抛出 Exception
。这可结合使用 declare parents 或者 AspectJ 5 新的 declare annotation 格式捕获模块化规则,表明抛出一个 Throwable
是否表明故障。在大多数情况下,我使用的简单规则对于粗粒度的监视是合适的,但是要避免不正确的警报,最好有更多的灵活性。
应用程序有可能在出现故障时并不抛出异常。一种最常见的情况是当控制器处理异常并将请求转发给一个错误页时。另一种情况是当对于页的 HTTP 响应指明一个错误时。清单 9 更新了清单 4 中的 ServletMonitor
以检测这种情况。我还对 AbstractRequestMonitor.RequestContext
工人对象做了小小的修改以设置错误上下文,并在设置错误上下文后记录故障。
清单 9. 检测常见 Web 故障
|
![]() ![]() |
![]()
|
有些时候,当我构建 Glassbox Inspector 时,需要让库方面更有可扩展性。在下面列出了一些可用的常见选项。其中一些选项我已经展示过了,它们都很有用:
- 提供一个抽象切点以定义方面用在什么地方(例如,一个
scope()
定义)。 - 提供一个空的切点,必须为要使用的某个建议覆盖它(如在 清单 2 中的
AbstractOperationMonitor
)。在许多情况下,少数的具体方面需要使用给定的建议,因此有一个空的默认值更有用。像这样的模板切点有一个空的默认实现与模板方面有一个空的默认实现很相像。 - 利用标志性接口表明方面应用于哪些类型。因为接口是继承的,所以这些类型的所有子类型都包括在内了。
- 利用 Java 5 注释表明方面应用于哪些类型和/或方法。这种方法使您可以细化对方面应用领域的控制,可以按方法定义,以及包括类而不包括它的子类。
- 提供运行时行为配置,有测试一种功能是否启用的逻辑。例子请参阅 运行时控制。
在应用程序中集成了这些扩展后,我就完成了对 Glassbox Inspector 监视的扩展(至少对于本文的目的 —— 本文结束的地方就是开放源码项目开始的地方!)。我现在有了一个将应用程序性能按照逻辑组织到一起的有用视图,这样就可以通过 web 应用程序中的请求用标准 JMX 客户机观察总体性能统计和错误。对于有疑问的地方,我还可以深挖数据库或者 Web 服务的性能和可靠性。图 1 显示了在 Spring Petclinic、iBatis JPetstore 和 Duke 的 Bookstore Web 应用程序例子中使用的 Glassbox Inspector 监视工具:
图 1. 使用中的完整监视基础架构的快照
![](https://i-blog.csdnimg.cn/blog_migrate/3ca50c28e1a777f9c601c1b48a73fa76.png)
![]() ![]() |
![]()
|
在这之后,我将重点放到如何更容易地部署扩展的 Glassbox Inspector 监视基础架构上。迈向用于企业管理的基础架构的第一步是扩展 Glassbox Inspector 来实现错误隔离。传统上,开发人员尽可能保证像性能监视这样的功能不会产生 影响被监视应用程序的错误。不过,监视代码中未预料到的行为(如未预计到的 null 值、抛出异常的代理等)有时会在监视代码自身中产生错误。在这些情况下,隔离 错误的能力可提高监视实现的健壮性。系统初始化时出现的问题可能造成错误隔离方面不能缓冲的错误,但是这种问题通常是立即发生的,并容易在开发时捕获,使得它的风险降低了很多。
我的基本错误隔离策略是,捕获并处理所有可能从建议执行连接点抛出的 Throwable
,这样它们就不会影响底层的应用程序。注意,建议执行切点匹配一些建议执行的连接点,因此这个方面将影响其他方面的行为。这种策略对于 before 和 after 建议都可以很好地工作,但是 around 建议实现错误隔离要复杂一点儿。如果在达到 proceed
语句之前抛出异常,那么我会仍然继续原来的连接点。相反,如果异常是在 proceed
语句处 抛出的,那么我会让异常通过而不会“隔离”它(即不吞掉它)。
因为在建议中没有调用 proceed
的连接点,因此不能编写像我需要的那样完全一般化的建议。不过,可以用 Glassbox Inspector 的结构提供它所使用的那种 around 建议的错误隔离。在 Glassbox Inspector 中惟一的 around 建议总是构造一个工人对象,然后对它调用 execute()
模板方法,这个方法调用 doExecute()
以完成原来的连接点。因此我将处理由监视器中(AbstractRequestMonitor
或者它的任何子类)的 helper 方法或者由在 execute
或者 doExecute()
中的调用返回的所有异常。清单 10 显示了我是如何扩展 Glassbox Inspector 以处理错误的:
清单 10. Glassbox Inspector 中的错误处理
|
从清单 10 中可以看到我首先定义了范围的切点,以限定 ErrorHandling
中的建议只应用于 Glassbox Inspector 中的代码,而不应用到 ErrorHandling
方面本身。然后,我定义了 inMonitor()
切点以定义词句上处于请求监视器中的代码(包括所有工人对象)。我定义了 voidReturnAdviceExecution()
切点,通过对建议执行的签名使用一个基于 if 的测试以匹配建议执行。before 和 after 建议总是返回 void
,而 Glassbox Inspector around 建议不会这样做。对于我的系统,这相当于匹配 before 和 after 建议而不是 around 建议的建议执行。
下一步,我定义 monitorHelperExec()
以匹配监视器中 helper 方法的执行(即所有不是 execute()
或者 doExecute()
的调用)。我定义了 monitorExecuteCalls()
为监视器中所有在 execute()
或者 doExecute()
方法中的调用,而不是对 doExecute()
本身的调用。这些切点结合在 handlingScope()
中以定义我要处理异常的连接点。
最终的效果是,在所有 before 或者 after 建议和监视 around 建议处处理异常以避免错误影响应用程序代码,但是又不会吞掉应用程序异常。为了处理异常,我捕获所有由进程抛出的 Throwable
,然后委派给 helper 方法以在第一次遇到时记录它们。然后建议返回,并且不抛出异常。Glassbox Inspector 还使用 AspectJ 的 declare soft
格式向编译器表明它在建议中处理了 checked 异常。
总之,我可以创建一个非常有效的错误隔离层。在 Glassbox Inspector 中增加这一功能所需要的修改开销很小,从后面的性能测量中就可以看出来。
![]() ![]() |
![]()
|
构建了一个有用的监视基础架构后,现在我想在不重新启动服务器的条件下增加对所运行的监视的运行时控制。运行时控制基础架构还让 Glassbox Inspector 的用户增加开销更大的监视器以在系统出问题时捕获更详细的数据。可以在运行时动态地启用或者禁止整个监视,也可以通过增加一个 if 测试来检查每一个建议的切点的简单标志,从而分别启用或者禁止个别建议。为了提供控制的常用框架,我使用清单 11 中的表达来启用和禁止每一个方面的建议:
清单 11. 在运行时启用和禁止建议
|
很自然地,我使用 JMX 提供必要的运行时控制,如清单 12 所示:
清单 12. 启用监视器的 JMX 管理
|
MonitorJmxManagement
方面定义了监视器的管理接口,它只包括一个属性 enabled。 通过 JMX 管理监视器所需要的其他支持来自在 第一部分 中讨论的 JmxManagement
方面。我还将一些常用代码从 StatsJmxManagement
中转移到这个共享类中。图 2 显示了用 JMX 在运行时控制监视的例子:
图 2. 在运行时控制监视
![](https://i-blog.csdnimg.cn/blog_migrate/0b48f86b4cdccf2193a1cf3fcc1e487f.png)
![]() ![]() |
![]()
|
到此为止,我已经可以为应用程序监视部署这个 Glassbox Inspector 了。我让它尽可能只应用到应用程序代码上。不过,在许多情况下,将方面应用到库代码是有利的。对于我工作的 Petclinic 和 JPetstore 示例应用程序来说,JDBC 访问是由 Spring 和 iBatis 库代码分别处理的,在 Petclinic 中,代码控制器逻辑是由 Spring 库处理的。因此让监视方面影响应用程序所使用的一些库代码是有好处的。有时,可以通过对来自应用程序代码的调用进行建议,或者在可以被建议的应用程序代码 中增加显式钩子(如继承在库类中定义的方法),从而避免对库代码的执行进行建议。
我可以选择两种基本方法将监视织入到应用程序中:在编译时或者在装载时。如果使用编译时织入,那么处理二进制 jar(例如,使用 ajc -inpath
命令行工具进行织入)容易得多,不用从源代码重新编译库。我在过去使用这种方法,它对应用程序启动提供了最好的性能,并且只有很少的内存开销。不过,它会 显著增加编译环境的复杂性。一个问题是要求 AspectJ 织入器能够解析对包含在被织入的 jar 中的第三方类的引用。对于 Spring,这意味着包括 Hibernate、多种开放源码缓冲管理器和 Servlet API 这些 JAR。
![]() |
|
对于 AspectJ 5 的另一种可行的方法是装载时织入。使用这种方法可以分别编译监视方面,并加入一个小的部署描述符以定义这个方面用在什么地方。
清单 13 展示了部署描述符 aop.xml 的一个例子,它描述了装载时织入应用在什么地方。AspectJ 5 装载时织入代理在系统内所有类 ClassLoader 都可访问的一个目录或者 jar 中寻找所有 META-INF/aop.xml 资源。这些方面可以被任何 Java ClassLoader 代理(如 Java 5 JVMTI -javaagent
、JRockIt JVM 代理或者甚至 WebSphere 或者 WebLogic ClassLoader 插件程序)自动装载,以将方面应用到系统中所有代码或者部分代码上。这使我可以对一个应用程序或者一个服务器进行监视,而不用事先建立它,也不用改变编译过程。
注意(例如)Spring 框架中引用不在类路径中的类的代码永远也不装载,因此这种方法不需要增加额外的 jar。在 2005 年 11 月撰写本文的时候,AspectJ 5 Milestone 4 已经发布,AspectJ 5 的最终版本预计在年底发布。装载时织入在保持大型系统中的模块独立性方面非常有用,它为方面提供了 Java 类装载为对象提供的同样的运行时解决方案。
清单 13. 装载时织入配置
|
清单 3 中的 aop.xml 文件首先定义 AspectJ 织入器在什么地方应该和不应该应用方面。在默认情况下,它影响所有通过 ClassLoader
装载的代码。我排除了系统的某些部分,在那些地方不需要装载时织入以加快启动时间。
AOP 配置文件然后列出要使用的所有方面。目前,这个方面清单必须手工维护,这通常是容易出错的地方。幸运的是,AspectJ 5 的最终版本计划包含在编译方面时生成这些清单的工具。
最后注意,这个示例文件包括一个 XML 定义的方面 CustomMVCMonitor
。这个方面扩 展了一个抽象方面以监视一个特定于应用程序的框架。基本上,它只定义了一个针对要监视的操作的切点。这表明了如何在不编译任何代码的情况下扩展 Glassbox Inspector 以监视自定义代码。还可以通过将监视方面从 aop.xml 文件中删除而不让它们运行。如果以这种方式取消它们的部署,可以降低开销,但是不能在不重新启动应用程序(或者服务器)的条件下重新启用它们。这种技术还 可以用于限制监视。例如,可以定义一个更狭窄的切点以监视在一个 XML 定义的方面中的 Struts 操作,并排除标准的 StrutsMonitor
。
我将展示在 Tomcat 5.5 应用服务器中部署 Glassbox Inspector。这个服务器默认实现了 Java 5,因此我使用 Java 5 的 -javaagent
启动参数调用 AspectJ 5 装载时织入。(请参阅 参考资料 以了解关于对更早版本的 Java 语言使用 AspectJ 装载时织入的知识。)要使用这个功能,只需要对启动命令增加一个新的 java 选项标志。这通常是通过编译一个 shell 脚本完成的。例如,可以在 Windows 计算机中的 setclasspath.bat 的开始处增加以下一行:
|
可以对于 Linux 或者其他 Unix 操作系统的 setclasspath.sh 脚本做类似的修改。
这一设置就可以在 Tomcat VM 中允许装载时织入。要监视 Web 应用程序,可以像通常那样编译它,并在以后在 WEB-INF/lib 目录中增加 glassboxInspector.jar 文件及其依赖文件。AspectJ 织入器然后寻找我部署的 aop.xml 文件,并保证它定义的方面在应用程序装载时被织入。另一种方法是,将 glassboxInspector.jar 和文件加到 Tomcat 的 shared/lib 文件夹中。这会增加对服务器中所有 web 应用程序的监视,而不需要为每一个应用程序增加这个 jar。这种解决方案类似于为应用服务器中的一个应用程序或者所有应用程序增加任何其他库功能。我甚至可以将这个监视器放到系统类路径中或者 Tomcat 的 common/lib 文件夹中。这样就可以监视 Tomcat 的内部,不过我看不出这有什么必要,而且这样做会增加启动时间。
我选择一次性地在整个服务器上部署监视。一般来说,应用服务器是作为整体管理的,最好对服务器上的所有应用程序有一致性的管理和监视能力。
![]() ![]() |
![]()
|
装载时织入的简单性的代价是启动应用程序需要更多的时间。我一直与 Alexandre Vasseur 和 Matthew Webster 进行努力,尽量在 AspectJ 5 发布之前优化装载时织入的性能。开销已经不算大了,我们找到了一些可以加入的优化。我预计装载时织入性能在下一年仍然会继续进一步优化。同样,这些测试是 对 Glassbox Inspector 的 alpha 版进行的。我预计在调优 Glassbox Inspector 和 AspectJ 装载时织入后,开销会显著降低。我计划优化 Glassbox Inspector 以使用 java.util.concurrent
库,使用的方面可以根据系统运行的是哪种 Java VM,使用多个实现中最有效的一种。
我对 Windows XP 工作站上 2005 年 10 月开发版本的 Aspect J(1.5.0 M4 之后)的服务器启动时间和每个请求的平均时间做了一些简单的测量。这个系统是 Inspector 的开发版本(alpha 1 之后)。在我的测试中,在 JRockIt 1.5.0_03 Java VM 上运行 Tomcat 5.5.9,并自动启动 iBatis 1.3 JPetstore、Spring 1.2.1 Petclinic 和 Duke 的 Bookstore 应用程序。
当系统启动后,端到端的响应时间开销很小。50 位用户同时运行在同一台计算机中时,平均服务器响应时间增加了 10 毫秒。不过,带装载时织入的启动用了约 15 秒,包括织入时间。对应地,不带织入的启动用了大约 5 秒。最后,使用装载时织入显著增加了内存开销:有织入与没织入时相比,Java 进程使用的内存增加了 100%。
表 1 显示了当启用 Glassbox Inspector 并带装载时织入时、部署但禁用了装载时织入时以及没有部署时,每个请求的开销、净启动时间和内存使用:
监视 | 平均端到端 响应时间(毫秒) | 服务器启动 时间(毫秒) | 启动进程 内存使用(MB) |
---|---|---|---|
启用 | 20 | 15 | 135 |
禁用(在运行时) | 10 | 130 | |
未部署 | 10 | 5 | 65 |
![]() ![]() |
![]()
|
在这篇由两部分组成的文章中,我向您展示了如何用 AspectJ 处理复杂的横切问题(在这里是应用程序监视),并逐步构造解决方案。即使您在下一个监视解决方案中不选择使用面向方面的方法,这里所讲的许多内容和技巧也 会证明是很有价值的。在我的其他 AspectJ 项目中发现它们显然是很有用的。
就是说,我希望本文提供了使用面向方面的代码进行监视的好例子。与传统的分散而且纠缠的测试代码相比,我编写的方面非常容易扩展和改编,它们提供了 获得关于生产应用程序的数据的更好方法。如果没有方面,那么实现在这里描述的复杂功能已经非常困难,更不用说随着对生产应用程序的了解加深而改写它了。
对 Glassbox Inspector 监视基础架构的开发和分析不会随着本文而结束。这个项目正在寻求多个领域的贡献,如:
- 捕获关于所发生情况的更多上下文(例如,堆栈跟踪和参数),特别是像故障或者不正常响应时间这样的反常结果。
- 监视更多操作、资源,如 JMS 和 EJB,并扩展 Inspector 对监视 XML 文档处理的基本支持。
- 处理分布式监视,以便跨集群应用程序跟踪信息,并跨分布式调用对信息进行关联。
- 利用 Java 5 管理信息,如 CPU 时间或者特定于线程的统计。
- 使用应用服务器 JMX 统计,如线程池。
- 捕获历史和趋势,带持久化存储和报告。
- 使用 JMX 提供报警,并提供统计汇总。累积 JMX 统计中关于嵌套的监视器的关键信息会很有用。
- 监视顶级资源信息(例如,调用服务或者连接到数据库所花费的总时间)。这可以提供更好的汇总数据。
- 提供不同程度的统计汇总(例如,一个请求所花时间的柱状图)。
- 适应性地发现要跟踪的相关参数(例如,对于未知的数据查询或者 Servlet 请求)。
- 对于高层数据库和服务访问框架提供资源监视(如 Hibernate、TopLink、EJB 3 Persistence Managers、JAX-WS 等)。
- 允许取样以改变(跨整个请求)所捕获的数据量。
- 监视对没有绑定到 Servlet 的 Web 应用程序的请求错误,如 404 错误。这是对 Servlet 过滤器进行建议的好例子,包括增加一个空的 Servlet 过滤器以匹配对一个应用程序的所有请求。
- 监视业务事件,如客户购买或者放弃购物车。
现在该您了。我鼓励您下载 Glassbox Inspector 并试验它。如果您能为这个项目出力,不管是提供反馈意见、开发其他的监视器、沿上面建议的方向扩展系统,还是为该项目建立一个新的方向,我都会很高兴。Glassbox Inspector 的完整源代码请参阅 下载。请参阅 参考资料 以了解更多关于这里讨论的主题的内容。
感谢 Eugene Kuleshov、Nicholas Lesiecki、Eamonn McManus、Srinivas Narayanan、Ramnivas Laddad、Will Edwards、Matt Hutton、David Pickering、Rob Harrop、Alex Vasseur、Paul Sutter 和 Mik Kersten 对本文进行审校并给予有见地的意见。