Instrumentation介绍学习

Instrumentation介绍html

Java Instrumentation指的是能够用独立于应用程序以外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。 Java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理机制,支持独立于JVM应用程序以外的程序以代理的方式链接和访问JVM。Instrumentation 的最大做用就是类定义的动态改变和操做。在 Java SE 5 及其后续版本当中,开发者能够在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,经过 – javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。java

premain方式
在Java SE5时代,Instrument只提供了premain一种方式,即在真正的应用程序(包含main方法的程序)main方法启动前启动一个代理程序。例如使用以下命令:web

 

[java] view plain copyapi

  1. java -javaagent:agent_jar_path[=options] java_app_name  


能够在启动名为java_app_name的应用以前启动一个agent_jar_path指定位置的agent jar。 实现这样一个agent jar包,必须知足两个条件:

 oracle

  1. 在这个jar包的manifest文件中包含Premain-Class属性,而且改属性的值为代理类全路径名。
  2. 代理类必须提供一个public static void premain(String args, Instrumentation inst)或 public static void premain(String args) 方法。

当在命令行启动该代理jar时,VM会根据manifest中指定的代理类,使用于main类相同的系统类加载器(即ClassLoader.getSystemClassLoader()得到的加载器)加载代理类。在执行main方法前执行premain()方法。若是premain(String args, Instrumentation inst)和premain(String args)同时存在时,优先使用前者。其中方法参数args即命令中的options,类型为String(注意不是String[]),所以若是须要多个参数,须要在方法中本身处理(好比用";"分割多个参数之类);inst是运行时由VM自动传入的Instrumentation实例,能够用于获取VM信息。app

 

premain实例-打印全部的方法调用
下面实现一个打印程序执行过程当中全部方法调用的功能,这个功能能够经过AOP其余方式实现,这里只是尝试使用Instrumentation进行ClassFile的字节码转换实现:

构造agent类

premain方式的agent类必须提供premain方法,代码以下:ide

 

[java] view plain copy函数

  1. package test;  
  2.   
  3. import java.lang.instrument.Instrumentation;  
  4.   
  5. public class Agent {  
  6.   
  7.     public static void premain(String args, Instrumentation inst){  
  8.         System.out.println("Hi, I'm agent!");  
  9.         inst.addTransformer(new TestTransformer());  
  10.     }  
  11. }  

premain有两个参数,args为自定义传入的代理类参数,inst为VM自动传入的Instrumentation实例。 premain方法的内容很简单,除了标准输出外,只有测试

 

 

[java] view plain copyspa

  1. inst.addTransformer(new TestTransformer());  

这行代码的意思是向inst中添加一个类的转换器。用于转换类的行为。

构造Transformer

下面来实现上述过程当中的TestTransformer来完成打印调用方法的类定义转换。

 

 

[java] view plain copy

  1. package test;  
  2.   
  3. import java.lang.instrument.ClassFileTransformer;  
  4. import java.lang.instrument.IllegalClassFormatException;  
  5. import java.security.ProtectionDomain;  
  6.   
  7. import org.objectweb.asm.ClassReader;  
  8. import org.objectweb.asm.ClassWriter;  
  9. import org.objectweb.asm.Opcodes;  
  10. import org.objectweb.asm.tree.ClassNode;  
  11. import org.objectweb.asm.tree.FieldInsnNode;  
  12. import org.objectweb.asm.tree.InsnList;  
  13. import org.objectweb.asm.tree.LdcInsnNode;  
  14. import org.objectweb.asm.tree.MethodInsnNode;  
  15. import org.objectweb.asm.tree.MethodNode;  
  16.   
  17. public class TestTransformer implements ClassFileTransformer {  
  18.   
  19.     @Override  
  20.     public byte[] transform(ClassLoader arg0, String arg1, Class<?> arg2,  
  21.             ProtectionDomain arg3, byte[] arg4)  
  22.             throws IllegalClassFormatException {  
  23.         ClassReader cr = new ClassReader(arg4);  
  24.         ClassNode cn = new ClassNode();  
  25.         cr.accept(cn, 0);  
  26.         for (Object obj : cn.methods) {  
  27.             MethodNode md = (MethodNode) obj;  
  28.             if ("<init>".endsWith(md.name) || "<clinit>".equals(md.name)) {  
  29.                 continue;  
  30.             }  
  31.             InsnList insns = md.instructions;  
  32.             InsnList il = new InsnList();  
  33.             il.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System",  
  34.                     "out", "Ljava/io/PrintStream;"));  
  35.             il.add(new LdcInsnNode("Enter method-> " + cn.name+"."+md.name));  
  36.             il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,  
  37.                     "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));  
  38.             insns.insert(il);  
  39.             md.maxStack += 3;  
  40.   
  41.         }  
  42.         ClassWriter cw = new ClassWriter(0);  
  43.         cn.accept(cw);  
  44.         return cw.toByteArray();  
  45.     }  
  46.   
  47. }  


