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,基本步骤如下:
-
定义一个代理类并添加premain(也就是在main执行前执行)方法。代理类可以是任何一个普通的Java类。
public static void premain(String args, Instrumentation inst) 复制代码
-
定义一个实现ClassFileTransformer接口的转换类(通常由代理带实现即可)
-
将第二步的转换类实例添加进Instrumentation里。
inst.addTransformer(ClassFileTransformer); 复制代码
实战
接下来,我们实现一个Instrument实例,在这个实例中,我们会在类的每一个方法里,插入一段代码,来统计方法代码的执行时长。
-
添加代理类,并添加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()); } } 复制代码
-
添加代码插入逻辑
我们需要借助字节码插入工具来完成我们的代码插入。最常用的字节码操作工具有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; 复制代码
-
将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了。
-
使用代理
-
首先,新建一个测试类,
public class App { public static void main(String[] args) { System.out.println("Hello World"); } private int testMethod(int a ,int b) { return a + b; } } 复制代码
-
然后,编译并运行生成的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… 欢迎指导。
-