在工作中有使用 sandbox 修改类中的方法体、返回等,现通过 demo 编码学习,了解其底层实现的原理。
agent 在 Java 生态中使用广泛,例如 arthashttps://arthas.aliyun.com/doc/
、trace 记录等框架等,都是通过 agent 实现。
Java 中的 agent 分为两类:
premain agent: 在 Jvm 启动前,将 agent 随 jvm 一起启动生效。 attach agent:在 Jvm 启动后,通过将 agent attach 到指定 pid 的 jvm 上生效。
不论是修改代码逻辑、修改函数返回,本质上来说都修改是加载类的字节码文件,而这两种 agent 分类均是通过 jvm 提供的扩展点在类加载前增加逻辑来修改字节码,达到修改实际生效的字节码文件的效果。
区别在于:
premain agent 由于是在 jvm 启动之前生效的,所以可以在类的初次加载前完成扩展逻辑,在类的初次加载时对类的字节码文件进行修改。
而 attach agent 生效时,类都已经加载完毕,所以此时需要触发想要修改的类 重新进行加载,进而修改其字节码文件。
Premain agent
premain,在 main 函数之前启动的意思。
通常是通过 jvm 启动参数指定 agent。
例如:
-javaagent:/Users/xxx/IdeaProjects/AgentDemo/target/AgentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar
执行入口
我们知道,当我们将 Java 代码打包成 jar 包时,需要指定入口 main 方法,main 方法即为整体的触发点。
Premain agent 也类似,需要指定一个 premain agent 的入口方法,不论是通过何种打包方式,最后在 jar 包的 MANIFEST.INF 文件中,通过:
Premain-Class: org.example.PreMainAgent
来指定该 premain agent 的入口类,同时默认调用以下两个方法:
//优先级更高
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
也就是说,当工程中存在一个这样的类,打包成存在 premain 入口的 jar 包。
即可通过测试的 java 程序中加上对应 jvm 启动参数,实现 premain agent 随 jvm 启动。
package org.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
public class PreMainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("PreMainAgent premain enter.args=" + agentArgs);
}
}
如何修改字节码文件
当进入触发入口的之后,通过入参中的 Instrumentation 来对字节码进行修改。
Instrumentation 为 jvm 提供的 允许开发者在 Java 程序运行时检查和修改应用程序的行为的工具类,在 agent 中主要是使用其对类的字节码文件进行修改。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
Instrumentation 中存在 Transformer 的概念,transformer 是在类加载前可以对类进行修改、检查的扩展点。
当我们调用inst.addTransformer(new DemoTransFormer(), true);
添加了一个 transformer 后,该 transformer 就会在类加载前生效。
例如下面这个 transformer,即可打印出所有加载类的名字。
static class DemoTransFormer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("PreMainAgent start transform. className=" + className);
return null;
}
}
而其返回的是一个byte[]
数组,即类加载后的字节码文件,也就是,如果想要对指定的类进行字节码文件修改,只需要在这修改,然后将修改后的字节码文件返回即可。
下面为示例,该transformer
对org/example/simple/Hello
进行 transform,在这个类的 sayHello 方法的最后,插入了一个打印 hello world 的语句。然后将修改后的字节码文件返回。举例使用的是修改字节码文件使用的 Javaassist 库。
static class DemoTransFormer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("PreMainAgent start transform. className=" + className);
if ("org/example/simple/Hello".equals(className)) {
try {
// 从ClassPool获得CtClass对象
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("org.example.simple.Hello");
CtMethod sayHello = clazz.getDeclaredMethod("sayHello");
String methodBody = "System.out.println("hello world!");";
sayHello.insertAfter(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (Throwable ex) {
System.out.println(ex.getClass().getCanonicalName());
ex.printStackTrace();
}
}
return null;
}
}
最后完整的 premain agent 类如下:
package org.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
public class PreMainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("PreMainAgent premain enter.args=" + agentArgs);
inst.addTransformer(new DemoTransFormer(), true);
}
static class DemoTransFormer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.startsWith("java") || className.startsWith("sun")
|| className.startsWith("com/intellij") || className.startsWith("jdk")) {
return null;
}
System.out.println("PreMainAgent start transform. className=" + className);
if ("org/example/simple/Hello".equals(className)) {
try {
// 从ClassPool获得CtClass对象
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("org.example.simple.Hello");
CtMethod sayHello = clazz.getDeclaredMethod("sayHello");
String methodBody = "System.out.println("hello world!");";
sayHello.insertAfter(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (Throwable ex) {
System.out.println(ex.getClass().getCanonicalName());
ex.printStackTrace();
}
}
return null;
}
}
}
测试使用的 java 代码如下:
package org.example;
import org.example.simple.Hello;
/**
* Hello world!
*/
public class App
{
public static void main( String[] args ) throws InterruptedException {
System.out.println( "entry main." );
while (true) {
Hello.sayHello();
Thread.sleep(1000);
}
}
}
package org.example.simple;
public class Hello {
public static void sayHello(){
System.out.println("hello");
}
}
启动输出如下:
PreMainAgent premain enter.args=null
//先加载main方法所在的类
PreMainAgent start transform. className=org/example/App
//执行main方法
entry main.
//加载Hello类 触发transform
PreMainAgent start transform. className=org/example/simple/Hello
//transform后,增加打印hello world的语句,并循环执行
hello
hello world!
hello
hello world!
hello
hello world!
premain agent 的流程图大致如下:
Attach agent
attach agent 是在 jvm 已经启动之后,再附着到指定的 java 程序中,他需要解决和 premain agent 相同的两个问题:
执行入口 如何修改字节码文件
执行入口
premain agent 随着用户本身的 jvm 启动,因此从用户的视角来看的话,只启动一个有特殊 jvm 参数的程序。
而 attach agent 不同,本身已经有一个 java 程序了,然后需要再启动一个 java 程序,后启动的 java 程序需要连接到之前的 java 程序中,将 agent 的 jar 包发送过去并 load,从而完成 attach。
整个过程的流程图如下:
其中 attach 的入口与 premain 不同,为
//优先级更高
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
package org.example;
import javassist.NotFoundException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AttachAgent {
public static void agentmain(String agentArgs, Instrumentation instrumentation) throws NotFoundException, ClassNotFoundException, UnmodifiableClassException, InterruptedException {
System.out.println("entry AttachAgent main.");
}
}
同样需要在 jar 包的 MANIFEST.INF 文件中,通过:
Agent-Class: org.example.AttachAgent
指定 attach 的 jar 包的 agent 入口即可。
连接并load attach agent的代码
package org.example;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
import java.util.Scanner;
public class AttachAgentMain {
public static void main(String[] args) {
//获取当前系统中所有 运行中的 虚拟机
System.out.println("Attach test agent start.");
System.out.println("Please select the jvm to attch.");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
System.out.println("------------------------------------------");
for (int i = 0; i < list.size(); i++) {
VirtualMachineDescriptor vmd = list.get(i);
System.out.println(i + " : " + vmd.displayName());
}
System.out.println("------------------------------------------");
try {
Scanner scanner = new Scanner(System.in);
//选择并连接指定的虚拟机
int vmSelect = scanner.nextInt();
VirtualMachineDescriptor vmd = list.get(vmSelect);
System.out.println("vmd = "+vmd.displayName());
System.out.println("vmd pid = "+vmd.id());
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
System.out.println("virtualMachine ="+virtualMachine.toString());
//load attach agent
virtualMachine.loadAgent("/Users/xxx/IdeaProjects/AgentDemo/target/AgentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar");
//结束
virtualMachine.detach();
}catch (Throwable e){
e.printStackTrace();
}
}
}
如何修改字节码
修改字节码的方式和 premain agent 相同,都是通过 transformer 生效来修改。
但是 transformer 只会在加载类的时候生效,而 attach agent 生效时,jvm 都已经运行了,此时类应该都已经加载完了。
所以此时需要比 premain agent 多一个重新 transformer 的操作,会重新拉取类本身的定义,然后按照现在定义的的 transformer 重新 transform 一遍,这样才可以做到在运行起来之后仍然修改类的字节码文件。
示例如下:
package org.example;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class AttachAgent {
public static void agentmain(String agentArgs, Instrumentation instrumentation) throws NotFoundException, ClassNotFoundException, UnmodifiableClassException, InterruptedException {
DemoTransFormer demoTransFormer = new DemoTransFormer();
instrumentation.addTransformer( demoTransFormer, true);
Class<?> claz = Class.forName("org.example.simple.Hello");
try {
System.out.println("start to retransform claz="+claz.getName());
//重新transform指定的类
instrumentation.retransformClasses(claz);
}catch (Throwable e){
e.printStackTrace();
}
}
static class DemoTransFormer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("AttachAgent start transform. className=" + className);
if ("org/example/simple/Hello".equals(className)) {
try {
// 从ClassPool获得CtClass对象
final ClassPool classPool = ClassPool.getDefault();
System.out.println("classPool="+classPool);
final CtClass clazz = classPool.get("org.example.simple.Hello");
System.out.println("clazz="+clazz);
CtMethod sayHello = clazz.getDeclaredMethod("sayHello");
String methodBody = "System.out.println("hello world after attach agent transform!");";
sayHello.insertAfter(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (Throwable ex) {
System.out.println(ex.getClass().getCanonicalName());
ex.printStackTrace();
}
}
return null;
}
}
}
打包agent范例
使用 maven-assembly-plugin 打包插件。
值得注意的是:打包的时候最好把依赖一起打包成一个 jar 包,否则在启动 agent 执行逻辑后会报java.lang.NoClassDefFoundError
。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-5</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!--premain agent 入口设置 -->
<Premain-Class>org.example.PreMainAgent</Premain-Class>
<!--attach agent 入口设置 -->
<Agent-Class>org.example.AttachAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>assembly</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
参考文章:
https://www.cnblogs.com/rickiyang/p/11368932.html
https://lotabout.me/2024/Java-Agent-101/
https://www.cnblogs.com/qisi/p/java_agent.html
手把手教你实现一个Java Agent
如喜欢本文,请点击右上角,把文章分享到朋友圈
如有想了解学习的技术点,请留言给若飞安排分享因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享
·END·
相关阅读:
一张图看懂微服务架构路线 基于Spring Cloud的微服务架构分析 微服务等于Spring Cloud?了解微服务架构和框架 如何构建基于 DDD 领域驱动的微服务? 微服务架构实施原理详解 微服务的简介和技术栈 微服务场景下的数据一致性解决方案 设计一个容错的微服务架构 作者:AboutWhat
来源:juejin.cn/post/7437917801326166055
版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
架构师 我们都是架构师!
关注架构师(JiaGouX),添加“星标”
获取每天技术干货,一起成为牛逼架构师
技术群请加若飞:1321113940 进架构师群
投稿、合作、版权等邮箱:admin@137x.com