永利最大(官方)网站

巧用Java Instrumentation检查和修改加载的类字节代码

原创|其它|编辑:郝浩|2009-05-18 11:14:33.000|阅读 1471 次

概述:在分析程序出错的原因时,了解它当时的状态将是非常有用的。在许多情况下,我们可以通过堆栈追踪实现此目的,但这些信息经常都是不可用的,或者您需要的可能是程序在出错时处理数据的相关信息。

#慧都22周年庆大促·界面/图表报表/文档/IDE/IOT/测试等千款热门软控件火热促销中>>

  在分析程序出错的原因时,了解它当时的状态将是非常有用的。在许多情况下,我们可以通过堆栈追踪实现此目的,但这些信息经常都是不可用的,或者您需要的可能是程序在出错时处理数据的相关信息。

  传统做法是使用 log4j 和 Java Logging API 等记录框架,然后再通过人工来编写和维护所需的记录语句。这种操作非常单调乏味且容易出错,一般适合自动实现。Java 5 添加的 Java Instrumentation 机制允许您通过提供 "Java 代理" 来检查和修改加载的类字节代码。

  本文将展示如何实现这种 Java 代理,它借助标准 Java Logging API 透明地对类中所有方法添加入口和出口记录。以 Hello World 为例:

 public class HelloWorld {

            public static void main(String args[]) {

       ;                System.out.println("Hello World");

      ;     }

}

      下面是添加了入口和出口记录语句的同一个用例:

import java.util.Arrays;

import java.util.logging.Level;

import java.util.logging.Logger;

public class LoggingHelloWorld {

        final static Logger _log = Logger.getLogger(LoggingHelloWorld.class.getName());

        public static void main(String args[]) {

        if (_log.isLoggable(Level.INFO)) {

        _log.info("> main(args=" + Arrays.asList(args) + ")");

        }

        System.out.println("Hello World");

        if (_log.isLoggable(Level.INFO)) {

        _log.info("< main()");

          }

     }

}

  默认记录程序生成的输出格式大体为:

  
2007-12-22 22:08:52 LoggingHelloWorld main

INFO: > main(args=[])

Hello World

2007-12-22 22:08:52 LoggingHelloWorld main

INFO: < main()

  可以看到,每个记录语句显示为两行:一行显示时间戳、提供的记录名称和生成调用的方法,另一行是提供的记录正文。

  本文的其余部分将演示如何通过操作加载的字节代码使原始的 Hello World 程序和有记录的 Hello World 程序有一致的行为效果。使用的操作机制是在 Java 5 中添加 Java Instrumentation。

  使用 Java Instrumentation API

  您可以通过 JVM arguments 调用 Java -javaagent:youragent.jar 或者 -javaagent:youragent.jar=argument 在试着运行指定的 main 之前使 Java 调用位于 youragent.jar 清单上的 premain(...) 方法。此 premain(...) 方法允许您通过系统类加载器注册类文件 transformer,它能提供 transform(...) 方法。在此后的进程中,此方法会作为每个类的一部分进行调用,而且可以在由类加载器处理为实际 Class 之前操作实际代码。

  为此,必须保证实现以下几点:

  一个用来实现 ClassFileTransformer 的类。

  transform(...) 方法将在每个类载入时被调用。参数是整个类完全的、原始的字节代码。

  一个用来提供一个静态空白点 premain() 方法的类。

  premain(...) 方法必须通过类加载器注册上面的转换器。它也能处理命令行上的参数。

  一个正确的 MANIFEST.MF 文件 .

  MANIFEST.MF 必须包含 Pre-Class: .. 行通过 premain() 方法访问类。此外,使用 Boot-Class-Path: 访问外部 .jar 文件.

  此代码必须和清单一起放入 .jar 文件,否则它将失败。

  com.runjva.instrumentation.LoggerAgent 示例代理

  本节列出一个名为 com.runjva.instrumentation.LoggerAgent 示例代理。它操作 java.lang.instrument.ClassFileTransformer 接口并提供所需的 premain(...) 方法。

  位于 transform(...) 方法中的实际字节代码操作通过 JBoss "Javassist&quot; 库来实现。这个库提供一个 Java 片断编译器和高级字节代码操作例程。这个编译器允许我们通过创建 Java 字符串片断并编译然后插入到合适的位置进行操作。

  签名抽取和返回值字节抽取方法是相当复杂的,并已经被放置在 com.runjva.instrumentation.JavassistHelper 内。它虽然没有列出但在示例代码 .zip 文件中可用。

  参阅 参考资料 示例代码部分并链接到 Javassist 和相关背景文章。

  这是 com.runjva.instrumentation.LoggerAgent 类:

