用Java Instrumentation 在类加载时添加记录

发布者:xanadu0214   来源:网络转载   发布日期:2013年11月06日   Java学习交流群:471651004
在分析程序出错的原因时,了解它当时的状态将是非常有用的。在许多情况下,我们可以通过堆栈追踪实现此目的,但这些信息经常都是不可用的,或者您需要的可能是程序在出错时处理数据的相关信息。

  传统做法是使用 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)) {

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

  }

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

  if (_log.isLoggable(Level.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" 库来实现。这个库提供一个 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 {

  public static void premain(String agentArgument,

  Instrumentation instrumentation) {

  if (agentArgument != null) {

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

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

  if (ntains("time")) {

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

  Runtime.getRuntime().addShutdownHook(new Thread() {

  public void run() {

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

  }

  });

  }

  // ... 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))";

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

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

  Class clazz, java.security.ProtectionDomain domain,

  byte[] bytes) {

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

  if (className.startsWith(ignore[i])) {

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

  if (cl.isInterface() == false) {

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

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

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

  cl.addField(field, getLogger);

  CtBehavior[] methods = cl.getDeclaredBehaviors();

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

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

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

  String returnValue = JavassistHelper.returnValue(method);

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

  + ");");

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

  + returnValue + ");");

  }

  }

  doMethod(...) 类创建 if (_log.isLoggable(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="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

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

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

  jar:

  [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

  [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 代码添加方法-调用记录。通过自动生成记录语句,保证了他们总是最新的,这样,减轻了程序员单调繁重的任务操作。

标签: 用JavaInstrumentation在类加载时添加记录 JavaInstrumentation

用Java Instrumentation 在类加载时添加记录相关推荐

  1. Java PDF数字签名(五)—Java 给PDF签名时添加可信时间戳

    一.程序运行环境 编译环境:IntelliJ IDEA 所需测试文件:PDF..pfx数字证书及密钥.PDF Jar包(Free Spire.PDF for Java).签名图片(.png格式) 可信 ...

  2. java easypoi导出word时添加空白页导出多组相同类型的数据

    1.需求 导出用户信息,模板如下: 要求:导出多个用户信息时,另起一页,根据模板生成下一条用户信息. 2.模板变化 easypoi不能直接创建空白页(下一页),但可以使用apache的poi中的doc ...

  3. mybatis添加记录时返回主键id

    参考:mybatis添加记录时返回主键id 场景 有些时候我们在添加记录成功后希望能直接获取到该记录的主键id值,而不需要再执行一次查询操作. 在使用mybatis作为ORM组件时,可以很方便地达到这 ...

  4. java -jar vm参数_java相关:运行jar程序时添加vm参数的方法

    java相关:运行jar程序时添加vm参数的方法 发布于 2020-3-15| 复制链接 下面小妖就为大家带来一篇运行jar程序时添加vm参数的方法.小妖觉得挺不错的,现在就分享给大家,也给大家做个参 ...

  5. MySql数据库记录相差14小时排错,使用Java访问Mysql数据库时出现时区异常的解决方案

    最近遇到1个大坑,A系统迁移到B系统,2边系统 同1个字段 createTime 看到的不一致. 表象: A系统: 2019-6-10 17:34 B系统: 2019-6-11 .... 再次尝试: ...

  6. Java单元测试和类加载

    Java单元测试和类加载 回顾: 1 Lambda表达式:相等于匿名内部类,实现代码作为方法的参数传统.函数式接口 变量=(参数列表)->{方法体};注意: ->操作符 分成两部分 左侧: ...

  7. JAVA Instrumentation

    什么是Instrumentation? java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序.这种监测和协助包括但不限于获 ...

  8. Java对象转JSON时如何动态的增删改查属性

    1. 前言 日常开发中少不了 JSON 处理,少不了需要在 JSON 中添加额外字段或者删除特定字段的需求.今天我们就使用Jackson类库来实现这个功能. 2. JSON 字符串增加额外字段 假如我 ...

  9. 获取虚拟机的唯一标识_JVM笔记:Java虚拟机的类加载机制(附详细思维导图)...

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 类加载的流程 类从被加载到虚拟机内存中开始, ...

最新文章

  1. python renames_Python os.renames() 方法
  2. 面向对象封装之无参无返,无参有返
  3. 转换流指定编码读写文件
  4. android主板读取vga线数据_智锐通掘金新基建上新系列之3.5quot; 与ATX工业主板图鉴...
  5. 微软e5服务器,OFFICE365 E5调用api使E5开发者续订(不使用服务器)
  6. Linux学习总结(6)——CenterOS7安装mysql5.5的方法
  7. 计算机网络的功能分布计算,网络中心的分布计算(转帖)
  8. 进度条(5.16-5.22)
  9. 蚂蚁森林用户须知_蚂蚁森林刷能量漏洞(轻松读懂规则)
  10. 猪八戒网分析报告_米米米米粒口红_新浪博客
  11. H3C 无线控制器关闭信号认证
  12. linux配置dhcp服务器时authoritative参数的作用
  13. 成功的背后!(给所有IT人)--转自CSDN博客排名第二 的大神
  14. 微信视频号视频或直播预约二维码如何生成?
  15. 匿名发脉脉的拼多多员工,是如何被发现的?背后真相令人发指...
  16. Wireshark 网卡出错 The capture session could not be initiated on interface ‘en0‘
  17. 谷歌浏览器(Chrome)最新v80版本下载
  18. 后端开发如何快速转前端开发
  19. telephony-apps
  20. nkoj 中山纪念中学 trie

热门文章

  1. PHP如何设置圆,php绘制圆形的方法
  2. PHP脚本执行超时的解决办法
  3. C语言素数的乘积,判断一个正整数是否有两个素数的乘积
  4. mysql按条件提取数据库_SQL中的where条件,在数据库中提取与应用浅析
  5. python功能强大的库_2018 年最受欢迎的15个顶级 Python 库
  6. python爬网页数据用什么_初学者如何用“python爬虫”技术抓取网页数据?
  7. checkbox居中 editor_C1FlexGrid绑定自定义Editor-C1CheckBox
  8. 注塑机摆放间距多少合适_红木家具之间正确的摆放距离
  9. java中io流实现哪个接口_第55节:Java当中的IO流-时间api(下)-上
  10. python字典转字符串后里面的冒号还在吗_Python字典、字符串及列表的相互转换