JavaAgent是一个JVM插件,它能够利用jvm提供的 Instrumentation API(Java1.5开始提供)实现字节码修改的功能。Agent分为2种:主程序运行前的Agent,主程序之后运行的Agent(Jdk1.6增加)。 JavaAgent常用于 代码热更新,AOP,JVM监控等功能。
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);
}
}
普通项目配置:
Manifest-Version: 1.0
Premain-Class: com.agent.AgentTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true
可以配置的属性:
Premain-Class 指定代理类
Agent-Class 指定代理类
Boot-Class-Path 指定bootstrap类加载器的搜索路径,在平台指定的查找路径失败的时候生效, 可选
Can-Redefine-Classes 是否需要重新定义所有类,默认为false,可选。
Can-Retransform-Classes 是否需要retransform,默认为false,可选
Maven配置:
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>
启动前探针使用方式比较局限,而且每次探针更改的时候,都需要重新启动应用,而主程序之后的探针程序就可以直接连接到已经启动的 jvm 中。可以实现例如动态替换类,查看加载类信息的一些功能。
实现一个指定动态类替换的功能
下面就实现一个指定类,指定class文件动态替换,实现动态日志增加的功能。
- 主程序后的探针程序名称必须为 agentmain
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 方法结束");
}
普通项目配置:
Manifest-Version: 1.0
Agent-Class: com.agent.AgentDynamic
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Maven配置:
<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>
先使用 jps
指令 或者 ps -aux|grep java
找到目标 JVM 线程 ID
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();
}
通过这个方法,我们就可以实现在运行时,对Class文件的动态修改替换
@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
这个是热加载的效果
UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)
类似的异常重新定义class 自身提供的Class字节码替换掉已存在的Class 应用于线上debug的时候比较方便
修改class 在已存在的Class字节码上修改后再进行替换,类似于对Class进行包装。 应用于通用aop服务的时候比较方便
可以直接采用指定文件进行读取,然后直接进行替换 一般实现的方式是下面这种方式:
byte[] classBytes = Files.readAllBytes(Paths.get(classFile));
ClassDefinition classDefinition = new ClassDefinition(redefineClass, classBytes);
inst.redefineClasses(classDefinition); 作者:我是小河神 https://www.bilibili.com/read/cv16232206 出处:bilibili
retransformClasses 的使用需要 Transformer 类的配合,使用 Transformer 的包装对 Class 进行包装,然后替换
对类进行包装的转换类
public interface ClassFileTransformer {
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
一般的实现方式是在transform方法中,一般是使用ASM,javassist之类的字节码操纵技术对字节码进行包装。
@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();
}