TestTransformer实现了ClassFileTransformer接口,该接口只有一个transform方法,参数传入包括该类的类加载器,类名,原字节码字节流等,返回被转换后的字节码字节流。 TestTransformer主要使用ASM实如今全部的类定义的方法中,在方法开始出添加了一段打印该类名和方法名的字节码。在转换完成后返回新的字节码字节流。详细的ASM使用请参考ASM手册。

 

设置MANIFEST.MF

设置MANIFEST.MF文件中的属性,文件内容以下:

 

[java] view plain copy

  1. Manifest-Version: 1.0  
  2. Premain-Class: test.Agent  
  3. Created-By: 1.6.0_29  

测试
代码编写完成后将代码编译打成agent.jar。 编写测试代码:

[java] view plain copy

  1. public class TestAgent {  
  2.   
  3.     public static void main(String[] args) {  
  4.         TestAgent ta = new TestAgent();  
  5.         ta.test();  
  6.     }  
  7.   
  8.     public void test() {  
  9.         System.out.println("I'm TestAgent");  
  10.     }  
  11.   
  12. }  


从命令行执行该类,并设置agent.jar

 

 

[java] view plain copy

  1. java -javaagent:agent.jar TestAgent  

将打印出程序运行过程当中实际执行过的全部方法名:

[java] view plain copy

  1. Hi, I'm agent!  
  2. Enter method-> test/TestAgent.main  
  3. Enter method-> test/TestAgent.test  
  4. I'm TestAgent  
  5. Enter method-> java/util/IdentityHashMap$KeySet.iterator  
  6. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext  
  7. Enter method-> java/util/IdentityHashMap$KeyIterator.next  
  8. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex  
  9. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext  
  10. Enter method-> java/util/IdentityHashMap$KeySet.iterator  
  11. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext  
  12. Enter method-> java/util/IdentityHashMap$KeyIterator.next  
  13. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex  
  14. Enter method-> com/apple/java/Usage$3.run  
  15. 。。。  

 

 

从输出中能够看出,程序首先执行的是代理类中的premain方法(不过代理类自身不会被本身转换,因此不能打印出代理类的方法名),而后是应用程序中的main方法。

agentmain方式

premain时Java SE5开始就提供的代理方式,给了开发者诸多惊喜,不过也有些须不变,因为其必须在命令行指定代理jar,而且代理类必须在main方法前启动。所以,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较苦难的。好比正常的生产环境下,通常不会开启代理功能,可是在发生问题时,咱们不但愿中止应用就可以动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是没法肯定的。 为解决运行时启动代理类的问题,Java SE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 与Permain相似,agent方式一样须要提供一个agent jar,而且这个jar须要知足:

  1. 在manifest中指定Agent-Class属性,值为代理类全路径
  2. 代理类须要提供public static void agentmain(String args, Instrumentation inst)或public static void agentmain(String args)方法。而且再两者同时存在时之前者优先。args和inst和premain中的一致。

