JavaAgent是在JDK5之后提供的新特性,也可以叫java代理。
开发者通过这种机制(Instrumentation)可以在类加载class文件之前修改方法的字节码(此时字节码尚未加入JVM),动态更改类方法实现AOP,提供监控服务如;方法调用时长、可用率、内存等。
1、Java字节码简介
Java字节码是众多字节码增强技术的知识基础。
Java语言写出的源代码首先需要编译成class文件,即字节码文件,然后被JVM加载并运行,每个 class文件 具有如下固定的数据格式
ClassFile {
u4 magic; // 魔数,固定为0xCAFEBABE
u2 minor_version; // 次版本
u2 major_version; // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推
u2 constant_pool_count; // 常量池个数
cp_info constant_pool[constant_pool_count-1]; // 常量池定义
u2 access_flags; // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等
u2 this_class; // 类索引
u2 super_class; // 父类索引
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
可以看到, class文件
总是一个魔数开头,后面跟着版本号,然后就是常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表
。
class文件本质上是一个字节码流,每个字节码所处的位置代表着一定的指令和含义。如何对class文件中定义的指令和字节码进行解读、增强定义、编排,这是字节码增强技术所要完成的事情。
了解Java字节码有助于字节码增强的开发,但并不是实现字节码增强开发的必要条件,最新主流的众多字节码增强工具框架类库都将字节码的编排进行了不同程度封装,在可读性、易编排性、排错性上提供开发便利性,学习曲线和开发难度得到了较好的改善。
2、Java字节码增强
对于字节码增强的开发来说,JVMTI是一个在实践中应该被熟悉的工具技术。
JVM从1.5版本开始提供 JVM Tool Interface ,这是JVM对外的、用于Java应用监控和调试的一系列工具接口,是JVM平台调试架构的重要组成部分。
The Java™ Platform Debugger Architecture is structured as follows:
Components Debugger Interfaces
/ |--------------|
/ | VM |
debuggee ----( |--------------| <------- JVM TI - Java VM Tool Interface(Jvm服务端调试接口)
\ | back-end |
\ |--------------|
/ |
comm channel -( | <--------------- JDWP - Java Debug Wire Protocol (Java调试通信协议)
\ |
|--------------|
| front-end |
|--------------| <------- JDI - Java Debug Interface (客户端调试接口和调试应用)
| UI |
|--------------|
JVM启动支持加载agent代理,而agent代理本身就是一个JVM TI的客户端,其通过监听事件的方式获取Java应用运行状态,调用JVM TI提供的接口对应用进行控制。
我们可以看下Java agent代理的两个入口函数定义
// 用于JVM刚启动时调用,其执行时应用类文件还未加载到JVM
public static void premain(String agentArgs, Instrumentation inst);
// 用于JVM启动后,在运行时刻加载
public static void agentmain(String agentArgs, Instrumentation inst);
这两个入口函数定义分别对应于JVM TI专门提供了执行 字节码增强(bytecode instrumentation) 的两个接口。
premain加载时刻增强(JVM 启动时加载),类字节码文件在JVM加载的时候进行增强,。
agentMain动态增强(JVM 运行时加载),已经被JVM加载的class字节码文件,当被修改或更新时进行增强
这两个接口都是从JDK 1.6开始支持。
我们无需对上面JVM TI提供的两个接口规范了解太多,Java Agent和 Java Instrument类包 封装好了字节码增强的上述接口通信。
上面我们已经说到了, 有两处地方可以进行 Java Agent 的加载,分别是 目标JVM启动时加载 和 目标JVM运行时加载,这两种不同的加载模式使用不同的入口函数:
1、JVM 启动时加载
入口函数如下所示:
// 函数1
public static void premain(String agentArgs, Instrumentation inst);
// 函数2
public static void premain(String agentArgs);
JVM 首先寻找函数1,如果没有发现函数1,则会寻找函数2
2、JVM 运行时加载
入口函数如下所示:
// 函数1
public static void agentmain(String agentArgs, Instrumentation inst);
// 函数2
public static void agentmain(String agentArgs);
与上述一致,JVM 首先寻找函数1,如果没有发现函数1,则会寻找函数2
这两组方法的第一个参数 agentArgs 是随同 “-javaagent” 一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这参数,inst 是 Instrumentation 类型的对象,是 JVM 自己传入的,我们可以那这个参数进行参数的增强操作。
2.1、premain演示
1、定义premain-class 定义两个接口方法
public class AgentDemo {
/**
* JVM 首先尝试在代理类上调用以下方法
* 该方法在main方法之前运行,
* 与main方法运行在同一个JVM中
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=========premain方法执行 1========");
System.out.println("agentArgs:="+agentArgs);
}
/**
* 候选的、兜底 方法:
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 则会执行 premain(String agentArgs)
*
*/
public static void premain(String agentArgs) {
System.out.println("=========premain 方法执行 2========");
System.out.println("agentArgs:="+agentArgs);
}
}
MANIFEST.MF 文件可以自己定义,也可以自动生成
2、编写MANIFEST.MF 文件
注意:最后需要有一个空行
Manifest-Version: 1.0
Can-Redefine-Classes: true # true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true # true 表示能重转换此代理所需的类,默认值为 false (可选)
Premain-Class: com.xiaoxu.javaagent.AgentDemo #premain方法所在类的位置
2、或者maven项目,在pom.xml加入
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xiaoxu</groupId>
<artifactId>java-agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>java-agent</name>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
</dependencies>
<build>
<plugins>
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-compiler-plugin</artifactId>-->
<!-- <version>3.6.1</version>-->
<!-- <configuration>-->
<!-- <source>${java.version}</source>-->
<!-- <target>${java.version}</target>-->
<!-- </configuration>-->
<!-- </plugin>-->
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>false</shadedArtifactAttached>
<createDependencyReducedPom>true</createDependencyReducedPom>
<createSourcesJar>true</createSourcesJar>
<shadeSourcesContent>true</shadeSourcesContent>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>com.xiaoxu.javaagent.AgentDemo</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</transformer>
</transformers>
<artifactSet>
<excludes>
<exclude>*:gson</exclude>
<exclude>io.netty:*</exclude>
<exclude>io.opencensus:*</exclude>
<exclude>com.google.*:*</exclude>
<exclude>com.google.guava:guava</exclude>
<exclude>org.checkerframework:checker-compat-qual</exclude>
<exclude>org.codehaus.mojo:animal-sniffer-annotations</exclude>
<exclude>io.perfmark:*</exclude>
<exclude>org.slf4j:*</exclude>
</excludes>
<!-- 将javassist包打包到Agent中 -->
<includes>
<include>javassist:javassist:jar:</include>
</includes>
</artifactSet>
<filters>
<filter>
<artifact>net.bytebuddy:byte-buddy</artifact>
<excludes>
<exclude>META-INF/versions/9/module-info.class</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3、Agent的简单使用
1、JVM 启动时加载
我们直接在 JVM 启动参数中加入 -javaagent 参数并指定 jar 文件的位置
# 指定agent程序并运行该类
java -javaagent:-javaagent:E:\IdeaProject\FrameworkSources\JavaAgent\target\java-agent-0.0.1-SNAPSHOT.jar PremainTest
或者
VM options: -javaagent:E:\IdeaProject\FrameworkSources\JavaAgent\target\java-agent-0.0.1-SNAPSHOT.jar
定义一个测试类执行
2、JVM 运行时加载
要实现动态调试,我们就不能将目标JVM停机后再重新启动,这不符合我们的初衷,因此我们可以使用 JDK 的 Attach Api 来实现运行时挂载 Agent。
Attach Api 是 SUN 公司提供的一套扩展 API,用来向目标 JVM 附着(attach)在目标程序上,有了它我们可以很方便地监控一个 JVM。
Attach Api 对应的代码位于 com.sun.tools.attach
包下,提供的功能也非常简单:
- 列出当前所有的 JVM 实例描述
- Attach 到其中一个 JVM 上,建立通信管道,让目标JVM加载Agent
该包下有一个类 VirtualMachine
,它提供了两个重要的方法:
-
VirtualMachine attach(String var0)
传递一个进程号,返回目标 JVM 进程的 vm 对象,该方法是 JVM进程之间指令传递的桥梁,底层是通过 socket 进行通信 -
void loadAgent(String var1)
该方法允许我们将 agent 对应的 jar 文件地址作为参数传递给目标 JVM,目标 JVM 收到该命令后会加载这个 Agent
有了 Attach Api ,我们就可以创建一个java进程,用它attach到对应的jvm,并加载agent。
4、ClassFileTransformer字节码转换器
我们需要了解的是,上述入口函数传入的第二个参数Instrumentation实例,即Java Instrument类 java.lang.instrument.Instrumentation ,查看其类定义,可以看到其提供的核心方法只有一个addTransformer,用于添加多个ClassFileTransformer
// 说明:添加ClassFileTransformer
// 第一个参数:transformer,类转换器
// 第二个参数:canRetransform,经过transformer转换过的类是否允许再次转换
void Instrumentation.addTransformer(ClassFileTransformer transformer, boolean canRetransform)
而 ClassFileTransformer 则提供了tranform()方法,用于对加载的类进行增强重定义,返回新的类字节码流。
需要特别注意的是,若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。
// 说明:对类字节码进行增强,返回新的类字节码定义
// 第一个参数:loader,类加载器
// 第二个参数:className,内部定义的类全路径
// 第三个参数:classBeingRedefined,待重定义/转换的类
// 第四个参数:protectionDomain,保护域
// 第五个参数:classfileBuffer,待重定义/转换的类字节码(不要直接在这个classfileBuffer对象上修改,需拷贝后进行)
// 注:若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。
byte[] ClassFileTransformer.transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte classfileBuffer)
4.1、Javassist 修改字节码
重写ClassFileTransformer 接口的唯一方法 transform() 方法,返回值是转换后的类字节码文件
在 transform 方法中,通过 Javassist 修改字节码
1、自定义字节码转换器
/**
* @author xiaoxu
* @description 自定义字节码转换器
* @date 2022/10/8
*/
public class CustomTransformer implements ClassFileTransformer {
/**
* byte[]: 类字节码文件数组
* className: 类文件路径
*/
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] bytes = null;
if ("com/xiaoxu/javaagent/transformer/HelloWorld".equals(className)) {
try {
// 获得一个ClassPool对象
ClassPool cp = ClassPool.getDefault();
// classPool根据需要读取类文件以构造CtClass对象
CtClass cc = cp.get("com.xiaoxu.javaagent.transformer.HelloWorld");
CtMethod method = cc.getDeclaredMethod("say");
// 方法增强
method.insertBefore("{System.out.println(\"before say()\");}");
method.insertAfter("{System.out.println(\"after say()\");}");
// 将增强类 转字节码返回
bytes = cc.toBytecode();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}else {
System.out.println(className +" 字节码转换器略过,不是目标字节码");
}
return bytes;
}
}
2、定义premain-class
public class AgentClassFileTransformerPreMain {
public static void premain(String args, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("premian: 执行啦!");
// 增加字节码增强的转换器
inst.addTransformer(new CustomTransformer(),true);
// main方法执行前 获取全部已加载的class
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class loadedClass : allLoadedClasses) {
System.out.println(loadedClass.getSimpleName());
}
}
}
打成jar包
3、定义目标类
public class HelloWorld {
public void say(){
System.out.println("xiaoxu hello world!");
}
}
4、使用javaagent参数 测试
public class PremainTest {
public static void main(String[] args) {
new HelloWorld().say();
}
}
动态修改字节码成功
Javassist 是一个非常早的字节码操作类库,开始于1999年,
它能够支持两种编辑方式:
- 源码级别
- 字节码指令级别
相比于晦涩的字节码级别,源码级别更加人性化,代码编写起来更加易懂。
5、Java字节码增强工具关系图
6、字节码增强组件对比
综上:引出ByteBuddy
参考文章
https://blog.csdn.net/crazymakercircle/article/details/126579528?spm=1001.2014.3001.5502