package com.runjva.instrumentation;

import java.lang.instrument.*;

import java.util.*;

import javassist.*;

public class LoggerAgent implements ClassFileTransformer {

 &nbsp;  public static void premain(String agentArgument,

       Instrumentation instrumentation) {

    if (agentArgument != null) {

 ;     String[] args = agentArgument.split(",");

      Set argSet = new HashSet(Arrays.asList(args));

    &nbsp; if (argSet.contains("time")) {

      ;   System.out.println("Start at " + new Date());

       &nbsp; Runtime.getRuntime().addShutdownHook(new Thread() {

           public void run() {

      &nbsp;      System.out.println("Stop at &quot; + new Date());

        ;  &nbsp;  }

 &nbsp;   &nbsp;   });

       }

 &nbsp;  // ... more agent option handling here

   }

instrumentation.addTransformer(new LoggerAgent());

}

  premain(...) 作为类转换器用来添加 LoggerAgent。它也将字符串参数看作一个逗号分隔的选项列表。如果给出选项 time,则将在此时或停机时打印出日期。


String def = "private static java.util.logging.Logger _log;";

 String ifLog = "if (_log.isLoggable(java.util.logging.Level.INFO))&quot;;

 String[] ignore = new String[] { "sun/", "java/", "javax/" };

  public byte[] transform(ClassLoader loader, String className,

   &nbsp;   Class clazz, java.security.ProtectionDomain domain,

     ;  byte[] bytes) {

&nbsp;  for (int i = 0; i < ignore.length; i++) {

    &nbsp;  if (className.startsWith(ignore[i])) {

     &nbsp;     return bytes;

        }

     }

       return doClass(className, clazz, bytes);

}  

       transform(...) 方法在示例化为实际对象前由系统类加载器加载的每个类调用。每个类都包含载入这些类所需要的代码,避免了对运行时库类添加记录器。需要查看类名称,并返回未修改的库类(注意:分隔符为斜线而不是点)。


 private byte[] doClass(String name, Class clazz, byte[] b) {

       ClassPool pool = ClassPool.getDefault();

       CtClass cl = null;

       try {

          cl = pool.makeClass(new java.io.ByteArrayInputStream(b));

      &nbsp;  if (cl.isInterface() == false) {

  &nbsp;     &nbsp;   CtField field = CtField.make(def, cl);

           String getLogger = "java.util.logging.Logger.getLogger("

               + name.replace('/', '.') + ".class.getName());";

       &nbsp;  cl.addField(field, getLogger);

         CtBehavior[] methods = cl.getDeclaredBehaviors();

        for (int i = 0; i &lt; methods.length; i++) {

     &nbsp;  &nbsp;  if (methods[i].isEmpty() == false) {

    ;   &nbsp;  doMethod(methods[i]);

      ;    }

       }

        b = cl.toBytecode();

    }

} catch (Exception e) {

  System.err.println("Could not instrument " + name

       + ", exception : " + e.getMessage());

 } finally {

     if (cl != null) {

       cl.detach();

     }

 } return b;

}

      doClass(...) 方法使用 Javassist 分析提供的字节流。如果它是一个实际类(与接口相对),则会添加一个名为 _log 的记录器字段,并初始化为类名称。每个非空方法通过 doMethod(...) 处理。 finally 语句确保类定义从 Javassist 池中删除以减少内存占用。

  
private void doMethod(CtBehavior method)

       throws NotFoundException, CannotCompileException {

   String signature = JavassistHelper.getSignature(method);

&nbsp; String returnValue = JavassistHelper.returnValue(method);

  method.insertBefore(ifLog + "_log.info(\">> " + signature

          + ");");

  method.insertAfter(ifLog + "_log.info(\"<< " + signature

   ;     + returnValue + ");");

     }

}
 
