• Instrumentation(1)


    Instrumentation介绍:

    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 的代理程序。

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

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

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


    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信息。

    premain实例-打印所有的方法调用
    下面实现一个打印程序执行过程中所有方法调用的功能,这个功能可以通过AOP其他方式实现,这里只是尝试使用Instrumentation进行ClassFile的字节码转换实现:

    构造agent类

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

    [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 copy
    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

  • 相关阅读:
    java的构造方法 java程序员
    No result defined for action cxd.action.QueryAction and result success java程序员
    大学毕业后拉开差距的真正原因 java程序员
    hibernate的回滚 java程序员
    验证码 getOutputStream() has already been called for this response异常的原因和解决方法 java程序员
    浅谈ssh(struts,spring,hibernate三大框架)整合的意义及其精髓 java程序员
    你平静的生活或许会在某个不可预见的时刻被彻底打碎 java程序员
    Spring配置文件中使用ref local与ref bean的区别. 在ApplicationResources.properties文件中,使用<ref bean>与<ref local>方法如下 java程序员
    poj1416Shredding Company
    poj1905Expanding Rods
  • 原文地址:https://www.cnblogs.com/zhangboyu/p/7452497.html
Copyright © 2020-2023  润新知