Java Agent是一种特殊的Java程序,它可以在JVM启动时或运行时动态加载,用于监控和修改其他Java应用程序的行为。通过Java Agent,开发者可以在不修改目标应用程序源码的情况下,动态地插入功能,如性能分析、日志记录、代码覆盖率测试、热更新等。
一、Java Agent的主要功能
1、监控类的加载:在类加载到JVM时,可以对类进行操作,例如记录日志、统计加载时间。2、修改类的字节码:在类被加载时,可以修改其字节码,例如插入调试代码、改变类的方法行为。3、重新定义已加载的类:在程序运行时,可以重新定义已经加载的类(需要JVM支持)。4、监控和获取对象的内存信息:可以获取对象的大小,用于内存分析。
二、Java Agent的实现方式
1、JVM启动时加载:在启动Java应用程序时,通过-javaagent参数加载。这种方式会在目标应用启动前执行,可以拦截所有类的加载过程2、运行时动态附加:在应用程序已经启动的情况下,通过附加到目标JVM进程来加载。这需要Java提供的Attach API
三、Java Agent的历史背景和具体应用场景
Java Agent功能是JDK1.5引入的,通过java.lang.instrument接口实现。这个接口基于JVMTI(Java Virtual Machine Tool Interface)机制,允许开发者构建一个独立于应用程序的代理程序,用于监测和协助运行在JVM上的程序
四、示例
示例1:静态加载方式(启动执行)
工程1 (agent)
步骤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>demo-javaagent</artifactId><version>1.0</version><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.25.0-GA</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>3.1.1</version><configuration><descriptorRefs><!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar--><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs><archive><!-- 设置manifest配置文件--><manifestEntries><!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。--><Premain-Class>demo.MethodAgentMain</Premain-Class><!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。--><Agent-Class>demo.MethodAgentMain</Agent-Class><!--Can-Redefine-Classes: 是否可进行类定义。--><Can-Redefine-Classes>true</Can-Redefine-Classes><!--Can-Retransform-Classes: 是否可进行类转换。--><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration><executions><execution><!--绑定到package生命周期阶段上--><phase>package</phase><goals><!--绑定到package生命周期阶段上--><goal>single</goal></goals></execution></executions></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>${maven.compiler.source}</source><target>${maven.compiler.target}</target></configuration></plugin></plugins></build>
</project>
步骤2: 创建 premain 方法,方法的主要功能是修改 App setName() 方法体
package demo;import java.lang.instrument.Instrumentation;public class MethodAgentMain {public static void premain(String args, Instrumentation inst) {MyTransformer tran = new MyTransformer();inst.addTransformer(tran);}
}
package demo;import javassist.*;import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;public class MyTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {if ("org/example/App".equals(className)) {try {// 从ClassPool获得CtClass对象final ClassPool classPool = ClassPool.getDefault();// 尝试添加额外的类路径(如果需要)classPool.appendClassPath(new ClassClassPath(this.getClass()));classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));final CtClass clazz = classPool.get("org.example.App");CtMethod convertToAbbr = clazz.getDeclaredMethod("setName");String methodBody = "{\n" +" this.name = \"ccc\" + \" aaa\";\n" +" }";convertToAbbr.setBody(methodBody);byte[] byteCode = clazz.toBytecode();clazz.detach();return byteCode;} catch (NotFoundException | CannotCompileException | IOException e) {e.printStackTrace();}}System.out.println(className);return classfileBuffer;}}
步骤3: 编译打包
执行 mvn clean package 编译打包,最终打包生成了 agent jar 包,结果示例:
工程2(主工程)
package org.example;public class App {private int code;private String name;public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "App{" +"code=" + code +", name='" + name + '\'' +'}';}
}
package org.example;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {App app = new App();app.setName("a");app.setCode(123);System.out.println(app);SpringApplication.run(DemoApplication.class, args);}
}
启动 javaagent
java -javaagent:demo-javaagent-1.0-jar-with-dependencies.jar -jar demo-1.0.0-SNAPSHOT.jar > output.log
查看运行结果:
App{code=123, name='ccc aaa'}
name 属性被成功修改。
示例2:动态加载方式(启动之后,接口调用触发)
在接口调用时触发某些行为,可以使用 Java Agent 来改变接口方法调用的行为
工程1 (agent)
步骤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>demo-javaagent</artifactId><version>1.0</version><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.25.0-GA</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>3.1.1</version><configuration><descriptorRefs><!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar--><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs><archive><!-- 设置manifest配置文件--><manifestEntries><!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。--><Premain-Class>demo.MethodAgentMain</Premain-Class><!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。--><Agent-Class>demo.MethodAgentMain</Agent-Class><!--Can-Redefine-Classes: 是否可进行类定义。--><Can-Redefine-Classes>true</Can-Redefine-Classes><!--Can-Retransform-Classes: 是否可进行类转换。--><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration><executions><execution><!--绑定到package生命周期阶段上--><phase>package</phase><goals><!--绑定到package生命周期阶段上--><goal>single</goal></goals></execution></executions></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>${maven.compiler.source}</source><target>${maven.compiler.target}</target></configuration></plugin></plugins></build>
</project>
步骤2: 创建 premain 方法,方法的主要功能是修改方法体,在方法调用前后添加日志输出
package demo;import java.lang.instrument.Instrumentation;public class MethodAgentMain {public static void premain(String args, Instrumentation inst) {MyTransformer tran = new MyTransformer();inst.addTransformer(tran);}
}
package demo;import javassist.*;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;public class MyTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {if (className != null && className.startsWith("org/example/controller/")) {ClassPool pool = ClassPool.getDefault();// 尝试添加额外的类路径(如果需要)pool.appendClassPath(new ClassClassPath(this.getClass()));pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));CtClass ctClass;try {ctClass = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));for (CtBehavior method : ctClass.getDeclaredBehaviors()) {if (method.isEmpty() || method.getMethodInfo() == null) {continue;}// 修改方法体,在方法调用前后添加日志输出method.insertBefore("System.out.println(\"Before method call: \" + $sig);");method.insertAfter("System.out.println(\"After method call: \" + $sig);");}return ctClass.toBytecode();} catch (Exception e) {e.printStackTrace();}}return null;}}
步骤3: 编译打包
执行 mvn clean package 编译打包,最终打包生成了 agent jar 包,结果示例:
工程2(主工程)
一个普通的spring工程
package org.example.controller;import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @Description: TODO* @Author: Top* @Version: V1.0* @Date: 2020-01-15 15:03*/
@RestController
@RequestMapping("/api/{edition}")
public class ConsumerController {@Autowiredprivate Environment env;@GetMapping("/detail")@ResponseBodypublic String detail(String name) throws JsonProcessingException {return name;}
启动工程:
java -javaagent:demo-javaagent-1.0-jar-with-dependencies.jar -jar demo-1.0.0-SNAPSHOT.jar > output.log
执行结果:
Before method call: [Ljava.lang.Class;@bab9ac
consumer
After method call: [Ljava.lang.Class;@2756b30e
Java agent原理说明:
主流的JVM都提供了Instrumentation的实现,但是鉴于Instrumentation的特殊功能,并不适合直接提供在JDK的runtime里,而更适合出现在Java程序的外层,以上帝视角在合适的时机出现。
因此如果想使用Instrumentation功能,拿到Instrumentation实例,我们必须通过Java agent。
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。
Java agent与Instrumentation密不可分,二者也需要在一起使用。因为Instrumentation的实例会作为参数注入到Java agent的启动方法中。
Instrumentation是Java提供的JVM接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。
public interface Instrumentation {/*** 注册一个Transformer,从此之后的类加载都会被Transformer拦截。* Transformer可以直接对类的字节码byte[]进行修改*/void addTransformer(ClassFileTransformer transformer);/*** 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。* retransformClasses可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性*/void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;/*** 获取一个对象的大小*/long getObjectSize(Object objectToSize);/*** 将一个jar加入到bootstrap classloader的 classpath里*/void appendToBootstrapClassLoaderSearch(JarFile jarfile);/*** 获取当前被JVM加载的所有类对象*/Class[] getAllLoadedClasses();
}
注意:
Instrumentation的局限性
在运行时,我们可以通过Instrumentation的redefineClasses方法进行类重定义,在redefineClasses方法上有一段注释需要特别注意:
java 代码解读复制代码 * The redefinition may change method bodies, the constant pool and attributes.* The redefinition must not add, remove or rename fields or methods, change the* signatures of methods, or change inheritance. These restrictions maybe be* lifted in future versions. The class file bytes are not checked, verified and installed* until after the transformations have been applied, if the resultant bytes are in* error this method will throw an exception.这里面提到,我们不可以增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。认识到这一点很重要,当我们通过ASM获取到增强的字节码之后,如果增强后的字节码没有遵守这些规则,那么调用redefineClasses方法来进行类的重定义就会失败。