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的配置信息如下:

     <build>
     <finalName>agent</finalName>
     <plugins>
         <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-jar-plugin</artifactId>
             <configuration>
                 <archive>
                     <manifest>
                         <addClasspath>true</addClasspath>
                     </manifest>
                     <manifestEntries>
                         <Premain-Class>org.greenleaf.ApmAgent</Premain-Class>
                         <Agent-Class>org.greenleaf.ApmAgent</Agent-Class>
                         <Can-Retransform-Classes>true</Can-Retransform-Classes>
                         <Can-Redefine-Classes>true</Can-Redefine-Classes>
                     </manifestEntries>
                 </archive>
             </configuration>
         </plugin>
     </plugins>
     </build>
    复制代码

    执行

         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类。

       <build>
          <plugins>
              <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>exec-maven-plugin</artifactId>
              <version>1.2.1</version>
      
              <configuration>
                  <executable>java</executable>
                  <arguments>
                      <argument>-javaagent:${project.parent.basedir}/agentlib/target/agent.jar="传递的参数"</argument>
                      <argument>-classpath</argument>
                      <classpath/>
                      <mainClass>org.greenleaf.sample.App</mainClass> <!-- 程序入口,主类名称 -->
                  </arguments>
              </configuration>
      
              </plugin>
          </plugins>
      </build>
      复制代码

    解释一下:

    -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… 欢迎指导。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值