Java Instrument实战

github.com/davyjoneswa… 欢迎指导。

简介

Instrumentation是Java5提供的新特性。使用Instrumentation,开发者可以构建一个代理,用来监测运行在JVM上的程序。

通过Instrument,我们可以在JVM执行某个类文件之前,对该类文件的字节码进行适当的修改来达到我们的目的。

使用指南

java.lang.instrument中需要关注的是ClassFileTransformer和Instrumentation接口。

  • ClassFileTransformer接口。这个接口提供了一个transform方法。我们需要的处理逻辑都是实现在里面。

     byte[] transform(ClassLoader loader,
               String className,
               Class classBeingRedefined,
               ProtectionDomain protectionDomain,
               byte[] classfileBuffer)
               throws IllegalClassFormatException
    复制代码

    如果transform方法返回null, 表示我们不对类进行处理直接返回。否则,会用我们返回的byte[]来代替原来的类。

  • Instrumentation接口。ClassFileTransformer必须添加进Instrumentation才能生效。

      Instrumentation inst;
      ClassFileTransformer classFileTransformer;
      inst.addTransformer(classFileTransformer);
    复制代码

要完成一个Instrument,基本步骤如下:

  1. 定义一个代理类并添加premain(也就是在main执行前执行)方法。代理类可以是任何一个普通的Java类。

     public static void premain(String args, Instrumentation inst)
    复制代码
  2. 定义一个实现ClassFileTransformer接口的转换类(通常由代理带实现即可)

  3. 将第二步的转换类实例添加进Instrumentation里。

     inst.addTransformer(ClassFileTransformer);
    复制代码

实战

接下来,我们实现一个Instrument实例,在这个实例中,我们会在类的每一个方法里,插入一段代码,来统计方法代码的执行时长。

  1. 添加代理类,并添加premain方法。同时我们让代理类实现ClassFileTransformer接口。

    package org.greenleaf;
    
    public class ApmAgent implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className,
                        Class classBeingRedefined, ProtectionDomain protectionDomain,
                        byte[] classfileBuffer) throws IllegalClassFormatException {
        }
     
        public static void premain(String agentArgs, Instrumentation inst) {
            inst.addTransformer(new ApmAgent());
        }
    }
    复制代码
  2. 添加代码插入逻辑
    我们需要借助字节码插入工具来完成我们的代码插入。最常用的字节码操作工具有javassist和ASM。由于javassist使用相对简单,在这里,我们使用javassist。关于javassist,读者可自行学习。

    ClassPool pool = new ClassPool(true);
    pool.appendClassPath(new LoaderClassPath(loader));
    try {
        CtClass cls = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
        
        CtMethod[] methods = cls.getDeclaredMethods();
        for (CtMethod method : methods) {
            //插入本地变量
            method.addLocalVariable("startTime", CtClass.longType);
            String codeStrBefore = "startTime=System.currentTimeMillis();";
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("System.out.println(")
                    .append("\"" + method.getName() + " time cost \"").append(" +     (System.currentTimeMillis() - startTime) + \"毫秒\");");
        
            String codeStrAfter = stringBuilder.toString();
            System.out.println(codeStrBefore);
            System.out.println(codeStrAfter);
            method.insertBefore(codeStrBefore);
            method.insertAfter(codeStrAfter);
        }
    
        File file = new File("./target/", cls.getSimpleName() + ".class");
        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
            fileOutputStream.write(cls.toBytecode());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cls.toBytecode();
    } catch (Exception e) {
        logger.error("", e);
    }
    return null;
    复制代码
  3. 将ApmAgent编译并打包成jar.
    需要在jar的META-INFO的MANIFEST.MF里添加如下信息。

     Premain-Class: org.greenleaf.ApmAgent
     Agent-Class: org.greenleaf.ApmAgent
     Can-Redefine-Classes: true
     Can-Retransform-Classes: true
    复制代码

    我们使用的是maven构建的jar. 读者也可以使用自己擅长的方式构建。使用maven在pom的配置信息如下:

     
     agent
     
         
             org.apache.maven.plugins
             maven-jar-plugin
             
                 
                     
                         true
                     
                     
                         org.greenleaf.ApmAgent
                         org.greenleaf.ApmAgent
                         true
                         true
                     
                 
             
         
     
     
    复制代码

    执行

         mvn clean package
    复制代码

    这样既可以看到我们生产的jar了。

  4. 使用代理

    1. 首先,新建一个测试类,

       public class App {
           public static void main(String[] args) {
               System.out.println("Hello World");
           }
      
           private int testMethod(int a ,int b) {
               return a + b;
           }
       }
      复制代码
    2. 然后,编译并运行生成的class。需要在java命令添加-javaagent:agent.jar参数. 我们使用exec-maven-plugin插件来运行java类。

       
          
              
              org.codehaus.mojo
              exec-maven-plugin
              1.2.1
      
              
                  java
                  
                      -javaagent:${project.parent.basedir}/agentlib/target/agent.jar="传递的参数"
                      -classpath
                      
                      org.greenleaf.sample.App 
                  
              
      
              
          
      
      复制代码

    解释一下:

    -javaagent:${project.parent.basedir}/agentlib/target/agent.jar="传递的参数" 
    • -javaagent:${project.parent.basedir}/agentlib/target/agent.jar 代表的是使用-javaagent参数。
    • 传递的参数:premain方法有个String agentArgs参数。传递的参数就是传递的参数,如果不需要,可以不传。

    运行结果:由于我们保存了修改后的的类,我们可以在target下看到App.class 如下:

    package org.greenleaf.sample;
    
    public class App {
        public App() {
            try {
                System.out.println("This code is inserted before constructor org/greenleaf/sample/App");
            } finally {
                Object var2 = null;
                System.out.println("This code is inserted after constructor org/greenleaf/sample/App");
            }
            
        }
    
        public static void main(String[] args) {
            long startTime = System.currentTimeMillis();
            System.out.println("Hello World");
            Object var4 = null;
            System.out.println("main time cost " + (System.currentTimeMillis() - startTime) + "毫秒");
        }
    
        private int testMethod(int a, int b) {
            long startTime = System.currentTimeMillis();
            int var6 = a + b;
            System.out.println("testMethod time cost " + (System.currentTimeMillis() - startTime) + "毫秒");
            return var6;
        }
    }
    复制代码

    可以看到,每一个方法,都被成功插入了我们期待的代码。

    更详细请参考: github.com/davyjoneswa… 欢迎指导。

你可能感兴趣的:(Java Instrument实战)