Jdk1.5 以后引入了 ​​javaAgent​​ 技术,javaAgent 是运行在方法之前的拦截器,相当于是对字节码进行的一个 AOP 操作,利用 javaAgent 和 ASM 字节码技术,在 JVM 加载 class 二进制文件的时候,利用 ASM 动态的修改加载的 class 文件。Javaagent 是 java 命令的一个参数。参数 javaagent 可以用于指定一个 jar 包。

Java-Agent_maven

对该 Java 包有 2 个要求:

  • 这个 jar 包的 MANIFEST.MF 文件必须指定​​Premain-Class​​ 项
  • Premain-Class 指定的那个类必须实现​​premain()​​ 方法

premain 方法

  • 从字面上理解,就是运行在 main 函数之前的的类
  • 当 Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行​​-javaagent​​​ 所指定 jar 包内​​Premain-Class​​​ 这个类的​​premain​​ 方法

使用 JavaAgent

  • 创建一个 Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定
  • 定义一个​​MANIFEST.MF​​​ 文件,必须包含 Premain-Class 选项,通常也会加入​​Can-Redefine-Classes​​​ 和​​Can-Retransform-Classes​​ 选项
  • 将​​premain​​​ 的类和​​MANIFEST.MF​​​ 文件打成​​jar​​ 包,使用插件自动生成

MANIFEST.MF

  • Premain-Class:包含 premain 方法的类(类的全路径名)
  • Agent-Class:包含 agentmain 方法的类(类的全路径名)
  • Boot-Class-Path:设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径(可选)
  • Can-Redefine-Classes:true 表示能重定义此代理所需的类,默认值为 false(可选)
  • Can-Retransform-Classes:true 表示能重转换此代理所需的类,默认值为 false(可选)
  • Can-Set-Native-Method-Prefix:true 表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

使用参数 -javaagent: jar包路径, 启动要代理的方法。

快速入门

创建一个 Maven 工程:

Java-Agent_java_02

添加依赖:

<?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>top.it6666</groupId>
<artifactId>JavaAgent</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.9.2</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!--
自动添加 META-INF/MANIFEST.MF
-->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>top.it6666.PreMainAgent</Premain-Class>
<Agent-Class>top.it6666.PreMainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

创建包, 在包中创建 ​​PreMainAgent.java​​:

Java-Agent_maven_03

/**
* @author BNTang
**/
public class PreMainAgent {

/**
* 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。
* 与 main函数不同的是,
* 这个参数是一个字符串而不是一个字符串数组
* 2、Inst 是一个 java.lang.instrument.Instrumentation 的实例,
* 由 JVM 自动传入
* java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
* 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
**/
public static void premain (String agentArgs, Instrumentation inst) {
System.out.println ("========= premain方法执行1 ========");
System.out.println (agentArgs);
}

/**
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 则会执行 premain(String agentArgs)
*/
public static void premain (String agentArgs) {
System.out.println ("========= premain方法执行2 ========");
System.out.println (agentArgs);
}
}

使用 maven 的 package 命令进行打包:

Java-Agent_java_04

测试

创建 ​​TestMain.java​​ 测试类:

Java-Agent_JavaAgent_05

/**
* @author BNTang
**/
public class TestMain {
public static void main (String[] args) {
System.out.println ("hello world");
}
}

在启动的时候, 配置 JavaAgent 参数,拷贝刚刚打包好的 jar 包的绝对路径:

Java-Agent_JavaAgent_06

配置参数:

Java-Agent_maven_07

Java-Agent_maven_08

配置的 JVM 的参数,新版本的 IDEA 需要开启这个配置如下图:

Java-Agent_java_09

-javaagent:D:\Develop\IDEAPro\JavaAgent\target\JavaAgent-1.0-SNAPSHOT.jar=test

运行 TestMain.java 最终效果如下图所示:

Java-Agent_maven_10

通过字节码对方法增强

新建一个 ​​Test.java​​​ 然后运行一下程序编译成字节码,然后将字节码拷贝到 ​​resources​​ 当中如下图:

