java instrument agent

Bertha 。 2022-07-19 02:19 276阅读 0赞

利用 Java 代码,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。

在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

另外,对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。

最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path 的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。

Sun JVM Attach API是Sun JVM中的一套非标准的可以连接到JVM上的API,从JDK6开始引入,除了Solaris平台的Sun JVM支持远程的Attach,在其他平台都只允许Attach到本地的JVM上。
一、Sun JVM Attach API功能上非常简单,仅提供了如下几个功能:

  • 列出当前所有的JVM实例描述(知道JDK工具jps吗,列出所有的java进程的pid)
  • Attach到其中一个JVM上,建立通信管道
  • 让目标JVM加载Agent(还记得JVM TI吧,运行后加载的Agent)

    二、通过几个实例简单地了解一下如上的几个功能
    1.列出当前所有的JVM实例描述

Java代码 收藏代码

  1. List list = VirtualMachine.list();
  2. for (VirtualMachineDescriptor vmd : list)
  3. {
  4. System.out.println(“pid:” + vmd.id() + “:” + vmd.displayName());
  5. }

编写一个javaAgent:

在premain方法的参数里:

[java] view plain copy

  1. public static void premain(String agentArgs, Instrumentation inst);

    java.lang.instrument 在jdk5之前的版本中是没有的,它是jdk5之后引入的新特性,这个特定将java的instrument功能从native库中解脱了出来,而使用纯java的方式来解决问题。

那么java instrumentation具体能干些什么呢?

使用instrumentation开发者可以构建独立于应用程序的java agent(代理)程序,用来监测运行在JVM上的程序,甚至可以动态的修改和替换类的定义。给力的说,这种方式相当于在JVM级别做了AOP支持,这样我们可以在不修改应用程序的基础上就做到了AOP.你不必去修改应用程序的配置,也不必重新打包部署验证。

下面讲到的基本都是以jdk5为基础的,当然JDK6已经更好的支持了这个特性。比如JDK5中只能通过命令行参数在启动JVM时指定javaagent参数来设置代理类,比如:

[java] view plain copy

  1. rem start the monitor..
  2. set JAVA_OPTS=%JAVA_OPTS% -javaagent:D:\tools\java\monitor.jar

而JDK6中已经不仅限于在启动JVM时通过配置参数来设置代理类,JDK6中通过 Java Tool API 中的 attach 方式,我们也可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

但是作为介绍和学习起步,我觉得JDK5支持的这点特定已经够了。now,let’s go!微笑

java instrument相关的类主要在Package java.lang.instrument下面,看它package的描述:

Provides services that allow Java programming language agents to instrument programs running on the JVM. The mechanism for instrumentation is modification of the byte-codes of methods.


关于java instrument

“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。

我认为Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 –javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序(这和上面讲到agent时是一致的,只是当时没有利用Instrumentation功能)。

现在写个简单的例子来说明java instrument的功能,这个例子很简单,就是计算某些方法的耗时,在最原始的方法中我们是这样做的,如下代码:

