zoukankan      html  css  js  c++  java
  • java.lang.instrument使用

    Java在1.5引入java.lang.instrument,你可以由此实现一个Java agent,通过此agent来修改类的字节码即改变一个类。

    程序启动之时启动代理(pre-main)

    通过java instrument 实现一个简单的profiler。当然instrument并不限于profiler,instrument可以做很多事情,它类似一种更低级,更松耦合的AOP,可以从底层来改变一个类的行为,你可以由此产生无限的遐想。

    接下来要做的事情,就是计算一个方法所花的时间,通常我们会在代码这么写: 
    在方法开始开头加入long stime = System.nanoTime();
    在方法结尾通过System.nanoTime()-stime得出方法所花时间,

    你不得不在你想监控的每个方法中写入重复的代码,好一点的情况,你可以用AOP来干这事,但总是感觉有点别扭,这种profiler的代码还是打包在你的项目中,java instrument使得这更干净。

    写agent类

    import java.lang.instrument.Instrumentation;  
    import java.lang.instrument.ClassFileTransformer;  
    public class PerfMonAgent {  
        static private Instrumentation inst = null;  
        /** 
         * This method is called before the application’s main-method is called, 
         * when this agent is specified to the Java VM. 
         **/  
        public static void premain(String agentArgs, Instrumentation _inst) {  
            System.out.println("PerfMonAgent.premain() was called.");  
            // Initialize the static variables we use to track information.  
            inst = _inst;  
            // Set up the class-file transformer.  
            ClassFileTransformer trans = new PerfMonXformer();  
            System.out.println("Adding a PerfMonXformer instance to the JVM.");  
            inst.addTransformer(trans);  
        }  
    }  

    写ClassFileTransformer类

    import java.lang.instrument.ClassFileTransformer;  
    import java.lang.instrument.IllegalClassFormatException;  
    import java.security.ProtectionDomain;  
    import javassist.CannotCompileException;  
    import javassist.ClassPool;  
    import javassist.CtBehavior;  
    import javassist.CtClass;  
    import javassist.NotFoundException;  
    import javassist.expr.ExprEditor;  
    import javassist.expr.MethodCall;  
    public class PerfMonXformer implements ClassFileTransformer {  
        public byte[] transform(ClassLoader loader, String className,  
                Class<?> classBeingRedefined, ProtectionDomain protectionDomain,  
                byte[] classfileBuffer) throws IllegalClassFormatException {  
            byte[] transformed = null;  
            System.out.println("Transforming " + className);  
            ClassPool pool = ClassPool.getDefault();  
            CtClass cl = null;  
            try {  
                cl = pool.makeClass(new java.io.ByteArrayInputStream(  
                        classfileBuffer));  
                if (cl.isInterface() == false) {  
                    CtBehavior[] methods = cl.getDeclaredBehaviors();  
                    for (int i = 0; i < methods.length; i++) {  
                        if (methods[i].isEmpty() == false) {  
                            doMethod(methods[i]);  
                        }  
                    }  
                    transformed = cl.toBytecode();  
                }  
            } catch (Exception e) {  
                System.err.println("Could not instrument  " + className  
                        + ",  exception : " + e.getMessage());  
            } finally {  
                if (cl != null) {  
                    cl.detach();  
                }  
            }  
            return transformed;  
        }  
        private void doMethod(CtBehavior method) throws NotFoundException,  
                CannotCompileException {  
            // method.insertBefore("long stime = System.nanoTime();");  
            // method.insertAfter("System.out.println(/"leave "+method.getName()+" and time:/"+(System.nanoTime()-stime));");  
            method.instrument(new ExprEditor() {  
                public void edit(MethodCall m) throws CannotCompileException {  
                    m.replace("{ long stime = System.nanoTime(); $_ = $proceed($$); System.out.println(/""  
                                    + m.getClassName()+"."+m.getMethodName()  
                                    + ":/"+(System.nanoTime()-stime));}");  
                }  
            });  
        }  
    }  

    上面两个类就是agent的核心了,jvm启动时并会在应用加载前会调用 PerfMonAgent.premain, 然后PerfMonAgent.premain中实例化了一个定制的ClassFileTransforme即 PerfMonXformer并通过inst.addTransformer(trans);把PerfMonXformer的实例加入Instrumentation实例(由jvm传入),这就使得应用中的类加载的时候, PerfMonXformer.transform都会被调用,你在此方法中可以改变加载的类,为了改变类的字节码,使用了jboss的javassist,虽然你不一定要这么用,但jboss的javassist真的很强大,让你很容易的改变类的字节码。在上面的方法中通过改变类的字节码,在每个类的方法入口中加入了long stime = System.nanoTime();,在方法的出口加入了System.out.println("methodClassName.methodName:"+(System.nanoTime()-stime));

    打包agent

    对于agent的打包,有点讲究,

    1. jar的META-INF/MANIFEST.MF加入Premain-Class: xx, xx在此语境中就是我们的agent类,即org.toy.PerfMonAgent
    2. 如果你的agent类引入别的包,需使用Boot-Class-Path: xx,xx在此语境中就是上面提到的jboss javassit 即/home/pwlazy/.m2/repository/javassist/javassist/3.8.0 .GA/javassist-3.8.0.GA.jar

    下面附上maven的pom

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">  
      <modelVersion>4.0.0</modelVersion>  
      <groupId>org.toy</groupId>  
      <artifactId>toy-inst</artifactId>  
      <packaging>jar</packaging>  
      <version>1.0-SNAPSHOT</version>  
      <name>toy-inst</name>  
      <url>http://maven.apache.org</url>  
      <dependencies>  
         <dependency>  
          <groupId>javassist</groupId>  
          <artifactId>javassist</artifactId>  
          <version>3.8.0.GA</version>  
        </dependency>  
        <dependency>  
          <groupId>junit</groupId>  
          <artifactId>junit</artifactId>  
          <version>3.8.1</version>  
          <scope>test</scope>  
        </dependency>  
      </dependencies>  
       <build>  
        <plugins>  
          <plugin>  
            <groupId>org.apache.maven.plugins</groupId>  
            <artifactId>maven-jar-plugin</artifactId>  
            <version>2.2</version>  
            <configuration>  
              <archive>  
                <manifestEntries>  
                  <Premain-Class>org.toy.PerfMonAgent</Premain-Class>  
                  <Boot-Class-Path>/home/pwlazy/.m2/repository/javassist/javassist/3.8.0.GA/javassist-3.8.0.GA.jar</Boot-Class-Path>  
                </manifestEntries>  
              </archive>  
            </configuration>  
          </plugin>  
          <plugin>  
           <artifactId>maven-compiler-plugin </artifactId >  
                  <configuration>  
                      <source> 1.6 </source >  
                      <target> 1.6 </target>  
                  </configuration>  
         </plugin>  
        </plugins>  
      </build>  
    </project>  

    最终打成一个包toy-inst-1.0-SNAPSHOT.jar

    随便打包个应用

    package org.toy;  
    public class App {  
        public static void main(String[] args) {  
            new App().test();  
        }  
        public void test() {  
            System.out.println("Hello World!!");  
        }  
    }  

    最终打成一个包toy-1.0-SNAPSHOT.jar

    执行命令运行应用

    java -javaagent:target/toy-inst-1.0-SNAPSHOT.jar -cp /home/pwlazy/work/projects/toy/target/toy-1.0-SNAPSHOT.jar org.toy.App   

    java选项中有-javaagent:xx,xx就是你的agent jar,java通过此选项加载agent,由agent来监控classpath下的应用。

    最后的执行结果

    PerfMonAgent.premain() was called.  
    Adding a PerfMonXformer instance to the JVM.  
    Transforming org/toy/App  
    Hello World!!  
    java.io.PrintStream.println:314216  
    org.toy.App.test:540082  
    Transforming java/lang/Shutdown  
    Transforming java/lang/Shutdown$Lock  
    java.lang.Shutdown.runHooks:29124  
    java.lang.Shutdown.sequence:132768  

    我们由执行结果可以看出执行顺序以及通过改变org.toy.App的字节码加入监控代码确实生效了。你也可以发现通过instrment实现agent是的监控代码和应用代码完全隔离了。

    程序启动之后启动代理(agent-main)

    agentmain 需要在 main 函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢?

    在 Java SE 6 文档当中,开发者也许无法在 java.lang.instrument 包相关的文档部分看到明确的介绍,更加无法看到具体的应用 agnetmain 的例子。不过,在 Java SE 6 的新特性里面,有一个不太起眼的地方,揭示了 agentmain 的用法。这就是 Java SE 6 当中提供的 Attach API。

    Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。Attach API只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

    为了简单起见,我们举例简化如下:依然用类文件替换的方式,将一个返回 1 的函数替换成返回 2 的函数,Attach API 写在一个线程里面,用睡眠等待的方式,每隔半秒时间检查一次所有的 Java 虚拟机,当发现有新的虚拟机出现的时候,就调用 attach 函数,随后再按照 Attach API 文档里面所说的方式装载 Jar 文件。等到 5 秒钟的时候,attach 程序自动结束。而在 main 函数里面,程序每隔半秒钟输出一次返回值(显示出返回值从 1 变成 2)。

    public class TestMainInJar { 
        public static void main(String[] args) throws InterruptedException { 
            System.out.println(new TransClass().getNumber()); 
            int count = 0; 
            while (true) { 
                Thread.sleep(500); 
                count++; 
                int number = new TransClass().getNumber(); 
                System.out.println(number); 
                if (3 == number || count >= 10) { 
                    break; 
                } 
            } 
        } 
     }
    
     import java.io.File; 
     import java.io.FileInputStream; 
     import java.io.IOException; 
     import java.io.InputStream; 
     import java.lang.instrument.ClassFileTransformer; 
     import java.lang.instrument.IllegalClassFormatException; 
     import java.security.ProtectionDomain; 
    
     class Transformer implements ClassFileTransformer { 
    
        public static final String classNumberReturns2 = "TransClass.class.2"; 
    
        public static byte[] getBytesFromFile(String fileName) { 
            try { 
                // precondition 
                File file = new File(fileName); 
                InputStream is = new FileInputStream(file); 
                long length = file.length(); 
                byte[] bytes = new byte[(int) length]; 
    
                // Read in the bytes 
                int offset = 0; 
                int numRead = 0; 
                while (offset <bytes.length 
                        && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { 
                    offset += numRead; 
                } 
    
                if (offset < bytes.length) { 
                    throw new IOException("Could not completely read file "
                            + file.getName()); 
                } 
                is.close(); 
                return bytes; 
            } catch (Exception e) { 
                System.out.println("error occurs in _ClassTransformer!"
                        + e.getClass().getName()); 
                return null; 
            } 
        } 
    
        public byte[] transform(ClassLoader l, String className, Class<?> c, 
                ProtectionDomain pd, byte[] b) throws IllegalClassFormatException { 
            if (!className.equals("TransClass")) { 
                return null; 
            } 
            return getBytesFromFile(classNumberReturns2); 
    
        } 
     }
    
     public class TransClass { 
         public int getNumber() { 
         return 1; 
        } 
     }

    含有 agentmain 的 AgentMain 类的代码为:

    import java.lang.instrument.ClassDefinition; 
    import java.lang.instrument.Instrumentation; 
    import java.lang.instrument.UnmodifiableClassException; 
    
     public class AgentMain { 
        public static void agentmain(String agentArgs, Instrumentation inst) 
                throws ClassNotFoundException, UnmodifiableClassException, 
                InterruptedException { 
            inst.addTransformer(new Transformer (), true); 
            inst.retransformClasses(TransClass.class); 
            System.out.println("Agent Main Done"); 
        } 
     }

    其中,retransformClasses 是 Java SE 6 里面的新方法,它跟 redefineClasses 一样,可以批量转换类定义,多用于 agentmain 场合。

    Jar 文件跟 Premain 那个例子里面的 Jar 文件差不多, Jar 文件当中的 Manifest 文件为 :

    Manifest-Version: 1.0 
    Agent-Class: AgentMain

    另外,为了运行 Attach API,我们可以再写一个控制程序来模拟监控过程:(代码片段)

    import com.sun.tools.attach.VirtualMachine; 
     import com.sun.tools.attach.VirtualMachineDescriptor; 
    ……
     // 一个运行 Attach API 的线程子类
     static class AttachThread extends Thread { 
     private final List<VirtualMachineDescriptor> listBefore; 
            private final String jar; 
            AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) { 
                listBefore = vms;  // 记录程序启动时的 VM 集合
                jar = attachJar; 
            } 
            public void run() { 
                VirtualMachine vm = null; 
                List<VirtualMachineDescriptor> listAfter = null; 
                try { 
                    int count = 0; 
                    while (true) { 
                        listAfter = VirtualMachine.list(); 
                        for (VirtualMachineDescriptor vmd : listAfter) { 
                            if (!listBefore.contains(vmd)) { 
                     //如果 VM 有增加,我们就认为是被监控的VM启动了
                     //这时,我们开始监控这个VM 
                                vm = VirtualMachine.attach(vmd); 
                                break; 
                            } 
                        } 
                        Thread.sleep(500); 
                        count++; 
                        if (null != vm || count >= 10) { 
                            break; 
                        } 
                    } 
                    vm.loadAgent(jar); 
                    vm.detach(); 
                } catch (Exception e) { 
                     ignore 
                } 
            } 
        } 
    ……
     public static void main(String[] args) throws InterruptedException {      
         new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start(); 
    
     }

    如果时间掌握得不太差的话,程序首先会在屏幕上打出 1,这是改动前的类的输出,然后会打出一些 2,这个表示 agentmain 已经被 Attach API 成功附着到 JVM 上,代理程序生效了,当然,还可以看到“Agent Main Done”字样的输出。

    以上例子仅仅只是简单示例,简单说明这个特性而已。真实的例子往往比较复杂,而且可能运行在分布式环境的多个 JVM 之中。

  • 相关阅读:
    springboot中添加自定义filter
    一台电脑同时运行多个tomcat配置方法:
    web 后台数据交互的方式
    xml
    开发互联网应用与开发企业级应用有什么异同
    新学期目标
    换了呗项目总结
    换了呗项目
    黄金点游戏
    自我介绍
  • 原文地址:https://www.cnblogs.com/wade-luffy/p/6078301.html
Copyright © 2011-2022 走看看