【JVM】使用 javaagent 和 动态 Attach两种方式实现类的动态修改和增强
一、基本概念介绍
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