java 监控 编译_编译后检测和性能监控

从本系列文章的第1部分中可以知道,监视Java应用程序的可用性和性能以及它们在生产中的依赖性非常重要,以确保问题检测和加速诊断和分类。 您要监视的类的源代码级工具可能具有我在第1部分中讨论的优点,但通常是不允许的或不切实际的。 例如,许多有趣的监视点可能在第三方组件中,而这些组件没有源代码。 在第2部分中,我将重点介绍在不修改原始源代码的情况下检测Java类和资源的方法。

在源代码之外编织检测的选项包括:

  • 拦截
  • 类包装
  • 字节码检测

本文概述了使用第1部分中介绍的ITracer接口来实现性能数据跟踪的示例,这些示例说明了这些技术。

通过侦听进行Java检测

拦截的基本前提是通过拦截构造转移特定的调用模式,并收集有关入站和出站调用传递的信息。 基本的拦截器实现执行以下操作:

  1. 获取入站上调用请求的当前时间。
  2. 在出站响应中重新获取当前时间。
  3. 将经过的时间计算为两次测量的差值。
  4. 将调用的经过时间提交给应用程序性能管理(APM)系统。

此流程如图1所示:

图1.性能数据收集拦截器的基本流程
性能数据收集拦截器的基本流程

许多Java框架,例如Java平台企业版(Java EE),都对侦听堆栈提供了核心支持,其中,服务调用通过一系列预处理和后处理组件传递。 这些堆栈为将仪器注入执行路径提供了绝佳的机会,具有两个好处。 首先,您不需要修改目标类的源代码。 其次,只需将拦截器类添加到JVM的类路径并修改组件的部署描述符,就可以将检测器拦截器插入执行流。

拦截的核心指标

经过时间是通常在拦截器中收集的一种度量。 其他标准指标也符合拦截模式。 我将介绍支持这些指标的ITracer界面的两个新方面,因此在这里我将进行简短的绕道讨论。

使用拦截器时要考虑的典型度量标准是:

  • 经过的时间 :完成执行的平均时钟时间。
  • 每个间隔的调用次数 :调用目标的次数。
  • 每个时间间隔的响应数 :目标响应调用的次数。
  • 每个时间间隔的异常 :目标调用导致异常的次数。
  • 并发性 :并发执行目标的线程数。

也可以选择两个ThreadMXBean度量标准,但是它们的实用性有限并且收集成本略高:

  • 耗用的CPU时间 :这是线程在执行过程中消耗的CPU时间,以纳秒为单位。 尽管最初CPU利用率看似很有用,但除了作为一种趋势模式外,它并不是特别有见地。 或者,以更高的收集成本,可以计算出线程为执行消耗的全部CPU资源的百分比的近似值。
  • 阻塞/等待计数和时间 :等待表示由于特定线程调度而导致的同步或等待。 执行等待资源时,块是最常见的,例如对来自远程数据库的Java数据库连接(JDBC)调用的响应。 (有关适用这些度量的说明,请参见本文的JDBC工具部分。)

为了阐明如何收集ThreadMXBean指标,清单1是快速回溯到基于源的工具。 在此示例中,我将针对heavilyInstrumentedMethod方法实现一个重型工具集。

清单1.大量使用的方法的示例
protected static AtomicInteger concurrency = new AtomicInteger();
.
.
for(int x = 0; x < loops; x++) {
   tracer.startThreadInfoCapture(CPU+BLOCK+WAIT);
   int c = concurrency.incrementAndGet();
   tracer.trace(c, "Source Instrumentation", "heavilyInstrumentedMethod", 
      "Concurrent Invocations");
   try {
      // ===================================
      //   Here is the method
      // ===================================
      heavilyInstrumentedMethod(factor);
      // ===================================
      tracer.traceIncident("Source Instrumentation", 
         "heavilyInstrumentedMethod", "Responses");
   } catch (Exception e) {
      tracer.traceIncident("Source Instrumentation", 
         "heavilyInstrumentedMethod", "Exceptions");
   } finally {
      tracer.endThreadInfoCapture("Source Instrumentation", 
         "heavilyInstrumentedMethod");
      c = concurrency.decrementAndGet();
      tracer.trace(c, "Source Instrumentation", 
         "heavilyInstrumentedMethod", "Concurrent Invocations");
      tracer.traceIncident("Source Instrumentation", 
         "heavilyInstrumentedMethod", "Invocations");
   }
   try { Thread.sleep(200); } catch (InterruptedException e) { }
}

