一、基本概念介紹
1、Java Instrumentation 包介紹
1)簡單介紹
基於 Instrumentation 來實現的有:
APM 產品: pinpoint、skywalking、newrelic、聽雲的 APM 產品等都基於 Instrumentation 實現
熱部署工具:Intellij idea 的 HotSwap、Jrebel 等
Java 診斷工具:Arthas、Btrace 等
由於對字節碼修改功能的巨大需求,JDK 從 JDK5 版本開始引入了java.lang.instrument
包。它可以通過 addTransformer 方法設置一個 ClassFileTransformer,可以在這個 ClassFileTransformer 實現類的轉換。
JDK 1.5 支持靜態 Instrumentation,基本的思路是在 JVM 啟動的時候添加一個代理(javaagent),每個代理是一個 jar 包,其 MANIFEST.MF 文件里指定了代理類,這個代理類包含一個 premain 方法。JVM 在類加載時候會先執行代理類的 premain 方法,再執行 Java 程序本身的 main 方法,這就是 premain 名字的來源。在 premain 方法中可以對加載前的 class 文件進行修改。這種機制可以認為是虛擬機級別的 AOP,無需對原有應用做任何修改,就可以實現類的動態修改和增強。
從 JDK 1.6 開始支持更加強大的動態 Instrument,在JVM 啟動后通過 Attach API 遠程加載,后面會詳細介紹。
2)Java Instrumentation 核心方法
Instrumentation 是 java.lang.instrument 包下的一個接口,這個接口的方法提供了注冊類文件轉換器、獲取所有已加載的類等功能,允許我們在對已加載和未加載的類進行修改,實現 AOP、性能監控等功能。
常用方法:
/** * 為 Instrumentation 注冊一個類文件轉換器,可以修改讀取類文件字節碼 */ void addTransformer(ClassFileTransformer transformer, boolean canRetransform); /** * 對JVM已經加載的類重新觸發類加載 */ void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; /** * 獲取當前 JVM 加載的所有類對象 */ Class[] getAllLoadedClasses()
它的 addTransformer 給 Instrumentation 注冊一個 transformer,transformer 是 ClassFileTransformer 接口的實例,這個接口就只有一個 transform 方法,調用 addTransformer 設置 transformer 以后,后續JVM 加載所有類之前都會被這個 transform 方法攔截,這個方法接收原類文件的字節數組,返回轉換過的字節數組,在這個方法中可以做任意的類文件改寫。
public class MyClassTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException { // 在這里讀取、轉換類文件 return classBytes; } }
2、Javaagent 介紹
Javaagent 是一個特殊的 jar 包,它並不能單獨啟動的,而必須依附於一個 JVM 進程,可以看作是 JVM 的一個寄生插件,使用 Instrumentation 的 API 用來讀取和改寫當前 JVM 的類文件。
Agent 的兩種使用方式
1.在 JVM 啟動的時候加載,通過 javaagent 啟動參數 java -javaagent:myagent.jar MyMain,這種方式在程序 main 方法執行之前執行 agent 中的 premain 方法 public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception 2.在 JVM 啟動后 Attach,通過 Attach API 進行加載,這種方式會在 agent 加載以后執行 agentmain 方法 public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception 這兩個方法都有兩個參數 第一個 agentArgument 是 agent 的啟動參數,可以在 JVM 啟動命令行中設置,比如java -javaagent:<jarfile>=appId:agent-demo,agentType:singleJar test.jar的情況下 agentArgument 的值為 "appId:agent-demo,agentType:singleJar"。 第二個 instrumentation 是 java.lang.instrument.Instrumentation 的實例,可以通過 addTransformer 方法設置一個 ClassFileTransformer。
第一種 premain 方式的加載時序如下:
Agent 打包方式:
1.為了能夠以 javaagent 的方式運行 premain 和 agentmain 方法,我們需要將其打包成 jar 包,並在其中的 MANIFEST.MF 配置文件中,指定 Premain-class 等信息,一個典型的生成好的 MANIFEST.MF 內容如下
Premain-Class: AgentMain Agent-Class: AgentMain Can-Redefine-Classes: true Can-Retransform-Classes: true
2.可以幫助生成上面 MANIFEST.MF 的 maven 配置
<build> <finalName>my-javaagent</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestEntries> <Agent-Class>AgentMain</Agent-Class> <Premain-Class>AgentMain</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
二、Agent 使用方式一:JVM 啟動參數
創建POM項目JavaAgent,項目結構如下
1)修改pom文件:

<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>JavaAgent</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>7.1</version> </dependency> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-commons</artifactId> <version>7.1</version> </dependency> </dependencies> <build> <finalName>my-trace-agent</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestEntries> <Agent-Class>AgentMain</Agent-Class> <Premain-Class>AgentMain</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <relocations> <relocation> <pattern>org.ow2.asm</pattern> <shadedPattern>me.ya.agent.hidden.org.ow2.asm</shadedPattern> </relocation> <relocation> <pattern>org.objectweb.asm</pattern> <shadedPattern>me.ya.agent.hidden.org.objectweb.asm</shadedPattern> </relocation> </relocations> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>
2)創建AgentMain類,實現在每個函數進入和結束時都打印一行日志,實現調用過程的追蹤的效果