[java] view plain copy

  1. package monitor.agent;
  2. /**
  3. * TODO Comment of MyTest
  4. *
  5. * @author yongkang.qiyk
  6. */
  7. public class MyTest {
  8. public static void main(String[] args) {
  9. sayHello();
  10. }
  11. public static void sayHello() {
  12. long startTime = System.currentTimeMillis();
  13. try {
  14. Thread.sleep(2000);
  15. System.out.println(“hello world!!”);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. long endTime = System.currentTimeMillis();
  20. System.out.println(“this method cost:” + (endTime - startTime) + “ms.”);
  21. }
  22. }

这样的方式优势劣势都很明显,优势:简单,任何人都会都能做。 劣势:假如有很多个方法要统计耗时时,需要手工在每个方法里加入上面红色部分的代码,然后编译打包部署。

如果利用Instrumentation 代理来实现这个功能是什么样的呢?

首先我们要测试的类依然是:MyTest.java,源码如下:

[java] view plain copy

  1. package monitor.agent;
  2. /**
  3. * TODO Comment of MyTest
  4. *
  5. * @author yongkang.qiyk
  6. */
  7. public class MyTest {
  8. public static void main(String[] args) {
  9. sayHello();
  10. sayHello2(“hello world222222222”);
  11. }
  12. public static voidsayHello() {
  13. try {
  14. Thread.sleep(2000);
  15. System.out.println(“hello world!!”);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. public static void sayHello2(String hello) {
  21. try {
  22. Thread.sleep(1000);
  23. System.out.println(hello);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }

这一次我没有手工的加入System.currentTimeMillis(); 上面是源码。我们可以直接运行它,可以得到如下结果:

[javascript] view plain copy

  1. hello world!!
  2. hello world222222222

接下来,我们建立一个 Transformer 类:MonitorTransformer 。

这个类实现了接口public interface ClassFileTransformer。 实现这个接口的目的就是在class被装载到JVM之前将class字节码转换掉,从而达到动态注入代码的目的。

那么首先要了解MonitorTransformer 这个类的目的,就是对想要修改的类做一次转换,这个用到了javassist对字节码进行修改,可以暂时不用关心jaavssist的原理,用ASM同样可以修改字节码,只不过比较麻烦些。只要知道这个类利用jaavssist将 monitor.agent.MyTest.sayHello 和 monitor.agent.MyTest.sayHello2 两个方法动态了添加了耗时统计的代码就可以了。源码如下:

[java] view plain copy

  1. /**
  2. * TODO Comment of MonitorTransformer
  3. * @author yongkang.qiyk
  4. *
  5. */
  6. publicclass MonitorTransformerimplements ClassFileTransformer {
  7. finalstatic Stringprefix =”\nlong startTime = System.currentTimeMillis();\n”;
  8. finalstatic Stringpostfix =”\nlong endTime = System.currentTimeMillis();\n”;
  9. finalstatic ListmethodList =new ArrayList();
  10. static{
  11. methodList.add(“monitor.agent.MyTest.sayHello”);
  12. methodList.add(“monitor.agent.MyTest.sayHello2”);
  13. }
  14. /* (non-Javadoc)
  15. * @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, java.lang.String, java.lang.Class, java.security.ProtectionDomain, byte[])
  16. */
  17. @Override
  18. publicbyte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  19. ProtectionDomain protectionDomain,byte[] classfileBuffer)
  20. throws IllegalClassFormatException {
  21. //先判断下现在加载的class的包路径是不是需要监控的类,通过instrumentation进来的class路径用‘/’分割
  22. if(className.startsWith(“monitor/agent”)){
  23. //将‘/’替换为‘.’m比如monitor/agent/Mytest替换为monitor.agent.Mytest
  24. className = className.replace(“/“,”.”);
  25. CtClass ctclass = null;
  26. try {
  27. //用于取得字节码类,必须在当前的classpath中,使用全称 ,这部分是关于javassist的知识
  28. ctclass = ClassPool.getDefault().get(className);
  29. //循环一下,看看哪些方法需要加时间监测
  30. for(String method :methodList){
  31. if (method.startsWith(className)){
  32. //获取方法名
  33. String methodName = method.substring(method.lastIndexOf(‘.’)+1, method.length());
  34. String outputStr =”\nSystem.out.println(\“this method “+methodName+” cost:\“ +(endTime - startTime) +\“ms.\“);”;
  35. //得到这方法实例
  36. CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);
  37. //新定义一个方法叫做比如sayHello$impl
  38. String newMethodName = methodName +”$impl”;
  39. //原来的方法改个名字
  40. ctmethod.setName(newMethodName);
  41. //创建新的方法,复制原来的方法 ,名字为原来的名字
  42. CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass,null);
  43. //构建新的方法体
  44. StringBuilder bodyStr =new StringBuilder();
  45. bodyStr.append(“{“);
  46. bodyStr.append(prefix);
  47. //调用原有代码,类似于method();($$)表示所有的参数
  48. bodyStr.append(newMethodName +”($$);\n”);
  49. bodyStr.append(postfix);
  50. bodyStr.append(outputStr);
  51. bodyStr.append(“}“);
  52. //替换新方法
  53. newMethod.setBody(bodyStr.toString());
  54. //增加新方法
  55. ctclass.addMethod(newMethod);
  56. }
  57. }
  58. return ctclass.toBytecode();
  59. } catch (IOException e) {
  60. //TODO Auto-generated catch block
  61. e.printStackTrace();
  62. } catch (CannotCompileException e) {
  63. //TODO Auto-generated catch block
  64. e.printStackTrace();
  65. } catch (NotFoundException e) {
  66. //TODO Auto-generated catch block
  67. e.printStackTrace();
  68. }
  69. }
  70. returnnull;
  71. }
  72. }

经过这个代码动态的添加代码之后原来的代码会变成和第一个手工添加System.currentTimeMillis();一样。

最后,我们还需要一个agent类,就是建立一个 Premain 类,将instrumentation注入进去,代码如下:

[java] view plain copy

  1. package monitor.agent;
  2. import java.lang.instrument.Instrumentation;
  3. /**
  4. * TODO Comment of MyAgent
  5. * @author yongkang.qiyk
  6. *
  7. */
  8. publicclass MyAgent {
  9. publicstaticvoid premain(String agentArgs, Instrumentation inst){
  10. System.out.println(“premain-1.”+agentArgs);
  11. inst.addTransformer(new MonitorTransformer());
  12. }
  13. }

到此为止,agent类已经修改字节码的类都已经写好了。将agent类打成jar包,

注意:MAINFESR.MF文件也打进去,最后一行一定要留空行,不然肯定会报错

Manifest-Version: 1.0
Premain-Class: monitor.agent.MyAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist.jar

空行

上面打成的jar包我叫做monitor.jar,放在D:\tools\java\monitor.jar 路径下,当然这个路径下还有刚才编写classtransformer类需要的一些第三方jar包,比如javassist.jar,在MENFEST.MF的Boot-Class-Path属性中也指定了这个jar包。

现在条件都具备了,就可以运行MyTest这个类了。

如前面两节所说的一样,要想使用agent类,需要设置JVM启动的参数。右键Run as —> Run configurations,设置运行参数:

566x623

运行,可以输出结果已经有点意思了:

596x289

so,我们既没有手工的去修改MyTest类的每个方法,也不需要重新打包部署应用代码。只要在启动应用时加上-javaagent参数,利用java instrumentation来修改class字节码,从而达到AOP的效果。

以后要是在多加一个monitor.agent.MyTest.sayHello3需要监测耗时,也不需要修改应用代码,只要在MonitorTransformer的methodList中多加一个方法就可以了:

[java] view plain copy

  1. static{
  2. methodList.add(“monitor.agent.MyTest.sayHello”);
  3. methodList.add(“monitor.agent.MyTest.sayHello2”);
  4. methodList.add(“monitor.agent.MyTest.sayHello3”);
  5. }

如果想搞的更智能一些,methodList可以搞成配置文件配置的,而不用写死在代码中。

下一次就写一下怎么利用java的-D参数用配置文件来配置methodList……..

发表评论

表情:
评论列表 (有 0 条评论,276人围观)

还没有评论,来说两句吧...

相关阅读

    相关 java agent

    一、基本概念 `Java agent`是在`JDK1.5`引入的,是一种可以动态修改`Java`字节码的技术。java类编译之后形成字节码被`JVM`执行,`JVM`在执

    相关 Java Agent

    一、什么是 Java Agent ? 笼统地来讲,Java Agent 是一个统称,该功能是 Java 虚拟机提供的一整套后门。通过这套后门可以对虚拟机方方面面进行监控与