JAVA探针技术
JavaAgent是一个JVM插件,它能够利用jvm提供的 Instrumentation API(Java1.5开始提供)实现字节码修改的功能。Agent分为2种:主程序运行前的Agent,主程序之后运行的Agent(Jdk1.6增加)。 JavaAgent常用于 代码热更新,AOP,JVM监控等功能。
1 主程序运行前的Agent
1.1 探针程序
- 名称必须为:premain
- 参数可以是:premain(String agentOps, Instrumentation inst) 也可以是:premain(String agentOps)
- 优先执行premain(String agentOps)
public class AgentTest {
该方法在main方法之前运行,与main方法运行在同一个JVM中
并被同一个System ClassLoader装载
被统一的安全策略(security policy)和上下文(context)管理
public static void premain(String agentOps, Instrumentation inst){
System.out.println("====premain 方法执行");
System.out.println("参数为:" agentOps);
}
如果不存在 premain(String agentOps, Instrumentation inst)
则会执行 premain(String agentOps)
public static void premain(String agentOps){
System.out.println("====premain方法执行2====");
System.out.println(agentOps);
}
}
1.2 在MANIFEST.MF配置环境参数
普通项目配置:
代码语言:javascript复制Manifest-Version: 1.0
Premain-Class: com.agent.AgentTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true
可以配置的属性:
代码语言:javascript复制Premain-Class 指定代理类
Agent-Class 指定代理类
Boot-Class-Path 指定bootstrap类加载器的搜索路径,在平台指定的查找路径失败的时候生效, 可选
Can-Redefine-Classes 是否需要重新定义所有类,默认为false,可选。
Can-Retransform-Classes 是否需要retransform,默认为false,可选
Maven配置:
代码语言:javascript复制manifestEntries里面的元素就与上面的配置对应
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>
com.agent.AgentTest
</Premain-Class>
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
<Manifest-Version>
true
</Manifest-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
1.3 使用探针
- 打包探针项目 为 preAgent.jar
- 启动主函数的时候添加jvm参数
- params 就对应 premain 函数中 agentOps 参数
2 主程序之后运行的Agent
启动前探针使用方式比较局限,而且每次探针更改的时候,都需要重新启动应用,而主程序之后的探针程序就可以直接连接到已经启动的 jvm 中。可以实现例如动态替换类,查看加载类信息的一些功能。
实现一个指定动态类替换的功能
下面就实现一个指定类,指定class文件动态替换,实现动态日志增加的功能。
探针程序
- 主程序后的探针程序名称必须为 agentmain
- 通过 agentOps 参数将需要替换的类名和 Class 类文件路径传递进来
- 然后获取全部加载的 Class 去,通过类名筛选出来要替换的 Class
- 通过传递进行的 Class 类文件路径加载数据
- 通过 redefineClasses 进行类文件的热替换
- 使用 redefineClasses 函数必须将 Can-Redefine-Classes 环节变量设置为 true
public static void agentmain(String agentOps, Instrumentation inst) {
System.out.println("====agentmain 方法开始");
String[] split = agentOps.split(",");
String className = split[0];
String classFile = split[1];
System.out.println("替换类为: " className);
Class<?> redefineClass = null;
Class<?>[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class<?> clazz : allLoadedClasses) {
if (className.equals(clazz.getCanonicalName())){
redefineClass = clazz;
}
}
if (redefineClass==null){
return;
}
//热替换
try {
byte[] classBytes = Files.readAllBytes(Paths.get(classFile));
ClassDefinition classDefinition = new ClassDefinition(redefineClass, classBytes);
inst.redefineClasses(classDefinition);
} catch (ClassNotFoundException | UnmodifiableClassException | IOException e) {
e.printStackTrace();
}
System.out.println("====agentmain 方法结束");
}
2.2 在MANIFEST.MF配置环境参数
普通项目配置:
代码语言:javascript复制Manifest-Version: 1.0
Agent-Class: com.agent.AgentDynamic
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Maven配置:
代码语言:javascript复制<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>
com.agent.AgentDynamic
</Agent-Class>
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
<Manifest-Version>
true
</Manifest-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
2.3 使用探针
先使用 jps
指令 或者 ps -aux|grep java
找到目标 JVM 线程 ID
- 编写使用探针程序
- 将目标线程attach到VirtualMachine
- 配置参数agentOps ,加载探针,此时就会执行探针中的程序
- 通过VirtualMachine还能获取到对应JVM的系统参数,以及探针的一些参数
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine target = VirtualMachine.attach("96003");//目标VM线程ID
String agentOps = "com.api.rcode.controller.HomeController,/Users/hailingliang/app/workspace/jave-code-note/server-api/target/classes/com/api/rcode/controller/HomeController.class";
target.loadAgent("/Users/hailingliang/app/workspace/jave-code-note/java-learn/agent/target/agent-1.0-SNAPSHOT.jar",
agentOps);
Properties agentProperties = target.getAgentProperties();
System.out.println(agentProperties);
Properties systemProperties = target.getSystemProperties();
System.out.println(systemProperties);
target.detach();
}
2.4 运行结果
通过这个方法,我们就可以实现在运行时,对Class文件的动态修改替换
代码语言:javascript复制@RestController("/home")
public class HomeController {
@RequestMapping("/h2")
public String h2(boolean p1, int p2){
System.out.println(p1 " " p2);
System.out.println("这个是热加载的效果");
return "h2 p1: " p1 " p2:" p2;
}
}
====agentmain 方法开始
参数为: com.api.rcode.controller.HomeController,/Users/hailingliang/app/workspace/jave-code-note/server-api/target/classes/com/api/rcode/controller/HomeController.class
objectSize 1040
====agentmain 方法完成
true 122112
这个是额外加的一段话12312312312132123
====agentmain 方法开始
参数为: com.api.rcode.controller.HomeController,/Users/hailingliang/app/workspace/jave-code-note/server-api/target/classes/com/api/rcode/controller/HomeController.class
objectSize 1040
====agentmain 方法完成
true 122112
这个是热加载的效果
2 探针修改Class的限制
2.1 主程序运行前Agent
- 除了名称以外,可以更改任意内容,名称改了,ClassLoad 就会出问题
2.2 主程序运行中Agent
- 不能修改Class的文件结构,即不能添加方法,不能添加字段,只能修改方法体的内容,否则就会报
UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)
类似的异常
2.3 两种修改类的方式
2.3.1 redefineClasses:
重新定义class 自身提供的Class字节码替换掉已存在的Class 应用于线上debug的时候比较方便
2.3.2 retransformClasses:
修改class 在已存在的Class字节码上修改后再进行替换,类似于对Class进行包装。 应用于通用aop服务的时候比较方便
2.3.3 redefineClasses
可以直接采用指定文件进行读取,然后直接进行替换 一般实现的方式是下面这种方式:
代码语言:javascript复制byte[] classBytes = Files.readAllBytes(Paths.get(classFile));
ClassDefinition classDefinition = new ClassDefinition(redefineClass, classBytes);
inst.redefineClasses(classDefinition); 作者:我是小河神 https://www.bilibili.com/read/cv16232206 出处:bilibili
2.3.4 retransformClasses
retransformClasses 的使用需要 Transformer 类的配合,使用 Transformer 的包装对 Class 进行包装,然后替换
2.3.5 ClassFileTransformer
对类进行包装的转换类
2.3.6 接口定义
代码语言:javascript复制public interface ClassFileTransformer {
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
2.3.7 实现方式
一般的实现方式是在transform方法中,一般是使用ASM,javassist之类的字节码操纵技术对字节码进行包装。
代码语言:javascript复制@Override
public byte[] transform(ClassLoader loader,
//要转换的类的定义加载程序,
String className,
//Java虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。
// 例如,“java/util/List”。
Class<?> classBeingRedefined,
//如果这是由重定义或重传触发的,则被重定义或重传的类;如果这是类加载,则为null
ProtectionDomain protectionDomain,
//正在定义或重新定义的类的保护域
byte[] classfileBuffer
//类格式的输入字节缓冲区——不得修改
) throws IllegalClassFormatException {
ClassReader classReader = new ClassReader(classfileBuffer);
PreClassVisitor preClassVisitor = new PreClassVisitor( new ClassWriter(ClassWriter.COMPUTE_MAXS));
classReader.accept(preClassVisitor,0);
return preClassVisitor.toByteArray();
}