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 {
@Override
public 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;
}
@Override
public 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 {
@Override
public 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 {
@Autowired
private Environment env;
@GetMapping("/detail")
@ResponseBody
public 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方法来进行类的重定义就会失败。