Java-Agent_maven_11

Test.java:

/**
* @author BNTang
* @version 1.0
* @project my-demo-pro
* @description
* @since Created in 2021/10/29 029 22:44
**/
public class Test {
String name = "Hello agent";

public void show() {
System.out.println("新功能");
System.out.println(name);
}

public void test() {
System.out.println("test");
}
}

修改 ​​PreMainAgent.java​​ 注意 JDK 版本必须是 1.8:

/**
* @author BNTang
**/
public class PreMainAgent {

/**
* 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。 与 main函数不同的是, 这个参数是一个字符串而不是一个字符串数组 2、Inst 是一个
* java.lang.instrument.Instrumentation 的实例, 由 JVM 自动传入 java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
* 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
**/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("========= premain方法执行1 ========");
System.out.println(agentArgs);
enhanceTest(agentArgs, inst);
}

/**
* 如果不存在 premain(String agentArgs, Instrumentation inst) 则会执行 premain(String agentArgs)
*/
public static void premain(String agentArgs) {
System.out.println("========= premain方法执行2 ========");
System.out.println(agentArgs);
}

public static void enhanceTest(String agentOps, Instrumentation inst) {
inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
// 判断是指定的class
if ("top/it6666/Test".equals(className)) {
try {
// 获取更改后的类class,字节数组
classfileBuffer = Files
.readAllBytes(Paths.get("D:\\Develop\\IDEAPro\\JavaAgent\\src\\main\\resources\\Test.class"));
} catch (IOException e) {
e.printStackTrace();
}
}
return classfileBuffer;
});
}
}

修改 ​​TestMain.java​​:

