Java Agent字节码插桩术
关于JVMTI(jvm tool interface)
JVMTI是⽤来开发和监控JVM所使⽤的程序接⼝,可以探查JVM内部状态,并控制JVM应⽤程序的执⾏,JVMTI的客户端,或称为代理(agent),提供了很多函数,可以监听感兴趣的事件,以便来查询或控制应⽤程序,进而实现控制JVM应用程序目标。但是需要注意的是,并⾮所有的JVM实现都⽀持JVMTI。
Idea的Debug功能就是通过JDWP协议定制化JVMTI的功能实现与JVM的双向通信连接,在获取断点位置的代码信息同时向IDEA客户端进行推送信息,实现监听功能!
如果要自己实现定制化的JVMTI功能的话,需要编写dll文件 更多资料参考 。
javaagent是java1.5之后引⼊的特性,基于JVMTI实现,⽀持JVMTI部分功能。主要应⽤场景是对类加载进⾏拦截修改和对已加载的类进⾏重定义。此外还⽀持获取已加载的类,以及实例内存占⽤计算等。
javaagent的启动方式
javaagent的表现形式是监听一个JAR包,用于监听目标应用,它有两种启动方式:
- 加载时启动:agent的功能随着⽬标应⽤⼀起启动,通过设置⽬标应⽤的jvm参数:"-javaagent:",即可加载javaagent监听目标jar包。
- 这种方式是在类加载之前进行拦截的,在符合JVM的规范下可以任意类的结构。比如修改类的名称,加入方法等等
- 运行时附着:借助jvmtools⼯具包将javaagent包注⼊到⽬标应⽤中,实现运行过程的监听。
- 类加载之后进行拦截,可以对方法内部增加逻辑,不能增加方法修改类的结构。
代码演示
加载时启动(premain)
与运行时JAR包的启动区别
运⾏时JAR包 | javaagentjar包 | |
启动类 | Main-class | Premain-class |
启动⽅法 | main | premain |
启动⽅式 | java-jarxxx.jar | jvm参数设置:-javaagent:xxx.jar |
编写启动类(Premain)
public class MyAgent {
/**
*
* @param arg 启动参数
* @param instrumentation 类的加载和定义都是通过它来实现的
*/
// 加载时启动
public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("hello premain");
}
}
maven插件设置然后进行编译打包
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Project-name>${project.name}</Project-name>
<Project-version>${project.version}</Project-version>
<!--设置启动类的位置-->
<Premain-Class>javaagent.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>
目标类
public class MyApp {
public static void main(String[] args) throws IOException {
System.out.println("访问主程序");
}
}
将打包好的javaagent的jar包位置作为参数设置到JVM参数中
可以看到在类加载之前就已经被拦截了
附着启动(agentmain)
如果想要在应⽤运⾏之后去监听它,⽽⼜不去重启它,就可以采⽤另⼀种⽅式附着启动。其相关属性通过以表来⽐对:
运⾏时JAR包 | javaagentjar包 | |
启动类 | Main-class | Agent-class |
启动⽅法 | main | agentmain |
启动⽅式 | java-jarxxx.jar | tools⼯具附着 |
编写启动类
public class MyAgent { /** * * @param arg 启动参数 * @param instrumentation 类的加载和定义都是通过它来实现的 */ // 运行时启动 public static void agentmain(String arg, Instrumentation instrumentation) { System.out.println("hello agentmain"); } }
maven插件设置然后进行编译打包
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.2</version> <configuration> <archive> <manifestEntries> <Project-name>${project.name}</Project-name> <Project-version>${project.version}</Project-version> <!--设置启动类的位置--> <Agent-Class>javaagent.MyAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> <skip>true</skip> </configuration> </plugin>
目标类
public class MyApp { public static void main(String[] args) throws IOException { System.out.println("访问主程序"); // 设置运行时 System.in.read(); } }
前置设置和加载时启动没有什么区别,运行时附着的启动方式是必须通过jvm/lib/tools.jar中的API注⼊⾄⽬标应⽤:
导入依赖
<dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8.0</version> <scope>system</scope> <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath> </dependency>
public class AttachStart { public static void main(String[] args) throws Exception { // 获取jvm进程列表 借用tool工具实现进程交互 List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (int i = 0; i < list.size(); i++) { System.out.println(String.format("[%s] %s", i, list.get(i).displayName())); } System.out.println("输入数字指定要attach的进程"); // 选择jvm进程 BufferedReader read = new BufferedReader(new InputStreamReader(System.in)); String line = read.readLine(); int i = Integer.parseInt(line); // 附着agent VirtualMachine virtualMachine = VirtualMachine.attach(list.get(i)); // 将对应的agent功能附着到指定进程进去实现运行时启动 virtualMachine.loadAgent("E:/javaCode/javaagent/target/javaagent-1.0-SNAPSHOT.jar", "111"); virtualMachine.detach(); System.out.println("加载成功"); } }
选择要附着的jvm进程,需要注意的是一旦附着成功,agent的功能实现是在对方的进程上实现的。
结果很明显,agent的功能是在目标类之后才显现出来
核心功能
addTransformer(变压器): 添加类加载拦截器,可重定义加载的类,类如果在这之前加载会失效。
retransformClasses(类重新变更):将类进行二次加载,重新被变压器所拦截,注意加载的类是有限制的,仅可对运⾏指令码进⾏修改:不可修改类结构如继承、接⼝、类符、变更属性、变更⽅法等。可以新增privatestatic/final的方法;必须maven插件添加Can-Retransform-Classes=true该⽅法执⾏才有效,且addTransformer⽅法的canRetransform参数也为true。
redefineClasses(重新定义类):在经过变压器之前进行重定义,逻辑上跟类重新变更没太大的区别
getAllLoadedClasses(获取所有类)
getInitiatedClasses(获取已实例化的类)
getObjectSize(计算对象大小)
示例
public class MyApp { public static void main(String[] args) throws IOException { System.out.println("访问主程序"); new HelloWorld().hello(); } } public class HelloWorld { public void hello() { System.out.println("hello!!"); } }
// 加载时启动 public static void premain(String arg, Instrumentation instrumentation) throws UnmodifiableClassException, ClassNotFoundException { System.out.println("hello premain"); // 如果类在变压器之前加载那么就不能修改类(运行时加载不能用) HelloWorld helloWorld = new HelloWorld(); instrumentation.addTransformer(new ClassFileTransformer() { /** * @param loader * @param className 动态加载的类名 * @param classBeingRedefined 这个类重新加载之前的类 * @param protectionDomain 类的基本信息 * @param classfileBuffer 这个类的字节码,如果返回null就按照原有的进行加载覆盖 * @return 返回指令码 * @throws IllegalClassFormatException */ @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (!"javaagent/HelloWorld".equals(className)) { return null; } // 配合javassist修改指令码 ClassPool pool = new ClassPool(); pool.appendSystemPath(); try { CtClass ctClass = pool.get("javaagent.HelloWorld"); CtMethod method = ctClass.getDeclaredMethod("hello"); method.insertBefore("System.out.println(\"插入前置逻辑\");"); return ctClass.toBytecode(); } catch (NotFoundException | CannotCompileException | IOException e) { e.printStackTrace(); } return null; } },true); // 重新触发指定类的加载但是对字节码的修改有一定的限制,只是重走变压器的逻辑 instrumentation.retransformClasses(HelloWorld.class); // 这里的访问的方法内部逻辑已经被修改了 // 原理有点像是spring热部署:(单例)类只加载一次它所指向的方法地址不变,但是方法自身指令码发生了改变(钥匙还是原来的钥匙但是房间里的东西可能就不一样了!)! helloWorld.hello(); }
结果
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报