  doMethod(...) 类创建 if (_log.isLoggable(INFO))_log.info(...) 代码并插入到每个方法的开头和结尾。选择这个级别作为无需任何记录系统配置就可生成输出的最低级别。

  需要注意的是 JavassistHelper 类在示例代码 .zip 文件中是可用的。(请参阅 参考资料)

  示例 MANIFEST.MF 文件

  此处,只需要两行:一行通过 premain 方法指出类,另一行使 Javassist 可用于代理。

  Premain-Class: com.runjva.instrumentation.LoggerAgentBoot-Class-Path: ../lib/javassist.jar

  需要注意,dist/loggeragent.jar 需要 lib/javassist.jar,即 ../lib 相对路径。

  示例 build.xml 文件

  build.xml 文件包含一个编译目标、一个 .jar 目标、一个传统的 HelloWorld 目标和一个具有记录器代理活动的 HelloWorld 目标。
  
 project name="Logger Agent (Java 5+)" default="all">
  <target name=&quot;all" depends="compile,jar,withoutAgent,withAgent"/>

  <target name="withAgent" description="run with logging added by java agent">
    <java fork="yes" classpath="bin" classname="com.runjva.demo.HelloWorld">
      <jvmarg value="-javaagent:dist/loggeragent.jar=time"/>
    </java>
  </target>

  <target name="withoutAgent" description="run normally">
    <java fork="yes" classpath="bin" classname="com.runjva.demo.HelloWorld">
    </java>
  </target>

  <target name="compile" description="compile classes">
    <delete dir="bin" />
    <mkdir dir="bin" />
    <javac source="1.4" srcdir="src" destdir="bin" debug="true" 
              optimize="true" verbose="false" classpath="lib/javassist.jar">
    </javac>
  </target>

  <target name="jar" depends="compile" description="create agent jar">
<jar basedir="bin" destfile="dist/loggeragent.jar" manifest="Manifest.mf"/>
  </target>
</project>
  运行 ant 产生的输出大体为:


 Buildfile: build.xml

   compile:

   [delete] Deleting directory /home/ravn/workspace/com.runjva.instrumentation/bin

&nbsp; [mkdir] Created dir: /home/ravn/workspace/com.runjva.instrumentation/bin

 [javac] Compiling 3 source files to /home/ravn/workspace/com.runjva.instrumentation/bin

jar:

&nbsp;[jar] Building jar: /home/ravn/workspace/com.runjva.instrumentation/dist/loggeragent.

jarwithoutAgent:

 [java] Hello World

withAgent:

 [java] Start at Fri Apr 18 21:13:53 CEST 2008

 [java] 18-04-2008 21:13:54 com.runjva.demo.HelloWorld main

 [java] INFO: >> main(args=[]) [java] Hello World

&nbsp;[java] 18-04-2008 21:13:54 com.runjva.demo.HelloWorld main

 [java] INFO: << main(args=[])

 [java] Stop at Fri Apr 18 21:13:54 CEST 2008all:BUILD SUCCESSFULTotal time: 2 seconds
 
  此输出显示已经添加了记录语句并实际生成了输出。实际的语句顺序可能在运行中有所改变,这是由于记录语句将被写入 System.err 和时间信息,输出将从 HelloWorld 写入 System.out。

  结束语

  Java Instrumentation API 可以不需要改变源代码或编译的字节代码透明地对运行时上的任何 Java 代码添加方法-调用记录。通过自动生成记录语句,保证了他们总是最新的,这样,减轻了程序员单调繁重的任务操作。


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@hmdbvip.cn

文章转载自:IT专家网论坛

为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP
PM娱乐城网络现金网站(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新) PM娱乐城最大(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新) 永利外围最新(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新) 网络权威朗驰娱乐大全(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新) 永利真人网上足球(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新) 利记最火十大网(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新) boyu·博鱼权威网络足球(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新) PM娱乐城网上足球(官方)网站/网页版登录入口/手机版登录入口-最新版(已更新)