/**
* @author BNTang
**/
public class TestMain {
public static void main(String[] args) {
new Thread(() -> {
while (true) {
new Test().show();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

PreMainAgent 重新 install,然后在运行效果如下图所示:

Java-Agent_java_12

使用 byte-buddy 来实现类的增强

依赖上方已经添加过了,添加拦截器 ​​MyInterceptor.java​​:

/**
* @author BNTang
*/
public class MyInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
try {
// 执行原方法
System.out.println("新增内容");
Object call = callable.call();
System.out.println("尾部新增");
return call;
} finally {
// 打印调用时长
System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");
}
}
}

修改 PreMainAgent.java:

/**
* @author BNTang
**/
public class PreMainAgent {

/**
* 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。 与 main函数不同的是, 这个参数是一个字符串而不是一个字符串数组 2、Inst 是一个
* java.lang.instrument.Instrumentation 的实例, 由 JVM 自动传入 java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
* 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
**/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("========= premain方法执行1 ========");
System.out.println(agentArgs);
buttyBuddyDemo(agentArgs, inst);
}

/**
* 如果不存在 premain(String agentArgs, Instrumentation inst) 则会执行 premain(String agentArgs)
*/
public static void premain(String agentArgs) {
System.out.println("========= premain方法执行2 ========");
System.out.println(agentArgs);
}

public static void buttyBuddyDemo(String agentOps, Instrumentation inst) {
// Byte-Buddy专门有个AgentBuilder来处理Java Agent的场景
new AgentBuilder.Default()
// 根据包名前缀拦截类
.type(ElementMatchers.nameStartsWith("top.it6666"))
// 拦截到的类由transformer处理
.transform(
(builder, typeDescription, classLoader, javaModule) -> builder.method(ElementMatchers.named("show"))
// .method(ElementMatchers.<MethodDescription>any())
.intercept(MethodDelegation.to(MyInterceptor.class)))
.installOn(inst);
}
}

测试方式,先用 maven.package 打包,然后在运行我们之前的测试类,运行效果如下图所示:

Java-Agent_jar_13

动态

我这里就单独创建了一个模块,就是专门用来演示和存放动态增强的代码,​​java-agent-attach​​,修改 pom.xml 添加如下依赖:

<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>D:/Develop/Java/Jdk/jdk1.8.0_281/lib/tools.jar</systemPath>
</dependency>
</dependencies>

取消 java-agent 的 JVM 参数配置:

Java-Agent_jar_14

将你需要进行增强的 .class 文件放入 java-agent 工程的 resources 当中:

Java-Agent_maven_15

然后改写 PreMainAgent.java 改写之后的内容如下:

/**
* @author BNTang
**/
public class PreMainAgent {

/**
* 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。 与 main函数不同的是, 这个参数是一个字符串而不是一个字符串数组 2、Inst 是一个
* java.lang.instrument.Instrumentation 的实例, 由 JVM 自动传入 java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
* 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
**/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("========= premain方法执行1 ========");
System.out.println(agentArgs);
simpleDemo(agentArgs, inst);
}

/**
* 如果不存在 premain(String agentArgs, Instrumentation inst) 则会执行 premain(String agentArgs)
*/
public static void premain(String agentArgs) {
System.out.println("========= premain方法执行2 ========");
System.out.println(agentArgs);
}

public static void agentmain(String agentOps, Instrumentation inst) {
System.out.println("=========agentmain方法执行========");
simpleDemo(agentOps, inst);
//transform是会对尚未加载的类进行增加代理层,这里是已经运行中的jvm,所以类以及被加载了
//必须主动调用retransformClasses让jvm再对运行中的类进行加上代理层
//下一次执行的时候, 要重新读取class字节码

Arrays.stream(inst.getAllLoadedClasses()).forEach(allLoadedClass -> {
//这里的Test路径,修改成你自己机器agent-demo-web工程的Test类的路径
if (allLoadedClass.getName().contains("top.it6666.Test")) {
try {
System.out.println(allLoadedClass.getName());
inst.retransformClasses(allLoadedClass);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
});
}

public static void simpleDemo(String agentOps, Instrumentation inst) {
inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
System.out.println(className);
//判断是指定的class
if ("top/it6666/Test".equals(className)) {
try {
System.out.println("获取更改后的类class 字节数组");
//获取更改后的类class 字节数组
String path = "D:\\Develop\\IdeaPro\\my-demo-pro\\java-agent\\java-agent-pro\\src\\main\\resources\\Test.class";
classfileBuffer = Files.readAllBytes(Paths.get(path));
} catch (IOException e) {
e.printStackTrace();
}
}
return classfileBuffer;
}, true);
}
}

如上的 path 路径里面的 .class 改为你自己的 .class 真实文件路径,然后其它的没啥需要注意的,紧接着添加 Attach.java:

/**
* @author BNTang
* @date 2021/10/29
*/
public class Attach {
public static void main(String[] args) {
// 查找所有jvm进程,排除attach测试工程
List<VirtualMachineDescriptor> attach = VirtualMachine.list()
.stream()
.filter(jvm -> {
System.out.println(jvm.displayName());
return !jvm.displayName().contains("Attach");
}).collect(Collectors.toList());

for (int i = 0; i < attach.size(); i++) {
System.out.println("[" + i + "] " + attach.get(i).displayName() + ":" + attach.get(i).id());
}

System.out.println("请输入需要attach的pid编号:");

Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
VirtualMachineDescriptor virtualMachineDescriptor = attach.get(new Integer(s));
try {
VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
virtualMachine.loadAgent("D:\\Develop\\IdeaPro\\my-demo-pro\\java-agent\\java-agent-pro\\target\\java-agent-pro-1.0-SNAPSHOT.jar", "param");
virtualMachine.detach();
} catch (AttachNotSupportedException e) {
System.out.println("AttachNotSupportedException:" + e.getMessage());
} catch (IOException e) {
System.out.println("IOException:" + e.getMessage());
} catch (AgentLoadException e) {
System.out.println("AgentLoadException:" + e.getMessage());
} catch (AgentInitializationException e) {
System.out.println("AgentInitializationException:" + e.getMessage());
}
}
}

virtualMachine.loadAgent 中的 jar 是你 java-agent 打包好的 jar 存放位置,一定要指定为你的其它的也没有什么需要注意的。

测试流程如下图:

Java-Agent_maven_16

Java-Agent_maven_17