清单1引入了两个新的构造:

  • ThreadInfoCapture方法ThreadInfoCapture方法是一种方便的助手,用于获取目标调用之前和之后之间的ThreadMXBean度量标准的经过时间和增量。 startThreadInfoCapture捕获当前线程的基线,而endThreadInfoCapture计算增量和跟踪。 由于这些指标会不断增加,因此您必须先获取基准,然后再计算差异。 这种情况不适用于跟踪器的增量功能,因为每个线程的绝对值都不同,并且在运行的JVM中线程不是恒定的。 还要注意,跟踪器使用堆栈来维护基线,因此可以(小心地)嵌套调用。 收集此数据需要一定的成本。 图2显示了收集不同的ThreadMXBean指标集的相对经过时间:
    图2.收集ThreadMXBean指标的相对成本
    收集ThreadMXBean指标的相对成本

    尽管谨慎使用调用时的开销不会造成灾难性的影响,但遵循一些与记录日志相同的注意事项(例如不要在紧密循环内进行)是有用的。
  • 并发性 :要跟踪在任何给定时间通过此代码的线程数量,您需要创建一个既线程安全AtomicInteger被目标类的所有实例访问的计数器(在这种情况下为静态AtomicInteger 。 但这是一个非常棘手的情况,因为可能发生以下情况:多个类加载器已经加载了该类,从而使计数器成为非排他性的并且使测量完全混乱。 一种解决方案是将并发计数器维护在JVM中保证有唯一性的位置,例如平台代理中的MBean。

并发仅在检测目标是多线程或池化对象时适用。 但这在这些情况下是非常有价值的指标,我将在稍后针对Enterprise JavaBean(EJB)拦截器进一步解决。 EJB拦截器是接下来将要讨论的基于拦截的工具的几个示例中的第一个,并利用清单1中介绍的相同跟踪方法。

EJB 3拦截器

随着EJB 3的发布,拦截器已成为Java EE体系结构的标准配置。 (某些Java应用程序服务器已经支持EJB拦截器已有一段时间了。)大多数Java EE应用程序服务器的确为诸如EJB之类的主要组件提供了至少一些性能指标报告,但是您可能想要实现自己的,因为:

  • 您需要基于上下文或范围/阈值的跟踪。
  • 应用程序服务器指标很好,但是您希望您的指标在APM系统中,而不是孤立在应用程序服务器中。
  • 应用程序服务器指标不符合您的要求。

即使这样,根据您的APM系统和应用程序服务器的实现,某些工作可能已经为您完成。 例如,WebSphere®PMI通过Java管理扩展(JMX)公开服务器指标(请参阅参考资料 )。 即使您的APM供应商本身不提供自动读取此数据的功能,您在阅读本文后也将知道如何进行自动读取。

在下一个示例中,我将拦截器注入到名为org.aa4h.ejb.HibernateService的无状态会话Bean的上下文中。 EJB 3拦截器的要求和依赖性很轻:

  • 接口javax.interceptor.InvocationContext
  • 注释javax.interceptor.AroundInvoke
  • 目标方法 :具有签名public Object anyName (InvocationContext ic )的任何名称的方法

清单2显示了示例EJB的拦截方法:

清单2. EJB 3拦截器方法
@AroundInvoke
public Object trace(InvocationContext ctx) throws Exception {
   Object returnValue = null;
   int concur = concurrency.incrementAndGet();
   tracer.trace(concur, "EJB Interceptors", ctx.getTarget().getClass()
               .getName(), ctx.getMethod().getName(),
               "Concurrent Invocations");
   try {
      tracer.startThreadInfoCapture(CPU + BLOCK + WAIT);
      // ===================================
      //   This is the target.
      // ===================================
      returnValue = ctx.proceed();
      // ===================================
      tracer.traceIncident("EJB Interceptors", ctx.getTarget().getClass()
            .getName(), ctx.getMethod().getName(), "Responses");
      concur = concurrency.decrementAndGet();
      tracer.trace(concur, "EJB Interceptors", ctx.getTarget().getClass()
            .getName(), ctx.getMethod().getName(),
            "Concurrent Invocations");
      return returnValue;
   } catch (Exception e) {
      tracer.traceIncident("EJB Interceptors", ctx.getTarget().getClass()
            .getName(), ctx.getMethod().getName(), "Exceptions");
      throw e;
   } finally {
      tracer.endThreadInfoCapture("EJB Interceptors", ctx.getTarget()
            .getClass().getName(), ctx.getMethod().getName());
      tracer.traceIncident("EJB Interceptors", ctx.getTarget().getClass()
            .getName(), ctx.getMethod().getName(), "Invocations");
   }
}

清单1一样, 清单2包含了一个繁重的工具集,通常不建议这样做,但此处以示例显示。 清单2中需要注意的几点是:

  • @AroundInvoke批注通过包装EJB调用将方法标记为拦截器。
  • 方法调用将调用向上传递到最终目标或可能的下一个拦截器。 因此,在此方法之前获取测量基准,并在此方法之后进行跟踪。
  • 传递给trace方法的InvocationContext为拦截器提供了与调用有关的所有元数据,包括:
    • 目标对象
    • 目标方法名称
    • 传递的参数
    注意这一点很重要,因为此拦截器可以应用于许多不同的EJB,因此您无法假设要拦截的呼叫类型。 从拦截器内部访问元数据的来源至关重要:没有它,您将无法得知被拦截的呼叫。 您的指标可能显示许多有趣的趋势,但是对于它们所指的操作却高度不确定。

从工具的角度看,这些拦截器最有用的方面是可以通过修改部署描述符将它们应用于EJB。 清单3显示了示例EJB的ejb-jar.xml部署描述符:

清单3. EJB 3拦截器部署描述符
<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
   http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd" version="3.0">
   <interceptors>
      <interceptor>
         <interceptor-class>
    org.runtimemonitoring.interceptors.ejb.EJBTracingInterceptor
    </interceptor-class>
            <around-invoke>
               <method-name>trace</method-name>
            </around-invoke>
      </interceptor>
   </interceptors>
   <assembly-descriptor>
      <interceptor-binding>
         <ejb-name>AA4H-HibernateService</ejb-name>
         <interceptor-class>
    org.runtimemonitoring.interceptors.ejb.EJBTracingInterceptor
    </interceptor-class>
      </interceptor-binding>
   </assembly-descriptor>
</ejb-jar>

如前所述,检测拦截器可用于上下文或基于范围/阈值的跟踪。 通过在InvocationContext中使用EJB调用参数值,可以增强此功能。 这些值可用于跟踪范围或其他上下文的化合物名称。 考虑org.myco.regional.RemoteManagement类中的EJB调用,该类具有issueRemoteOperation(String region, Command command)方法。 EJB接受命令,然后对由区域标识的服务器进行远程调用。 在这种情况下,区域服务器分布在广泛的地理区域,每个服务器都有其自己的WAN特性。 这提供了与第1部分中薪资处理示例类似的模式,因为在不考虑将命令分派到哪个区域的情况下,很难表征对该EJB的调用的经过时间。 您可能希望,远离大陆的地区的经过时间比隔壁的地区要慢得多。 但是可以通过InvocationContext参数确定区域,因此您可以简单地将区域代码添加到跟踪化合物名称中,并按区域划分性能数据,如清单4所示:

清单4. EJB 3拦截器实现的上下文跟踪
String[] prefix = null;
if(ctx.getTarget().getClass().getName()
   .equals("org.myco.regional.RemoteManagement") &&
    ctx.getMethod().getName().equals("issueRemoteOperation")) {
   prefix = new String[]{"RemoteManagement",
      ctx.getParameters()[0].toString(),
      "issueRemoteOperation"};
}
// Now add prefix to the tracing compound name

Servlet过滤器拦截器

Java Servlet API提供了一个称为filter的构造,该构造与EJB 3拦截器非常相似,包括无源代码注入和元数据的可用性。 清单5显示了带有简化工具的过滤器的doFilter方法。 指标的复合名称是根据过滤器类名称和请求的统一资源标识符(URI)构建的:

清单5.一个servlet过滤器拦截器方法
public void doFilter(ServletRequest req, ServletResponse resp,
      FilterChain filterChain) throws IOException, ServletException {
   String uri = null;
   try {
      uri = ((HttpServletRequest)req).getRequestURI();
      tracer.startThreadInfoCapture(CPU + BLOCK + WAIT);
      // ===================================
      //   This is the target.
      // ===================================
      filterChain.doFilter(req, resp);
      // ===================================
   } catch (Exception e) {
   } finally {
      tracer.endThreadInfoCapture("Servlets", getClass().getName(), uri);
   }
}

清单6显示了清单5中的过滤器的web.xml部署描述符的相关片段:

清单6. Servlet过滤器部署描述符
<web-app >
     <filter>
      <filter-name>ITraceFilter</filter-name>
      <display-name>ITraceFilter</display-name>
      <filter-class>org.myco.http.ITraceFilter</filter-class>
   </filter>
   <filter-mapping>
   <filter-name>ITraceFilter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>
</web-app>

EJB客户端拦截器和上下文传递

前面的示例着重于服务器端组件,但是也提供了一些用于实现检测的选项,例如客户端的侦听。 Ajax客户端可以注册性能监听器,以测量XMLHttpRequest的运行时间,并且可以在下一个请求的参数列表的末尾搭载请求的URI(对于复合名称)和运行时间。 而且某些Java EE服务器(例如JBoss)允许客户端拦截器在本质上完全执行EJB 3拦截器的工作,并且还可以在下一个请求上搭载测量提交。

监视方程式的客户端经常被忽略。 下次您听到用户抱怨您的应用程序运行缓慢时,请不要立即将其关闭,因为您的服务器端监视库可确保您的服务器端正常。 客户端工具可确保您正在衡量用户实际体验的内容,而这可能并不总是与服务器端指标保持一致。

一些Java EE实现所支持的客户端拦截器在EJB的客户端上实例化并绑定。 这意味着,如果您有一个远程客户端通过远程方法调用(RMI)协议在服务器上调用EJB,则还可以从远程客户端无缝收集性能数据。 在远程调用关系的两侧实现拦截器类还增加了在两者之间传递上下文以导出其他性能数据的功能。

下面的示例演示了一对拦截器,它们共享数据并得出传输时间(传递请求和响应所花费的时间),以及客户端对服务器远程请求所经历的响应时间的看法。 该示例使用JBoss应用程序服务器的客户端和服务器端EJB 3拦截器的专有实现。

拦截器对通过在同一个有效负载中附加上下文数据,在与EJB调用相同的调用中传递上下文数据。 上下文数据由以下内容组成:

  • 客户端请求发出的时间 :EJB客户端拦截器发出请求时的时间戳记
  • 服务器端请求接收时间 :EJB服务器端拦截器接收到请求时的时间戳记
  • 服务器端响应发送时间 :EJB服务器端拦截器将响应分派回客户端时的响应时间戳。

调用自变量被视为堆栈结构,由此上下文数据被推入自变量或从自变量弹出。 上下文数据由客户端拦截器推送到调用中,由服务器拦截器弹出,然后传递到EJB服务器存根。 在返回行程中,相反的情况也会发生。 图3说明了此流程:

图3.客户端和服务器EJB拦截器数据流
客户端和服务器EJB拦截器数据流

为此示例构建拦截器需要为客户端和服务器实现org.jboss.aop.advice.Interceptor接口。 此接口有一种重要的方法:

public abstract java.lang.Object invoke(
   org.jboss.aop.joinpoint.Invocation invocation) throws java.lang.Throwable

此方法引入了调用封装的思想,由此方法的执行被封装到一个代表以下内容的离散对象中:

  • 目标阶层
  • 要调用的方法名称
  • 有效负载,包括作为参数传递给目标方法的参数

然后,可以传递该对象,直到将其传递给调用程序为止,该调用程序将对调用对象进行编组,并针对端点目标对象动态执行该对象。

客户端拦截器将当前请求时间添加到调用上下文,服务器端拦截器添加请求接收的时间戳和响应发送的时间戳。 可选地,服务器可以导出客户端请求,并且客户端计算该请求的总经过时间以及上下传输时间。 每种情况下的计算为:

  • 客户端,向上传输等于ServerSideReceivedTime减去ClientSideRequestTime
  • 客户端,向下传输等于ClientSideReceivedTime减去ServerSideRespondTime
  • 服务器端,向上传输等于ServerSideReceivedTime减去ClientSideRequestTime

清单7显示了客户端拦截器的invoke方法:

清单7.客户端拦截器的invoke方法
/**
 * The interception invocation point.
 * @param invocation The encapsulated invocation.
 * @return The return value of the invocation.
 * @throws Throwable
 * @see org.jboss.aop.advice.Interceptor#invoke(org.jboss.aop.joinpoint.Invocation)
 */
public Object invoke(Invocation invocation) throws Throwable {
   if(invocation instanceof MethodInvocation) {
      getInvocationContext().put(CLIENT_REQUEST_TIME, System.currentTimeMillis());
      Object returnValue = clientInvoke((MethodInvocation)invocation);
      long clientResponseTime = System.currentTimeMillis();
      Map<String, Serializable> context = getInvocationContext();
      long clientRequestTime = (Long)context.get(CLIENT_REQUEST_TIME);
      long serverReceiveTime = (Long)context.get(SERVER_RECEIVED_TIME);
      long serverResponseTime = (Long)context.get(SERVER_RESPOND_TIME);
      long transportUp = serverReceiveTime-clientRequestTime;
      long transportDown = serverResponseTime-clientResponseTime;
      long totalElapsed = clientResponseTime-clientRequestTime;
      String methodName = ((MethodInvocation)invocation).getActualMethod().getName();
      String className = ((MethodInvocation)invocation).getActualMethod()
         .getDeclaringClass().getSimpleName();
      ITracer tracer = TracerFactory.getInstance();
      tracer.trace(transportUp, "EJB Client", className, methodName, 
         "Transport Up", transportUp);
      tracer.trace(transportDown, "EJB Client", className, methodName, 
         "Transport Down", transportDown);
      tracer.trace(totalElapsed, "EJB Client", className, methodName, 
         "Total Elapsed", totalElapsed);
      return returnValue;
   } else {
      return invocation.invokeNext();
   }
}

服务器端拦截器在概念上相似,除了在此示例中为避免额外的复杂性之外,它使用本地线程来检测reentrancy (在这种情况下,同一请求处理线程多次调用同一EJB(因此也称为拦截器)在同一远程呼叫中。 拦截器将忽略除第一个此类请求之外的所有请求的跟踪和上下文处理。 清单8显示了服务器端拦截器的invoke方法:

清单8.服务器端拦截器的invoke方法
/**
 * The interception invocation point.
 * @param invocation The encapsulated invocation.
 * @return The return value of the invocation.
 * @throws Throwable
 * @see org.jboss.aop.advice.Interceptor#invoke(org.jboss.aop.joinpoint.Invocation)
 */
public Object invoke(Invocation invocation) throws Throwable {
   Boolean reentrant = reentrancy.get();
   if((reentrant==null || reentrant==false)
      && invocation instanceof MethodInvocation) {
      try {
         long currentTime = System.currentTimeMillis();
         MethodInvocation mi = (MethodInvocation)invocation;
         reentrancy.set(true);
         Map<String, Serializable> context = getInvocationContext(mi);
         context.put(SERVER_RECEIVED_TIME, currentTime);
         Object returnValue = serverInvoke((MethodInvocation)mi);
         context.put(SERVER_RESPOND_TIME, System.currentTimeMillis());
         return addContextReturnValue(returnValue);
      } finally {
         reentrancy.set(false);
      }
   } else {
      return invocation.invokeNext();
   }
}

JBoss通过一种面向方面的编程(AOP)技术来应用拦截器(请参阅参考资料 ),该技术读取一个名为ejb3-interceptors-aop.xml的指令文件,并根据在那里定义的指令来应用拦截器。 JBoss使用这种AOP技术在运行时将核心Java EE规则应用于EJB 3类。 因此,除了性能监视拦截器之外,此文件还包含有关事务管理,安全性和持久性等方面的指令。 客户端指令非常简单。 它们被简单地定义为包含一系列拦截器类名称的stack name XML元素。 在那里定义的每个类名称也都可以用作PER_VMPER_INSTANCE拦截器,指示每个EJB实例是应该共享一个拦截器实例还是具有自己唯一的,未共享的实例。 出于性能监视拦截器的目的,应根据拦截器代码是否是线程安全的来确定此配置。 如果代码可以安全地并发处理多个线程,则使用PER_VM策略会更有效,而效率较低但线程安全的策略会使用PER_INSTANCE

服务器端拦截器的配置稍微复杂一些。 拦截器是根据XML中定义的一组语法模式和过滤器应用的。 如果所讨论的特定EJB方法与定义的模式匹配,那么将应用为该模式定义的拦截器。 拦截器可以通过进一步完善定义来定位已部署EJB的特定子集。 对于客户端拦截器,可以通过创建特定于目标Bean的新stack name来实现自定义堆栈。 在服务器端,可以在新domain定义自定义堆栈。 可以在EJB的批注中指定单个EJB的关联客户端stack name和服务器堆栈domain 。 或者,在您无法或不想修改源代码的情况下,可以在EJB的部署描述符中指定或覆盖等效项。 清单9显示了用于此示例的缩写的ejb3-interceptors-aop.xml文件:

清单9. EJB 3 AOP的简化配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE aop PUBLIC
   "-//JBoss//DTD JBOSS AOP 1.0//EN"
   "http://labs.jboss.com/portal/jbossaop/dtd/jboss-aop_1_0.dtd">

<aop>
   .
   .
   <interceptor
      class="org.runtimemonitoring.ejb.interceptors.ClientContextualInterceptor"
      scope="PER_VM"/>
   .
   .
   <stack name="StatelessSessionClientInterceptors">
      <interceptor-ref
         name="org.runtimemonitoring.ejb.interceptors.ClientContextualInterceptor"/>
      .
      .
   </stack>
   .
   .
  <interceptor
     class="org.runtimemonitoring.ejb.interceptors.ServerContextualInterceptor"
     scope="PER_VM"/>
    .
    .
   <domain name="Stateless Bean">
      <bind pointcut="execution(public * *->*(..))">
         <interceptor-ref name="org.aa4h.ejb.interceptors.ServerContextualInterceptor"/>
         .
         .
        </bind>
   </domain>
</aop>

这种类型的性能数据收集用一块石头杀死两只鸟。 首先,它从客户端的角度告诉您EJB的持续性能是什么。 其次,如果性能不佳,则原因是否是客户端和服务器之间的网络连接速度较慢,则传输时间可以很好地说明问题。 图4显示了总耗用时间和从客户端测量的传输上/下性能指标,其中客户端和服务器之间存在人为的慢速网络链接,以突出显示传输时间:

图4.上下文客户端拦截器性能指标
上下文客户端拦截器性能指标

当您使用客户端拦截器时,客户端拦截器类本身必须位于客户端应用程序的类路径中。 或者,您必须启用从服务器的远程类加载,以便可以在启动时将客户端拦截器及其依赖项下载到客户端。 如果客户端的系统时钟与服务器的系统时钟几乎不完全同步,则您将体验到与它们之间的差异成正比的奇特结果。

Spring拦截器

尽管Java EE具有正交和无缝拦截的机会,但是许多流行的非Java EE容器也支持隐式和显式拦截。 我用容器这个词来暗示某种使用或鼓励松散耦合的框架。 紧密耦合的缺失是专门提供实现拦截功能的能力。 这些类型的框架通常称为依赖项注入或控制反转(IoC)体系结构。 它们使您可以从外部定义各个组件如何“粘合”在一起,而不是对各个组件进行硬编码以直接彼此对话。 我将结束我的拦截与讨论(参见使用追踪拦截器在Spring框架的性能数据收集的审查相关信息 ),一个流行的IoC框架。

Spring Framework使您可以使用普通旧Java对象(PO​​JO)来构建应用程序。 您的POJO仅包含您的业务逻辑,并且该框架添加了构建企业应用程序所需的内容。 如果您在最初编写Java应用程序时不考虑使用工具,则Spring的分层体系结构可能会很有用。 尽管将应用程序体系结构支持到Spring中不一定是一件容易的事,但是Spring的POJO管理特性,除了一系列Java EE和AOP集成之外,还提供了充足的机会将通常硬连线的Java类委托给Spring的容器管理。 在此过程中,您可以通过侦听添加性能检测,而无需修改目标类的源代码。

经常将Spring描述为IoC容器,因为它逆转了Java应用程序的传统拓扑。 在传统拓扑中,一个中央程序或控制线程会按程序加载其所有必需的组件和依赖项。 使用IoC,容器可以加载多个组件,并根据外部配置管理组件之间的依赖关系。 此依赖项管理称为依赖项注入,因为依赖项(例如JDBC DataSource )是通过容器注入组件的; 组件不需要搜索自己的依赖项。 出于检测目的,可以轻松修改容器的配置,以将拦截器插入这些组件之间的“结缔组织”中。 图5说明了这个概念:

图5. Spring和拦截的概述
弹簧和拦截概述

现在,我将提供一个使用Spring进行拦截的简单示例。 它涉及一个EmpDAOImpl类,它是一个基本的数据访问对象(DAO)模式类,实现了DAO接口,该接口定义了一种称为public Map<Integer, ? extends DAOManaged> get(Integer...pks) ,?的方法public Map<Integer, ? extends DAOManaged> get(Integer...pks) public Map<Integer, ? extends DAOManaged> get(Integer...pks) 。 该接口要求我传入需要完整对象的主键数组,并且DAO实现将返回对象的Map 。 此代码中的缺陷列表太长,无法在此处详细说明。 可以说,它没有提供任何工具,也没有使用任何类型的对象关系映射(ORM)框架。 图6概述了类结构。 请参阅下载以获取此处引用的任何工件的完整源代码和文本文件。

图6. EmpDAO
EmpDAO类

根据spring.xml文件的配置,将EmpDAOImpl部署到Spring容器中,清单10显示了其缩写部分:

清单10. Spring示例的基本容器配置
<beans>
   <bean id="tracingInterceptor"
      class="org.runtimemonitoring.spring.interceptors.SpringTracingInterceptor">
      <property name="interceptorName" value="Intercepted DAO"/>
   </bean>
   
   <bean id="tracingOptimizedInterceptor"
      class="org.runtimemonitoring.spring.interceptors.SpringTracingInterceptor">
      <property name="interceptorName" value="Optimized Intercepted DAO"/>
   </bean>
   
   <bean id="DataSource"
      class="org.apache.commons.dbcp.BasicDataSource"
      destroy-method="close"
      p:url="jdbc:postgresql://DBSERVER:5432/runtime"
      p:driverClassName="org.postgresql.Driver"
      p:username="scott"
      p:password="tiger"
      p:initial-size="2"
      p:max-active="5"
      p:pool-prepared-statements="true"
      p:validation-query="SELECT CURRENT_TIMESTAMP"
      p:test-on-borrow="false"
      p:test-while-idle="false"/>
   
   <bean id="EmployeeDAO" class="org.runtimemonitoring.spring.EmpDAOImpl"
      p:dataSource-ref="DataSource"/>
      
   <bean id="empDao" class="org.springframework.aop.framework.ProxyFactoryBean">
      <property name="proxyInterfaces" value="org.runtimemonitoring.spring.DAO"/>
      <property name="target" ref="EmployeeDAO"/>
      <property name="interceptorNames">
         <list>
            <idref local="tracingInterceptor"/>
         </list>
      </property>
      
   </bean>
   <bean id="empDaoOptimized"
      class="org.springframework.aop.framework.ProxyFactoryBean">
      <property name="target" ref="EmployeeDAO"/>
      <property name="optimize">
         <value>true</value>
      </property>
      <property name="proxyTargetClass">
         <value>true</value>
      </property>
      <property name="interceptorNames">
         <list>
            <idref local="tracingOptimizedInterceptor"/>
         </list>
      </property>
   </bean>
   
</beans>

其他一些对象也将被部署。 通过引用它们的Spring bean id来描述这些组件,您可以在清单10中的每个bean元素中看到它们:

  • tracingInterceptortracingOptimizedInterceptor :两个类型为SpringTracingInterceptor拦截器。 此类包含ITracer调用,以将收集的数据跟踪到APM系统。
  • DataSource :JDBC DataSource用于将JDBC连接池化到称为runtime的示例数据库,该数据库将注入到EmpDAOImpl
  • EmployeeDAO :作为示例的一部分,我将调用EmpDAOImpl
  • empDaoempDaoOptimized :spring.xml文件中定义的最后两个bean是Spring ProxyFactoryBean 。 这些本质上是EmpDAOImpl代理,每个代理都引用一个拦截器。 尽管可以直接访问EmpDAOImpl ,但是使用代理可以调用​​拦截器并生成性能指标。 清单10中的两个代理和两个拦截器说明了一些区别和配置注意事项。 请参阅优化拦截器侧栏。

Spring容器是从SpringRunner类引导的。 它还启动了一个测试循环,该循环针对四个目标调用DAO.get

  • EmployeeDAO Spring bean,代表未按Spring方式管理的DAO。
  • empDao Spring bean,代表具有标准拦截器的按Spring empDao DAO。
  • empDaoOptimized Spring bean,它代表具有优化拦截器的按Spring管理的DAO。
  • 非Spring管理的EmpDAOImpl与Spring管理的bean形成对比。

Spring通过称为org.aopalliance.intercept.MethodInterceptor的接口实现了这些类型的拦截器。 只有一种方法可以实现: public Object invoke(MethodInvocation invocation) throws ThrowableMethodInvocation对象提供了两个关键项:具有某些上下文的跟踪器(即,正在被拦截的方法名称)和proceed方法,该方法将调用向前引导到预期的目标。

清单11显示了SpringTracingInterceptor类的invoke方法。 在这种情况下, interceptorName属性不是必需的,但我添加了该属性以为该示例提供其他上下文。 对于完整的多功能拦截器实现,跟踪器通常会将类名添加到跟踪上下文中,以便将所有被拦截类中的所有方法都跟踪到单独的APM名称空间中。

清单11. SpringTracingInterceptor类的invoke方法
public Object invoke(MethodInvocation invocation) throws Throwable {
   String methodName = invocation.getMethod().getName();
   tracer.startThreadInfoCapture(WAIT+BLOCK);
   Object returnValue = invocation.proceed();
   tracer.endThreadInfoCapture("Spring", "DAO", 
      interceptorName, methodName);
   tracer.traceIncident("Spring", "DAO", interceptorName, 
      methodName, "Responses Per Interval");
   return returnValue;
}

SpringRunner类是此示例的主要入口点。 它初始化Spring bean工厂,然后开始一个长循环,将负载加到每个bean上。 清单12中显示了该循环的代码。请注意,由于Spring拦截器未对daoNoInterceptordaoDirect进行检测,因此我在SpringRunner循环中手动添加了检测。

清单12. SpringRunner循环
Map<Integer, ? extends DAOManaged> emps = null;
DAO daoIntercepted = (DAO) bf.getBean("empDao");
DAO daoOptimizedIntercepted = (DAO) bf.getBean("empDaoOptimized");
DAO daoNoInterceptor = (DAO) bf.getBean("EmployeeDAO");
DataSource dataSource = (DataSource) bf.getBean("DataSource");
DAO daoDirect = new EmpDAOImpl();
// Not Spring Managed, so dependency is set manually
daoDirect.setDataSource(dataSource);
for(int i = 0; i < 100000; i++) {
    emps = daoIntercepted.get(empIds);
    log("(Interceptor) Acquired ", emps.size(), " Employees");
    emps = daoOptimizedIntercepted.get(empIds);
    log("(Optimized Interceptor) Acquired ", emps.size(), "
       Employees");
    tracer.startThreadInfoCapture(WAIT+BLOCK);
    emps = daoNoInterceptor.get(empIds);
    log("(Non Intercepted) Acquired ", emps.size(), " Employees");
    tracer.endThreadInfoCapture("Spring", "DAO",
       "No Interceptor DAO", "get");
    tracer.traceIncident("Spring", "DAO", 
       "No Interceptor DAO", "get", "Responses Per Interval");
    tracer.startThreadInfoCapture(WAIT+BLOCK);
    emps = daoDirect.get(empIds);
    log("(Direct) Acquired ", emps.size(), " Employees");
    tracer.endThreadInfoCapture("Spring", "DAO",
       "Direct", "get");
    tracer.traceIncident("Spring", "DAO", "Direct",
       "get", "Responses Per Interval");
}

通过APM系统报告的结果显示了几个比较项。 表1列出了每个Spring bean进行测试运行的平均调用时间:

表1. Spring拦截器测试运行结果
Spring Bean 平均经过时间(毫秒) 最小经过时间(毫秒) 最长经过时间(毫秒) 计数
直接 145 124 906 5110
优化的拦截器 145 125 906 5110
没有拦截器 145 124 891 5110
拦截器 155 125 952 5110

图7显示了在APM中为此测试用例创建的度量树:

图7. Spring拦截器测试运行APM指标树
Spring Interceptor Test Run APM指标树

图8在图表中显示了这些数据:

图8. Spring拦截器测试运行结果
Spring拦截器测试运行结果

显然,这些结果聚集在一起非常接近,但是确实出现了一些模式。 优化的拦截器确实略微超出了非优化的拦截器。 但是,此测试运行中仅运行一个线程,因此对于比较分析不是特别有用。 在下一节中,我将扩展此测试用例并实现多个线程。

通过类包装的JDBC检测

我发现典型的企业Java应用程序中大多数长期性能问题的根本原因在于数据库接口。 通过JDBC进行数据库调用可能是从JVM到外部服务的最常见调出,以获取JVM中本地不可用的数据集或资源,因此这并不完全令人惊讶。 从逻辑上讲,在这种情况下,可能的犯罪者是数据库客户端,数据库本身或两者之间的阴谋。 但是,许多面向数据库客户端的应用程序受到许多性能反模式的困扰,其中包括:

  • 逻辑上正确,但性能不佳SQL。
  • 不够具体的请求检索的数据远远多于实现所需功能所需的数据。
  • 不断频繁地检索相同的冗余数据。
  • 请求的基数差,导致大量数据库请求检索一种逻辑结构的数据,而有效地检索同一数据集的请求数量较少。 (我自己的数据库访问公理是更喜欢一个查询,该查询将返回大量行和列,而不是多个查询来检索较短和较窄的数据集。)此模式通常与嵌套的类结构和试图应用的开发人员相关联传统封装概念规定每个对象管理自己的数据检索,而不是委派给一个通用的统一数据请求者。

我当然不会在每个实例中都反对应用程序代码和设计,并且在本系列的第3部分中,我介绍了监视数据库以进行性能统计的方法。 但总的来说,最有效的解决方案往往在客户手中。 因此,在Java应用程序中监视数据库接口性能的最佳目标是JDBC。

我将演示如何使用类包装的概念来检测JDBC客户端。 类包装背后的想法是,可以将目标类包装在检测代码层中,该检测代码保留与包装的类相同的外部行为。 The challenge in these scenarios is how to introduce the wrapped classes seamlessly without interfering with the dependent structures.

In this example, I take advantage of the fact that JDBC is primarily a completely interface-defined API: the specification contains few concrete classes, and JDBC's architecture precludes the necessity for tightly coupling directly to the database-vendor-specific supplied classes, ever. JDBC concrete implementations are loaded implicitly, and source code rarely references these concrete classes directly. As a result, you can define a brand new JDBC driver that has no implementation other than delegating all of the calls made against it to the "real" driver underneath while collecting performance data in the process.

I've built an implementation called WrappingJDBCDriver , which is sufficiently functional to demonstrate performance data collection and to support the EmployeeDAO test case from the previous Spring example . Figure 9 shows an overview of how the WrappingJDBCDriver works:

Figure 9. Overview of the WrappingJDBCDriver
Overview of the WrappingJDBCDriver

The standard processing for loading a JDBC driver requires two items: the driver's class name and the JDBC URL of the database that's targeted for connection. The driver loader loads the driver class (possibly by calling Class.forName(jdbcDriverClassName) ). Most JDBC drivers register themselves with the JDBC java.sql.DriverManager when they are class loaded. The driver loader then passes the JDBC URL to an instance of the JDBC driver to test if the driver accepts that URL. Assuming the URL is accepted, the loader can call connect on the driver and get back a java.sql.Connection .

The wrapped driver has the class name org.runtimemonitoring.jdbc.WrappingJDBCDriver . When it is instantiated, it loads a configuration file called wrapped-driver.xml from the classpath. This file contains instrumentation configuration items indexed by a figurative name associated to the target driver:

  • <Figurative Name>.driver.prefix : The JDBC driver's real JDBC URL prefix — for example, jdbc.postgresql:
  • <Figurative Name>.driver.class : The JDBC driver's class name — for example, org.postgresql.Driver .
  • <Figurative Name>.driver.class.path : A series of comma-separated classpath entries to the JDBC driver's location. This item is optional; if it's not included, the WrappingJDBCDriver uses its own class loader to locate the driver class.
  • <Figurative Name>.tracer.pattern.<Zero Based Index> : A series of regular-expression patterns used to extract a tracing category for a specific target database. The indexing must start at 0, and the sequence defines the tracing category's hierarchy.

The basic premise of the WrappingJDBCDriver is to configure a JDBC client application to use a "munged" JDBC URL that no other JDBC driver (including the instrumentation-targeted one) will recognize and therefore not accept, with the exception of the WrappingJDBCDriver . The WrappingJDBCDriver will recognize the munged URL, internally load the target driver, and associate it to the munged URL. The munged URL is then "unmunged" and delegated to the internal driver to acquire a real connection to the target database. The real connection is then wrapped in a WrappingJDBCConnection and returned to the requesting application. The munge algorithm can be very basic as long as it renders the JDBC URL sufficiently unrecognizable to the target "real" JDBC driver. Otherwise, the WrappingJDBCDriver might be bypassed by the real driver. In this example, I'm munging the real JDBC URL of jdbc:postgresql://DBSERVER:5432/runtime to jdbc:!itracer!wrapped:postgresql://DBSERVER:5432/runtime .

The "real" driver's class name and optional classpath configuration items serve to allow the WrappingJDBCDriver to locate and classload the driver class so that it can be wrapped and delegated to. The tracer pattern configuration items are a set of regular expressions that instruct the WrappingJDBCDriver how to determine the tracing name space for this target database. These expressions are applied to the "real" JDBC URL and are required so that the tracer can provide performance metrics to the APM system that are demarcated by the target database. If the WrappingJDBCDriver is used for multiple (possibly different) databases, it is necessary to establish this demarcation so that the collected metrics can be grouped by the target database. For example, a JDBC URL of jdbc:postgresql://DBSERVER:5432/runtime might generate a namespace of postgresql, runtime .

Listing 13 shows a sample wrapped-driver.xml file using the figurative name of postgres mapped to the PostgreSQL 8.3 JDBC Driver:

Listing 13. Sample wrapped-driver.xml file
<properties>
   <entry key="postgres.driver.prefix">jdbc:postgresql:</entry>
   <entry key="postgres.driver.class">org.postgresql.Driver</entry>
   <entry key="postgres.driver.class.path">
      C:\Postgres\psqlJDBC\postgresql-8.3-603.jdbc3.jar
   </entry>
   <entry key="postgres.tracer.pattern.0">:([a-zA-Z0-9]+):</entry>
   <entry key="postgres.tracer.pattern.1">.*\/\/.*\/([\S]+)</entry>
</properties>

This partial implementation is inspired by an open source product called P6Spy (see Related topics ).

To demonstrate the use of WrappingJDBCDriver , I created a new enhanced version of the EmpDAO Spring test case. The new Spring configuration file is spring-jdbc-tracing.xml, and the new entry-point class is SpringRunnerJDBC . This test case includes several additional comparative testing points, so some of the naming conventions are updated for clarity. I've also enhanced the test case to make it multithreaded, which creates different and interesting behavior in the collected metrics. And, to add some variability, the arguments to the DAO can be randomized.

I've added the following tracing enhancements to this new test case:

  • Two data sources are defined. One uses the direct JDBC driver, and the other uses the instrumented JDBC driver.
  • The data sources can optionally be accessed through Spring proxies that are instrumented to monitor the elapsed time of acquiring connections.
  • The DAO interceptors have been enhanced to monitor the number of concurrent threads passing through the interceptors.
  • An additional background thread is spawned to poll for usage statistics on the data sources.
  • All WrappingJDBC classes invoke most of their tracer calls through the base class, which is WrappingJDBCCore . In addition to issuing a straight passthrough to its ITracer , the base class also issues rollup-level tracing at the database instance level. This demonstrates a common feature in APM systems whereby low-level and specific metrics can be traced multiple times to higher-level name spaces to provide summary-level metrics. For example, all JDBC calls in any object type roll up to the database level to summarize the average elapsed time and volume of requests for all calls to the database.

Listing 14 displays instances of new bean definitions in the spring-jdbc-tracing.xml file. Note that the JDBC URL defined in the InstrumentedJDBC.DataSource bean uses the munged convention.

Listing 14. Excerpts from spring-jdbc-tracing.xml
<!-- A DataSource Interceptor -->
<bean id="InstrumentedJDBCDataSourceInterceptor"
   class="org.runtimemonitoring.spring.interceptors.SpringDataSourceInterceptor">
   <property name="interceptorName" value="InstrumentedJDBC.DataSource"/>
</bean>

<!-- A DataSource for Instrumented JDBC -->
<bean id="InstrumentedJDBC.DataSource"
   class="org.apache.commons.dbcp.BasicDataSource"
   destroy-method="close"
   p:url="jdbc:!itracer!wrapped:postgresql://DBSERVER:5432/runtime"
   p:driverClassName="org.runtimemonitoring.jdbc.WrappingJDBCDriver"
   p:username="scott"
   p:password="tiger"
   p:initial-size="2"
   p:max-active="10"
   p:pool-prepared-statements="true"
   p:validation-query="SELECT CURRENT_TIMESTAMP"
   p:test-on-borrow="false"
   p:test-while-idle="false"/>


<!-- The Spring proxy for the DataSource -->
<bean id="InstrumentedJDBC.DataSource.Proxy"
   class="org.springframework.aop.framework.ProxyFactoryBean">
   <property name="target" ref="InstrumentedJDBC.DataSource"/>
   <property name="optimize"><value>true</value></property>
   <property name="proxyTargetClass"><value>true</value></property>
   <property name="interceptorNames">
      <list>
         <idref local="InstrumentedJDBCDataSourceInterceptor"/>
      </list>
   </property>
</bean>

<!--
The Spring proxy for the DataSource which is injected into
the DAO bean instead of the DataSource bean itself.
-->
<bean id="InstrumentedJDBC.DataSource.Proxy"
   class="org.springframework.aop.framework.ProxyFactoryBean">
   <property name="target" ref="InstrumentedJDBC.DataSource"/>
   <property name="optimize"><value>true</value></property>
   <property name="proxyTargetClass"><value>true</value></property>
   <property name="interceptorNames">
      <list>
         <idref local="InstrumentedJDBCDataSourceInterceptor"/>
      </list>
   </property>
</bean>

Figure 10 displays the APM metric tree for this test case:

Figure 10. Instrumented JDBC metric tree
Instrumented JDBC metric tree

With a higher volume of data from this example, it is possible to demonstrate some concrete examples of the causes of thread BLOCK s and WAIT s. The SpringRunnerJDBC adds a ThreadInfoCapture(WAIT+BLOCK) trace around a simple statement at the end of each loop, which is Thread.currentThread().join(100) . According to the APM system, this shows up as an average thread wait of 103 ms. So when a thread is put into a wait state waiting for something to happen, it incurs a wait of some period of time. In contrast, when a thread tries to acquire a connection from a DataSource , it is accessing a tightly synchronized resource, and with the increased number of threads contending for connections, the DAO.get method clearly shows an increased number of thread blocks.

This test case displays a few more DAO.get bean instances that are due to the addition of instrumented and noninstrumented data sources. Table 2 shows the updated list of comparative scenarios and the numerical results:

Table 2. Instrumented JDBC test run results
Test case Average elapsed time (ms) Minimum elapsed time (ms) Maximum elapsed time (ms) 计数
Direct access, raw JDBC 5 0 78 12187
Direct access, instrumented JDBC 27 0 281 8509
No interceptor Spring bean, raw JDBC 15 0 125 12187
No interceptor Spring bean, instrumented JDBC 35 0 157 8511
Instrumented Spring bean, raw JDBC 16 0 125 12189
Instrumented Spring bean, instrumented JDBC 36 0 250 8511
Optimized Instrumented Spring bean, raw JDBC 15 0 203 12188
Optimized instrumented Spring bean, instrumented JDBC 35 0 187 8511

The results show some interesting patterns, but one aspect clearly stands out: The instrumented JDBC is visibly slower than the raw JDBC. Consider this a cautionary tale about ensuring that instrumentation is as streamlined and tuned as possible. In this basic example of JDBC instrumentation, the cause of the performance differential is a combination of the inserted tracing, the longer code path, and the amount of extra object creation required to execute a sequence of queries. If I ever want to use this approach in a performance-intensive environment, I'll need to do some more work on this code base! Using the instrumented DAO.get beans causes another visible but much less dramatic impact. Again, this can be attributed to some additional overhead in reflective invocation, a longer code path, and the tracing activity. It looks like the tracer adapter could also use some tuning, but the hard reality is that all instrumentation imposes some level of overhead. Figure 11 displays the elapsed time results for this test:

Figure 11. Instrumented JDBC results
Instrumented JDBC results

The last point of interest for this section is the thread block times that are rolled up to the database level. These database-level statistics represent the aggregated values of all collected metrics for all calls to that database per interval. The elapsed times are averaged, but the counts (responses per interval, blocks, and waits) are totaled per interval. In this test case, the aggregated per-interval average block time was zero, but in Figure 12, you can observe a feature of some APM visualization tools. Although the average was zero, each interval had a maximum (and a minimum) reading. In this graph, my APM is displaying a flat zero line indicating the average but also the maximum value:

Figure 12. JDBC aggregated block times
JDBC aggregated block times

In this article's last section, I'll introduce a final technique for instrumenting Java classes without modifying the source: bytecode instrumentation .

Bytecode instrumentation

The non-source-based instrumentation techniques I've shown you to this point involve adding objects and often lengthening the code-execution path beyond the execution of the tracing code itself. Bytecode instrumentation (BCI) is a technique in which bytecode in injected directly into a Java class to achieve some purpose that the class did not originally support. This process has a variety of uses for developers who want to modify a class without touching the source, or want to change the class definition dynamically at run time. I'll show how you can use BCI to inject performance-monitoring instrumentation into your classes.

Different BCI frameworks accomplish this goal in different ways. A simple technique for instrumenting at a method level is to rename the target method and insert a new method with the original signature that contains the tracing directives and calls the original (renamed) method. An open source BCI tool called JRat illustrates a technique that is specific to collecting elapsed times for method executions and consequently is less verbose than a general-purpose BCI AOP tool (see Related topics ). I've condensed an example from the JRat project into Listing 15:

Listing 15. Example of an instrumented method using BCI
//
//  The Original Method
//
public class MyClass {
 public Object doSomething() {
        // do something
    }
}
//
//  The New and Old Method
//
public class MyClass {
    private static final MethodHandler handler = HandlerFactory.getHandler(...);
    // The instrumented method
    public Object doSomething() {
        handler.onMethodStart(this);
        long startTime = Clock.getTime();
        try {
           Object result = real_renamed_doSomething(); // call your method
           handler.onMethodFinish(this, Clock.getTime() - startTime, null);
        } catch(Throwable e) {
           handler.onMethodFinish(this, Clock.getTime() - startTime, e);
           throw e;
        }
    }
    // The renamed original method
    public Object real_renamed_doSomething() {
        // do something
    }
}

The two general strategies for implementing BCI are:

  • Static : Java classes or class libraries are instrumented, and the instrumented classes are saved back to a copy of the original class or library. These are then deployed to an application where the instrumented classes are treated the same as any other class.
  • Dynamic : Java classes are instrumented at run time during the class-loading process. The instrumented classes only ever exist in memory; when the JVM ends, they disappear.

Among the advantages of dynamic BCI is the flexibility it provides. Dynamic BCI is typically executed in accordance with a set of directives that have been configured (usually in a file). Modifying the instrumentation simply requires an update of that file and a JVM recycle, although hot swap is widely supported. However, first I'll examine a static instrumentation procedure.

Static BCI

In this example, I'll use static BCI to instrument the EmpDAOImpl class. I'll use JBoss AOP, an open source BCI framework (see Related topics ).

The first step is to define the interceptor I want to use to collect the method-invocation performance data, because this class will be statically woven into the bytecode of the EmpDAOImpl class. In this case, the JBoss interface is identical to the interceptors I defined for Spring, except that the imported class names are different. The interceptor for this example is org.runtimemonitoring.aop.ITracerInterceptor . The next step is to define the jboss-aop.xml file, using the same syntax used to define the one for EJB 3 interceptors. This file is shown in Listing 16:

Listing 16. Static BCI jboss-aop.xml file
<aop>
   <interceptor class="org.runtimemonitoring.aop.ITracerInterceptor" scope="PER_VM"/>
   <bind
    pointcut="execution(public * $instanceof{org.runtimemonitoring.spring.DAO}->get(..))">
      <interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
   </bind>
</aop>

The next step is to execute the static instrumentation process using a JBoss-supplied tool called the Aop Compiler (aopc). This is easiest to accomplish in an Ant script. Listing 17 illustrates the Ant task and a snippet of output from the compiler indicating that my defined pointcut was matched to my target class:

Listing 17. The aopc Ant task and output
<target name="staticBCI" depends="compileSource">
   <taskdef name="aopc" classname="org.jboss.aop.ant.AopC"
      classpathref="aop.lib.classpath"/>
   <path id="instrument.target.path">
      <path location="${classes.dir}"/>
      </path>
   <aopc compilerclasspathref="aop.class.path" verbose="true">
      <classpath path="instrument.target.path"/>
      <src path="${classes.dir}"/>
      <aoppath path="${conf.dir}/jboss-aop/jboss-aop.xml"/>
   </aopc>
</target>

Output:

[aopc] [trying to transform] org.runtimemonitoring.spring.EmpDAOImpl
[aopc] [debug] javassist.CtMethod@955a8255[public transient get
   ([Ljava/lang/Integer;)Ljava/util/Map;] matches pointcut:
   execution(public * $instanceof{org.runtimemonitoring.spring.DAO}->get(..))

The pointcut defined in jboss-aop.xml files like the one defined in Listing 16 implement an AOP-specific syntax, the purpose of which is to provide an expressive and wildcarded language for defining pointcut targets either specifically or generally. Virtually any identifying attribute of a method can be mapped from class and package names down to annotations and return types. In Listing 17 , I'm specifying that any public method called get in any instance of org.runtimemonitoring.spring.DAO should be targeted. Accordingly, because org.runtimemonitoring.spring.EmpDAOImpl is the only concrete class that matches this criterion, it's the only class instrumented.

At this point, the instrumentation is complete. To run the SpringRunner test case with this instrumentation enabled, the location of the jboss-aop.xml file must be defined in a system property at JVM startup with a JVM argument such as -Djboss.aop.path=[directory]/jboss-aop.xml . The premise is that you can derive some flexibility from the fact that a jboss-aop.xml is used at static instrumentation build time, and then again at run time, because you could initially instrument every single class but activate only specific ones at run time. The APM system metric tree generated for the SpringRunner test case now contains metrics for EmpDAOImpl . This tree section is displayed in Figure 13:

Figure 13. Static BCI metric tree
Static BCI metric tree

Although some flexibility is possible in static instrumentation, the fact that classes cannot be activated for instrumentation unless they are statically processed — which is somewhat laborious — is ultimately limiting. Moreover, once the classes have been statically instrumented, they can be activated only for the interceptor defined at instrumentation time. In the next example, I'll repeat this test case using dynamic BCI.

Dynamic BCI

A number of options for achieving dynamic BCI are available, but there's a clear advantage to sticking with the Java 1.5 javaagent interface. I'll briefly describe this interface here at a high level; for more in-depth coverage of the subject, see Andrew Wilcox's article "Build your own profiling tool" (see Related topics ).

The javaagent enables run-time dynamic BCI through two constructs. First, when the JVM is started up with the -javaagent: a JAR file , where the named JAR file contains a javaagent implementation, it invokes a public static void premain(String args, Instrumentation inst) method in the class defined in a special manifest entry. As the name premain suggests, this method is invoked before the main Java application entry point, which allows the called class to have the absolute first access to start modifying loaded classes. It does this by registering instances of ClassTransformer s (the second construct). The ClassTransformer interface is responsible for effectively intercepting calls from classloaders and rewriting the bytecode of a loaded class on the fly. The ClassTransformer 's single method — transform — is passed the class to be redefined and a byte array containing the class's bytecode. The transform method can then implement all manner of modifications, and it returns a new byte array containing the bytecode of the modified (or instrumented) class. This model allows for fast and efficient class transformation, and unlike some earlier methods, does not require a native component to work.

Implementing dynamic BCI on the SpringRunner test case takes two steps. First, the org.runtimemonitoring.spring.EmpDAOImpl class should be recompiled cleanly to remove the static BCI from the last test case. Second, the JVM startup options need to retain the -Djboss.aop.path=[directory]/jboss-aop.xml option, and the javaagent option needs to be added like this:

-javaagent:[directory name]/jboss-aop-jdk50.jar

Listing 18 shows a slightly modified jboss-aop.xml file to illustrate the benefits of dynamic BCI:

Listing 18. Abbreviated dynamic BCI jboss-aop.xml file
<interceptor class="org.runtimemonitoring.aop.ITracerInterceptor"
   scope="PER_VM"/>
<interceptor class="org.runtimemonitoring.aop.PreparedStatementInterceptor"
   scope="PER_VM"/>
<bind
 pointcut="execution(public * $instanceof{org.runtimemonitoring.spring.DAO}->get(..))">
   <interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
</bind>
<bind
 pointcut="execution(public * $instanceof{java.sql.Connection}->prepareStatement(..))">
   <interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
</bind>
 pointcut="execution(public * $instanceof{java.sql.PreparedStatement}->executeQuery(..))">
   <interceptor-ref name="org.runtimemonitoring.aop.ITracerInterceptor"/>
</bind>

One benefit is that any class can be instrumented, including third-party libraries, so Listing 18 shows instrumentation on all instances of java.sql.Connection . More powerful yet is the ability to apply any arbitrary (but applicable) interceptor to any defined pointcut. For example, org.runtimemonitoring.aop.PreparedStatementInterceptor is a trivial but slightly different interceptor from ITracerInterceptor . Entire libraries of interceptors (more commonly and broadly meaning aspects in AOP parlance) can be developed and are available from open source providers. These aspect libraries can provide a wide array of perspectives that are useful depending on the type of instrumentation you wish to apply, the API you want to instrument, or a customized overlap of several of these items.

The metric tree for these additional metrics is shown in Figure 14. Note that through the use of the Jakarta Commons DataSource providers in Spring, several classes implement java.sql interfaces.

Figure 14. Dynamic BCI metric tree
Dynamic BCI Metric Tree

The most dramatic benefit of the BCI approach overall is clear when you compare the performance difference between my WrappingJDBC instrumentation technique and the BCI-instrumented drivers. This is displayed in Figure 15, which shows the comparative elapsed times of PreparedStatement.executeQuery :

Figure 15. BCI vs. wrapping performance
BCI vs. wrapping performance

第2部分的结论

In this article, I've presented a number of ways that you can instrument your Java applications in order to trace useful performance monitoring data to your APM system. The techniques I've outlined do not require modification of the original source code. Each individual case requires some evaluation to determine which approach is most suitable, but it's clear that BCI has become mainstream. Home-grown, open source, and commercial APM systems that implement it for Java performance management can be a critical part of a well-performing and highly available system.

This series' third and final installment addresses ways to monitor resources outside the JVM, including hosts and their operating systems and remote services such as databases and messaging systems. It concludes with additional application performance management issues such as data management, data visualization, reporting, and alerting.

Go to Part 3 now.


翻译自: https://www.ibm.com/developerworks/java/library/j-rtm2/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值