不过如此设计的再运行时进行代理有个问题——如何在应用程序启动以后再开启代理程序呢? JDK6中提供了Java Tools API,其中Attach API能够知足这个需求。

Attach API中的VirtualMachine表明一个运行中的VM。其提供了loadAgent()方法,能够在运行时动态加载一个代理jar。具体须要参考《Attach API》

agentmain实例-打印当前已加载的类

构造agent类

agentmain方式的代理类必须提供agentmain方法:

package loaded;

import java.lang.instrument.Instrumentation;

public class LoadedAgent {
    @SuppressWarnings("rawtypes")
    public static void agentmain(String args, Instrumentation inst){
        Class[] classes = inst.getAllLoadedClasses();
        for(Class cls :classes){
            System.out.println(cls.getName());
        }
    }
}

agentmain方法经过传入的Instrumentation实例获取当前系统中已加载的类。

设置MANNIFEST.MF

设置MANIFEST.MF文件,指定Agent-Class:

Manifest-Version: 1.0
Agent-Class: loaded.LoadedAgent
Created-By: 1.6.0_29

绑定到目标VM

将agent类和MANIFEST.MF文件编译打成loadagent.jar后,因为agent main方式没法向pre main方式那样在命令行指定代理jar,所以须要借助Attach Tools API。

package attach;

import java.io.IOException;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

public class Test {
    public static void main(String[] args) throws AttachNotSupportedException,
            IOException, AgentLoadException, AgentInitializationException {
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        vm.loadAgent("/Users/jiangbo/Workspace/code/java/javaagent/loadagent.jar");

    }

}

该程序接受一个参数为目标应用程序的进程id,经过Attach Tools API的VirtualMachine.attach方法绑定到目标VM,并向其中加载代理jar。

构造目标测试程序

构造一个测试用的目标应用程序:

package attach;

public class TargetVM {
    public static void main(String[] args) throws InterruptedException{
        while(true){
            Thread.sleep(1000);
        }
    }
}

这个测试程序什么都不作,只是不停的sleep。:) 运行该程序,得到进程ID=33902。 运行上面绑定到VM的Test程序,将进程id做为参数传入:

java attach.Test 33902

观察输出,会打印出系统当前全部已经加载类名

java.lang.NoClassDefFoundError
java.lang.StrictMath
java.security.SignatureSpi
java.lang.Runtime
java.util.Hashtable$EmptyEnumerator
sun.security.pkcs.PKCS7
java.lang.InterruptedException
java.io.FileDescriptor$1
java.nio.HeapByteBuffer
java.lang.ThreadGroup
[Ljava.lang.ThreadGroup;
java.io.FileSystem
。。。

参考文档

附:agent jar中manifest的属性

  • Premain-Class: 当在VM启动时,在命令行中指定代理jar时,必须在manifest中设置Premain-Class属性,值为代理类全类名,而且该代理类必须提供premain方法。不然JVM会异常终止。
  • Agent-Class: 当在VM启动以后,动态添加代理jar包时,代理jar包中manifest必须设置Agent-Class属性,值为代理类全类名,而且该代理类必须提供agentmain方法,不然没法启动该代理。
  • Boot-Class-Path: Bootstrap class loader加载类时的搜索路径,可选。
  • Can-Redefine-Classes: true/false;标示代理类是否可以重定义类。可选。
  • Can-Retransform-Classes: true/false;标示代理类是否可以转换类定义。可选。
  • Can-Set-Native-Prefix::true/false;标示代理类是否须要本地方法前缀,可选。

当一个代理jar包中的manifest文件中既有Premain-Class又有Agent-Class时,若是以命令行方式在VM启动前指定代理jar,则使用Premain-Class;反之若是在VM启动后,动态添加代理jar,则使用Agent-Class

相关文章
相关标签/搜索