import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.commons.AdviceAdapter; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; import static org.objectweb.asm.Opcodes.ASM7; public class AgentMain { public static class MyMethodVisitor extends AdviceAdapter { protected MyMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(ASM7, mv, access, name, desc); } @Override protected void onMethodEnter() { mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("<<<enter " + this.getName()); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); super.onMethodEnter(); } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(">>>exit " + this.getName()); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } } public static class MyClassVisitor extends ClassVisitor { public MyClassVisitor(ClassVisitor classVisitor) { super(ASM7, classVisitor); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); if (name.equals("<init>")) return mv; return new MyMethodVisitor(mv, access, name, descriptor); } } public static class MyClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException { if (!"MyJavaAgentTest".equals(className)) return bytes; ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); ClassVisitor cv = new MyClassVisitor(cw); cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); return cw.toByteArray(); } } public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { inst.addTransformer(new MyClassFileTransformer(), true); } }
3)定義需要修改的代碼類,MyJavaAgentTest(為了方便我還是放在agent項目中)
public class MyJavaAgentTest { public static void main(String[] args) { new MyJavaAgentTest().foo(); } public void foo() { bar1(); bar2(); } public void bar1() { } public void bar2() { } }
4)打包javaagent項目生成jar文件,並將java文件同MyJavaAgentTest放在同一個目錄下如上圖(放在同一個目錄為了方便執行)
執行如下命令:
java -javaagent:my-trace-agent.jar MyJavaAgentTest
實現了我們的功能,執行結果如下:
➜ java git:(master) ✗ java -javaagent:my-trace-agent.jar MyJavaAgentTest <<<enter main <<<enter foo <<<enter bar1 >>>exit bar1 <<<enter bar2 >>>exit bar2 >>>exit foo >>>exit main
*** 實踐中我有遇到一個問題:就是當MyJavaAgentTest類指定有包名的話就不好使類,如果有解決的請告知
三、Agent 使用方式二:Attach API 使用
新建一個pom項目JavaAttachAgent
1)修改pom文件如下:

<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>JavaAttachAgent</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>7.1</version> </dependency> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-commons</artifactId> <version>7.1</version> </dependency> </dependencies> <build> <finalName>my-attach-agent</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestEntries> <Agent-Class>AgentMain</Agent-Class> <Premain-Class>AgentMain</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <relocations> <relocation> <pattern>org.ow2.asm</pattern> <shadedPattern>me.ya.agent.hidden.org.ow2.asm</shadedPattern> </relocation> <relocation> <pattern>org.objectweb.asm</pattern> <shadedPattern>me.ya.agent.hidden.org.objectweb.asm</shadedPattern> </relocation> </relocations> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>
2)創建AgentMain類,實現修改對應方法的返回值
動態 Attach 的 agent 與通過 JVM 啟動 javaagent 參數指定的 agent jar 包的方式有所不同,動態 Attach 的 agent 會執行 agentmain 方法,而不是 premain 方法。

import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.commons.AdviceAdapter; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; import static org.objectweb.asm.Opcodes.ASM7; /** * Created By Arthur Zhang at 2019/9/4 */ public class AgentMain { public static class MyMethodVisitor extends AdviceAdapter { protected MyMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(ASM7, mv, access, name, desc); } @Override protected void onMethodEnter() { // 在方法開始插入 return 50; mv.visitIntInsn(BIPUSH, 50); mv.visitInsn(IRETURN); } } public static class MyClassVisitor extends ClassVisitor { public MyClassVisitor(ClassVisitor classVisitor) { super(ASM7, classVisitor); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); // 只轉換 foo 方法 if ("foo".equals(name)) { return new MyMethodVisitor(mv, access, name, descriptor); } return mv; } } public static class MyClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException { if (!"MyTestMain".equals(className)) return bytes; ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); ClassVisitor cv = new MyClassVisitor(cw); cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); return cw.toByteArray(); } } public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { System.out.println("agentmain called"); inst.addTransformer(new MyClassFileTransformer(), true); Class classes[] = inst.getAllLoadedClasses(); for (int i = 0; i < classes.length; i++) { if (classes[i].getName().equals("MyTestMain")) { System.out.println("Reloading: " + classes[i].getName()); inst.retransformClasses(classes[i]); break; } } } }
3)創建MyAttachMain類,實現attach到目標進程 (為了方便我還是放在agent項目中)
因為是跨進程通信,Attach 的發起端是一個獨立的 java 程序,這個 java 程序會調用 VirtualMachine.attach 方法開始和目標 JVM 進行跨進程通信。
下面的PID通過jps查看對應的進程ID,如11901
下面的agent.jar全路徑地址為打包好的jar路徑:/Users/zhangboqing/Software/MyGithub/jvm-learn/JavaAttachAgent/src/main/java/my-attach-agent.jar
import com.sun.tools.attach.VirtualMachine; public class MyAttachMain { public static void main(String[] args) throws Exception { // VirtualMachine vm = VirtualMachine.attach(args[0]); VirtualMachine vm = VirtualMachine.attach(PID); try { vm.loadAgent("agent.jar全路徑地址"); } finally { vm.detach(); } } }
4)創建待測試的Java類MyTestMain(為了方便我還是放在agent項目中)
import java.util.concurrent.TimeUnit; public class MyTestMain { public static void main(String[] args) throws InterruptedException { while (true) { System.out.println(foo()); TimeUnit.SECONDS.sleep(3); } } public static int foo() { return 100; // 修改后 return 50; } }
5)運行並驗證
1.運行MyTestMain類main方法(idea中運行就行)
2.通過jps查看該類的進程ID
3.修改MyAttachMain的進程ID
打包JavaAttachAgent將my-attach-agent.jar放入MyAttachMain同一個目錄方便測試並修改MyAttachMain中的jar地址
啟動該類
4.查看MyTestMain的運行結果,驗證成功,如下
100 100 100 agentmain called Reloading: MyTestMain 50 50