阅读完需:约 59 分钟
Java Agent 直译为 Java 代理,也常常被称为 Java 探针技术。
Java Agent 是在 JDK1.5 引入的,是一种可以动态修改 Java 字节码的技术。Java 中的类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器对这些字节码进行修改,以此来完成一些额外的功能。
Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程,进行工作。启动时只需要在目标程序的启动参数中添加-javaagent 参数添加 ClassFileTransformer 字节码转换器,相当于在main方法前加了一个拦截器。
所谓Java Agent,其功能都是基于java.lang.instrument中的类去完成。Instrument提供了允许Java编程语言代理检测JVM上运行的程序的功能,而检测的机制就是修改字节码。Instrument位于rt.jar中,java.lang.instrument包下,使用Instrument可以用来检测或协助运行在 JVM中的程序;甚至对已加载class进行替换修改,这也就是我们常说的热部署、热加载。一句话总结Instrument:检测类的加载行为对其进行干扰(修改替换)
Instrument的实现基于JVMTI(Java Virtual Machine Tool Interface)的,所谓JVMTI就是一套由 Java 虚拟机提供的,为JVM 相关的工具提供的本地编程接口集合。JVMTI基于事件驱动,简单点讲就是在JVM运行层面添加一些钩子可以供开发者去自定义实现相关功能。
Java Agent 主要有以下功能
- Java Agent 能够在加载 Java 字节码之前拦截并对字节码进行修改;
- Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;
Java Agent 的应用场景
- IDE 的调试功能,例如 Eclipse、IntelliJ IDEA ;
- 热部署功能,例如 JRebel、XRebel、spring-loaded;
- 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
- 各种性能分析工具,例如 Visual VM、JConsole 等;
- 全链路性能检测工具,例如 Skywalking、Pinpoint等;
Java Agent 实现原理
在了解Java Agent的实现原理之前,需要对Java类加载机制有一个较为清晰的认知。一种是在main方法执行之前,通过premain来执行,另一种是程序运行中修改,需通过JVM中的Attach实现,Attach的实现原理是基于JVMTI。
主要是在类加载之前,进行拦截,对字节码修改
下面我们分别介绍一下这些关键术语:
JVMTI 就是JVM Tool Interface,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展
JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现
JVMTIAgent是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
-
Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置 -
Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数 -
Agent_OnUnload函数,在agent卸载时调用
javaagent 依赖于instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的
instrument 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent
JVM Attach 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行
相关API初探
Instrumentation
java.lang.instrument包下关键的类为:java.lang.instrument.Instrumentation。该接口提供一系列替换转化class定义的方法。接下来看一下该接口的主要方法进行以下说明:
addTransformer
用于注册transformer。除了任何已注册的转换器所依赖的类的定义外,所有未来的类定义都将可以被transformer看到。当类被加载或被重新定义(redefine,可以是下方的redefineClasses触发)时,transformer将被调用。如果canRetransform为true,则表示当它们被retransform时(通过下方的retransformClasses),该transformer也会被调用。addTransformer共有如下两种重载方法:
void addTransformer(ClassFileTransformer transformer,boolean canRetransform)
void addTransformer(ClassFileTransformer transformer)
redefineClasses
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException,
UnmodifiableClassException
此方法用于替换不引用现有类文件字节的类定义,就像从源代码重新编译以进行修复并继续调试时所做的那样。该方法对一系列ClassDefinition进行操作,以便允许同时对多个类进行相互依赖的更改(类a的重新定义可能需要类B的重新定义)。假如在redifine时,目标类正在执行中,那么执行中的行为还是按照原来字节码的定义执行,当对该类行为发起新的调用时,将会使用redefine之后的新行为。
注意:此redefine不会触发类的初始化行为
当然redefine时,并不是随心所欲,我们可以重新定义方法体、常量池、属性、但是不可以添加、移除、重命名方法和方法和入参,不能更改方法签名或更改继承。当然,在未来的版本中,这些限制可能不复存在。
在转换之前,不会检查、验证和安装类文件字节,如果结果字节出现错误,此方法将抛出异常。而抛出异常将不会有类被重新定义
retransformClasses
针对JVM已经加载的类进行转换,当类初始加载或重新定义类(redefineClass)时,可以被注册的ClassFileTransformer进行转化;但是针对那些已经加载完毕之后的类不会触发这个transform行为进而导致这些类无法被我们agent进行监听,所以可以通过retransformClasses触发一个事件,而这个事件可以被ClassFileTransformer捕获进而对这些类进行transform。
此方法将针对每一个通过addTransformer注册的且canRetransform是true的,进行调用其transform方法,转换后的类文件字节被安装成为类的新定义,从而拥有新的行为。
redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。
ClassFileTransformer
在我们的agent中,需要提供该接口的实现,以便在JVM定义类之前转换class字节码文件,该接口中就提供了一个方法,此方法的实现可以转换提供的类文件并返回一个新的替换类文件:
byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException
Instrumentation接口源码
public interface Instrumentation
{
//添加ClassFileTransformer
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//添加ClassFileTransformer
void addTransformer(ClassFileTransformer transformer);
//移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
//是否可以被重新定义
boolean isRetransformClassesSupported();
// 重新转换提供的类集。此功能有助于检测已加载的类。
// 最初加载类或重新定义类时,可以使用 ClassFileTransformer 转换初始类文件字节。
// 此函数重新运行转换过程(无论之前是否发生过转换)。
retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 返回当前 JVM 配置是否支持重新定义类。
// 重新定义已加载的类的能力是 JVM 的可选能力。
// 仅当代理 JAR 文件中的 Can-Redefine-Classes 清单属性设置为 true(如包规范中所述)并且 JVM 支持此功能时,才支持重新定义。
boolean isRedefineClassesSupported();
//重新定义Class文件
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
//是否可以修改Class文件
boolean isModifiableClass(Class<?> theClass);
//获取所有加载的Class
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
//获取指定类加载器已经初始化的类
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
//获取某个对象的大小
long getObjectSize(Object objectToSize);
//添加指定jar包到启动类加载器检索路径
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
//添加指定jar包到系统类加载检索路径
void appendToSystemClassLoaderSearch(JarFile jarfile);
//本地方法是否支持前缀
boolean isNativeMethodPrefixSupported();
//设置本地方法前缀,一般用于按前缀做匹配操作
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
JVM启动前的agent实现
Instrument是JDK5开始引入,在JDK5中Instrument要求在目标JVM程序运行之前通过命令行参数javaagent来设置代理类,在JVM初始化之前,Instrument启动在JVM中设置回调函数,检测特点类加载情况完成实际增强工作。
-javaagent: jarpath[ =options]
这里jarpath就是我们的agent jar的路径,agent jar必须符合jar文件规范。代理JAR文件的manifest(META-INF/MANIFEST.MF)必须包含属性Premain-Class。此属性的值是代理类的类名。代理类必须实现一个公共静态premain方法,该方法原则上与主应用程序入口点类似。在JVM初始化之后,将按照指定代理的顺序调用每个主方法(premain),然后将调用实际应用程序的主方法(main)。每个premain方法必须按照启动顺序返回。
premain方法可以有如下两种重载方法,如果两者同时存在,则优先调用多参数的方法:
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
JVM启动后的agent实现
JDK6开始为Instrument增加很多强大的功能,其中要指出的就是在JDK5中如果想要完成增强处理,必须是在目标JVM程序启动前通过命令行指定Instrument,然后在实际应用中,目标程序可能是已经运行中,针对这种场景下如果要保证 JVM不重启得以完成我们工作,这不是我们想要的,于是JDK6中Instrument提供了在JVM启动之后指定设置java agent达到Instrument的目的。
该实现需要确保以下3点:
- agent jar中manifest必须包含属性Agent-Class,其值为agent类名。
- agent类中必须包含公有静态方法agentmain
- system classload必须支持可以将agent jar添加到system class path。
agent jar将被添加到system class path,这个路径就是SystemClassLoader加载主应用程序的地方,agent class被加载后,JVM将会尝试执行它的agentmain方法,同样的,如果以下两个方法都存在,则优先执行多参数方法:
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
看到这里,结合JVM前启动前agent的实现和JVM启动后agent的实现,可能想问是否可以在一个agent class中同时包含premain、agentmain呢,答案是可以的,只不过在JVM启动前不会执行agentmain,同样的,JVM启动后不会执行premain。
如果我们的agent无法启动(agent class无法被加载、agentmain出异常、agent class没有合法的agentmain方法等),JVM将不会终止!
入门案例
JVM启动前替换实现
我们已经知道通过配置-javaagent:文件.jar后,在java程序启动时候会执行premain方法。接下来我们使用javassist字节码增强的方式,来监控方法程序的执行耗时。
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。
加入依赖
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
<type>jar</type>
</dependency>
构建项目
MyAgent
public class MyAgent {
//JVM 首先尝试在代理类上调用以下方法
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("this is my agent:" + agentArgs);
MyMonitorTransformer monitor = new MyMonitorTransformer();
inst.addTransformer(monitor);
}
//如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
public static void premain(String agentArgs) {
}
}
MyMonitorTransformer
public class MyMonitorTransformer implements ClassFileTransformer {
private static final Set<String> classNameSet = new HashSet<>();
static {
classNameSet.add("com.enmalvi.javaagent.ApiTest");
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
String currentClassName = className.replaceAll("/", ".");
if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
return null;
}
System.out.println("transform: [" + currentClassName + "]");
CtClass ctClass = ClassPool.getDefault().get(currentClassName);
CtBehavior[] methods = ctClass.getDeclaredBehaviors();
for (CtBehavior method : methods) {
enhanceMethod(method);
}
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private void enhanceMethod(CtBehavior method) throws Exception {
if (method.isEmpty()) {
return;
}
String methodName = method.getName();
if ("main".equalsIgnoreCase(methodName)) {
return;
}
final StringBuilder source = new StringBuilder();
// 前置增强: 打入时间戳
// 保留原有的代码处理逻辑
source.append("{")
.append("long start = System.nanoTime();\n") //前置增强: 打入时间戳
.append("$_ = $proceed($$);\n") //调用原有代码,类似于method();($$)表示所有的参数
.append("System.out.print(\"method:[")
.append(methodName).append("]\");").append("\n")
.append("System.out.println(\" cost:[\" +(System.nanoTime() - start)+ \"ns]\");") // 后置增强,计算输出方法执行耗时
.append("}");
ExprEditor editor = new ExprEditor() {
@Override
public void edit(MethodCall methodCall) throws CannotCompileException {
methodCall.replace(source.toString());
}
};
method.instrument(editor);
}
}
我们需要在Maven中配置,编译打包的插件,这样我们就可以很轻松的借助Maven生成Agent的jar包
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!--
Manifest-Version 文件版本
Premain-Class
包含 premain 方法的类(类的全路径名)main方法运行前代理
Agent-Class
包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构
Boot-Class-Path
设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选)
Can-Redefine-Classes true
表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes true
表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix true
表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
-->
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.enmalvi.javaagent.JavaAgnet01.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
否则我们需要在resources下创建META-INF/MANIFEST.MF文件,文件内容如下,我们可以看出这个与Maven中的配置是一致的,然后通过配置编译器,借助编译器打包成jar包,需指定该文件
Manifest-Version: 1.0
Premain-Class: com.enmalvi.javaagent.JavaAgnet01.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
告示文件MANIFEST.MF参数说明:
-
Manifest-Version文件版本 -
Premain-Class包含premain方法的类(类的全路径名)main方法运行前代理,这个属性用来指定JVM启动时的代理agent,它必须包含premain方法,如果这个属性不存在,则JVM将终止。注意是类的全路径 -
Agent-Class包含agentmain方法的类(类的全路径名)main开始后可以修改类结构,如果agent实现支持在JVM启动后某个时间启动代理的机制,那么该属性则指定该代理类。如果该属性不存在,代理将不会启动。 -
Boot-Class-Path设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选)该属性可以指定BootStrapClassLoad加载的路径。 -
Can-Redefine-Classestrue表示能重定义此代理所需的类,默认值为 false(可选)该属性用来指定该agent是否针对redefineClass产生作用 -
Can-Retransform-Classestrue表示能重转换此代理所需的类,默认值为 false (可选)该属性用来指定该agent是否针对retransformClass产生作用 -
Can-Set-Native-Method-Prefixtrue表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
最后通过Maven生成Agent的jar包,然后修改测试目标程序的启动器,添加JVM参数即可
参数示例
-javaagent:/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar=testargs
测试类
public class ApiTest {
public static void main(String[] args) {
ApiTest apiTest = new ApiTest();
apiTest.echoHi();
}
private void echoHi(){
System.out.println("hi agent");
}
}
结果
this is my agent:testargs
transform: [com.enmalvi.javaagent.ApiTest]
hi agent
method:[echoHi] cost:[40833ns]
JVM启动后替换实现
最早 JDK 1.5发布 java.lang.instrument 包时,agent 是必须在 JVM 启动时,通过命令行选项附着(attach)上去。但在 JVM 正常运行时,加载 agent 没有意义,只有出现问题,需要诊断才需要附着 agent。JDK 1.6 实现了 attach-on-demand(按需附着),可以使用 Attach API 动态加载 agent。这个 Attach API 在 tools.jar 中。JVM 启动时默认不加载这个 jar 包,需要在 classpath 中额外指定。使用 Attach API 动态加载 agent 的示例代码如下:
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
public class AgentLoader {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.err.println("Usage: java -cp .:$JAVA_HOME/lib/tools.jar"
+ " com.demo.AgentLoader <pid/name> <agent> [options]");
System.exit(0);
}
String jvmPid = args[0];
String agentJar = args[1];
String options = args.length > 2 ? args[2] : null;
for (VirtualMachineDescriptor jvm : VirtualMachine.list()) {
if (jvm.displayName().contains(args[0])) {
jvmPid = jvm.id();
break;
}
}
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentJar, options);
jvm.detach();
}
}
启动时加载 agent,-javaagent 传入的 jar 包需要在 MANIFEST.MF 中包含 Premain-Class 属性,此属性的值是 代理类 的名称,并且这个 代理类 要实现 premain 静态方法。启动后加载 agent 也是类似,通过 Agent-Class 属性指定 代理类,代理类 要实现 agentemain 静态方法。agent 被加载后,JVM 将尝试调用 agentmain 方法。
对于已经定义加载的类,需要使用重定义类(调用 Instrumentation.redefineClass)或重转换类(调用 Instrumentation.retransformClass)。
// 注册提供的转换器。如果 canRetransform 为 true,那么重转换类时也将调用该转换器
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
// 使用提供的类文件重定义提供的类集。新的类文件字节,通过 ClassDefinition 传入
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException
// 重转换提供的类集。对于每个添加时 canRetransform 设为 true 的转换器,在这些转换器中调用 transform 方法
void retransformClasses(Class<?>... classes)
throws UnmodifiableClassException
重定义类(redefineClass)从 JDK 1.5 开始支持,而重转换类(retransformClass)是 JDK 1.6 引入。相对来说,重转换类能力更强,当存在多个转换器时,重转换将由 transform 调用链组成,而重定义类无法组成调用链。重定义类能实现的逻辑,重转换类同样能完成,所以保留重定义类方法(Instrumentation.redefineClass)可能只是为了向后兼容。
agent class实现
public class Myattah {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
System.out.println("agentmainagentmainagentmainagentmain---agentmain");
inst.addTransformer(new MyMonitorTransformer2(), true);
// 关键点
Class<?> aClass = Class.forName("com.enmalvi.javaagent.JavaAgnet01.MainController");
if (inst.isModifiableClass(aClass)) {
inst.retransformClasses(aClass);
}
}
}
MyMonitorTransformer2
public class MyMonitorTransformer2 implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
System.out.println(className+"---------");
if (className.contains("MainController")){
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("com.enmalvi.javaagent.JavaAgnet01.MainController");
CtMethod convertToAbbr = clazz.getDeclaredMethod("test");
String methodBody = "return \"hello world【version2]\";";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
}
// 如果返回null则字节码不会被修改
return null;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
MainController
@RestController
public class MainController {
@GetMapping("agent")
public String test(){
System.out.println("hello world");
return "test";
}
}
这里关键的一点就是在我们的agentmain中手动retransform一下我们需要增强的类。
启动应用程序,并attach
这里我们需要获取目标JVM程序,并且进行attach加载我们的agent
public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().equals("com.enmalvi.javaagent.JavaAgentApplication")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar");
virtualMachine.detach();
}
}
}
这里有个点很重要,在执行的时候容易报错找不到com.sun.tools.attach的包,这里需要在pom的做修改,强行添加
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/lib/tools.jar</systemPath>
</dependency>
代理类的添加
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.enmalvi.javaagent.JavaAgnet05.MyAgent</Premain-Class>
<Agent-Class>com.enmalvi.javaagent.JavaAgnet01.Myattah</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
关于Agent注意事项
VirtualMachine该类允许我们通过给 attach() 方法传入一个jvm的pid(进程id),远程连接到jvm上 。
我们可以通过 loadAgent() 方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在 Class 加载前改变 Class 的字节码,也可以在 Class 加载后重新加载。
这里在修改类的时候特别需要注意,如果被修改的类同时被两个方法修改(Premain,Agent)如果修改那么就会出现错误,无法重定义类,不能同时作用于一个类。
(目前存疑,感觉是可以同时作用于一个类的,报错是–(class redefinition failed: attempted to delete a method)类重定义失败:试图删除方法)
java.lang.instrument 解析
instrument对于agent而言太重要了,所以我们先来了解一下instrument
写一个agent.jar可以分为以下几步
- 实现
ClassFileTransFormer类 —-定义class的字节码转化逻辑 - 实现
premain或者domain方法,调用Instrumentation接口,添加定义好的转换逻辑。 - 制作
agent.jar,在MAINIFEST.MF中配置agent.jar的入口信息。 - 执行
agent.jara.- 启动时加载 比如
java -javaagent:"proj-premain.jar=hello agent" -cp "target/classes/java/main" com.demo.App -
jvm运行时加载,获取目标JVM的对象,执行loadgent动作。
- 启动时加载 比如
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentJar, options);
jvm.detach();
JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口,这些接口可以供开发者扩展自行的逻辑。
ClassFileLoadHook
ClassFileLoadHook 事件。比如我想监听JVM加载某个类的事件,那么我们就可以实现一个回调函数赋给jvmtiEnv的回调方法集合里的ClassFileLoadHook(Class类加载事件),
那么当JVM进行类加载时就会触发回调函数,我们就可以在JVM加载类的时候做一些扩展操作。
比如上面提到的更改这个类的Class文件信息来增强这个类的方法。
agent函数 (Agent_OnLoad、Agent_OnAttach、Agent_OnUnload)
JVMTI运行时,一个JVMTIAgent对应一个jvmtiEnv或者是多个,JVMTIAgent是一个动态库,利用JVMTI暴露出来的接口来进行扩展。
主要有三个函数:
-
Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的 -
Agent_OnAttach函数(通过-agentlib加载vm参数中)Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后给对应的目标程序发送load命令来加载,则在加载过程中会调用Agent_OnAttach方法 -
Agent_OnUnload方法:在agent卸载时调用,要么主动下发卸载命令,要么jvm关闭是触发。
Instrument的实现
Instrument 就是一种 JVMTIAgent,它实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是在使用时,Instrument既可以在启动时加载,也可以再运行时加动态加载
premain启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式agentmain运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载
premain 启动时加载
-
Instrument agent启动时加载会实现Agent_OnLoad方法,具体实现逻辑如下:创建并初始化JPLISAgent - 监听
VMInit事件,在vm初始化完成之后执行下面逻辑- 创建
Instrumentation接口的实例,也就是InstrumentationImpl对象 - 监听
ClassFileLoadHook事件(类加载事件,通过set callback) - 调用
InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
- 创建
- 解析
MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的内容
agentmain 启动时加载
Instrument agent运行时加载会使用Agent_OnAttach方法,会通过JVM的attach机制来请求目标JVM加载对应的agent,过程如下
- 创建并初始化
JPLISAgent - 解析
javaagent里的MANIFEST.MF里的参数 - 创建
InstrumentationImpl对象 - 监听
ClassFileLoadHook事件 - 调用
InstrumentationImpl类的loadClassAndCallAgentmain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的agentmain-Class类的agentmain方法
ClassFileLoadHook回调实现
启动时加载和运行时加载都是监听同一个jvmti事件那就是ClassFileLoadHook,这个是类加载的事件,在读取类文件字节码之后回调用的,这样就可以对字节码进行修改操作。
在类加载时修改类的字节码
在JVM加载类文件时,执行回调,加载Instrument agent,创建Instrumentation接口的实例并且执行premain方法,premain方法中注册自定义的ClassFileTransformer来对字节码文件进行操作,这个就是在加载时进行字节码增强的过程。
修改内存已经存在的类
那么如果java类已经加载完成了,在运行的过程中需要进行字节码增强的时候还可以使用Instrumentation接口的redifineClasses方法,
通过执行该方法,在JVM中相当于是创建了一个VM_RedifineClasses的VM_Operation,此时会stop_the_world,具体的执行过程如下:
- 挨个遍历要批量重定义的
jvmtiClassDefinition - 然后读取新的字节码,如果有关注
ClassFileLoadHook事件的,还会走对应的transform来对新的字节码再做修改字节码解析好,创建一个klassOop - 对象对比新老类,并要求如下:
- 父类是同一个实现的接口数也要相同,并且是相同的接口类
- 访问符必须一致
- 字段数和字段名要一致
- 新增的方法必须是
private static/final的 - 可以删除修改方法
- 对新类做字节码校验
- 合并新老类的常量池
- 如果老类上有断点,那都清除掉
- 对老类做 JIT 去优化
- 对新老方法匹配的方法的
jmethodId做更新,将老的jmethodId更新到新的method上 - 新类的常量池的
holer指向老的类 - 将新类和老类的一些属性做交换,比如常量池,methods,内部类
- 初始化新的
vtable和itable - 交换
annotation的method、field、paramenter - 遍历所有当前类的子类,修改他们的
vtable及itable
sun.instrument 源码分析

这个是在premain和agentmain中如何使用instrument。注意三个方法
-
addTransformer(new Transformer, true)–jdk5 -
retransformClass(Target.class)–JDK6 -
redifineClasses(new ClassDefintion)— jdk5
这个三个方法就是用来执行转化字节码的
ClassFileTransFormer 转换器接口
default byte[] transform(ClassLoader loader,String className,Class<> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer)
参数解析
-
ClassLoader: 被加载类的loader -
className: 正在加载类的名称 -
classBeingRedefined: 目标类为A,需要被重定义,被重定义的类记为A1,那么classBeingRedefined: 就代表着A1 -
classFileBuffer当前类的定义

TransformerManager
instrument内拥有一个TransformerManager,包含了TransFormer数组。也就是说存在多个TransFormer时,TransFormer会形成一个链。上一个transformer对字节的修改,被传递到下一个transformer。
jdk6支持对本地方法的修改,所以会对分为4类,4类按顺序执行
- 不支持retransform的
TransFormer - 不支持retransform 的
TransFormerNative - 支持retransform的
TransFormer - 支持retransform
TransFormerNative

触发转换的动作
addTransformer(ClassFileTransformer transformer, boolean canRetransform);
这个函数的作用是,添加到transfomer数组中去。每当有一个类加载时会触发ClassFileLoadHook的回调,
这个回调会执行transfomer链,得到被加工过的字节码—保留在classFileBuffer类,最后被加载进方法区。
redefineClasses(ClassDefinition… definitions)
接受多个类的定义。再经过所transfomer数组(通常不添加)的加工,最后被加载进方法区。
这个方法是来源于jdk5
官方文档用来替换类内不存在任何引用(refrence)的类。往往修改一个类A也会导致相关联的B也要被修改。所以这里参数可以传入多个类。如果被redefine的方法,在栈stack中有了,此时再发生redefine。那么结果是,stack中方法继续执行老的定义,新的调用将会采用redefine后的方法。

retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
这个类在JDK6引入,和redefine类似但更强大。
可以对于已经加载的类(包括被redined的类)进行修改。
当类第一次加载时或者被redefined时,类的字节码byte[] ,会经过transfomer数组的加工。这个函数就是用来重放这个过程,不管内存中的类是否被装换过。
retransformClasses遵从以下步骤:
- 总是从类最初的定义开始(没有修改过的,原始的)
- 执行所有
canRetransform为true的transfomer,跳过canRetransform为false的transfomer。 - 加载进内存,和
redefine一样,存在栈里面的方法改变,对新方法的调用用新定义。
注意
- 如果所有的
transfomer为flase,那么他从最原始的类的定义出发,跳过所有transfomer,得到结果还是最原始的类。 - 如果内存类已经被修改了,那么会发生原始的类定义替换被修改过的类
总结
只要使用了addTransformer,那么每有类加载的时候,就会触发transfomer链加工。redefine接受一组类的定义,然后触发transfomer的加工,替换进内存里。retransformClasses和redefineClasses类似,从最原始的类定义,开始重放transfomer链接的加工(只重放所有被标记为canRetransform为true的transfomer),替换进内存。

JDK.attach 解析
从JDK6开始引入,除了Solaris平台的Sun JVM支持远程的Attach,在其他平台都只允许Attach到本地的JVM上。
用Java语言编写的工具使用此API附加到目标虚拟机(VM),并将其tool agent 加载到目标VM中。
什么是attach机制?
说简单点就是jvm提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作,比如说我们为了让另外一个jvm进程把线程dump出来,那么我们跑了一个jstack的进程,然后传了个pid的参数,告诉它要哪个进程进行线程dump,既然是两个进程,那肯定涉及到进程间通信,以及传输协议的定义,比如要执行什么操作,传了什么参数等
涉及进程间通信。启动Attach Listener进程和VM进程通信。交换数据的方式使用的是SOCKET。
功能
- 内存dump
- 线程dump
- 类信息统计(比如加载的类及大小以及实例个数等)
- 动态加载agent
- 动态设置vm flag(但是并不是所有的flag都可以设置的,因为有些flag是在jvm启动过程中使用的,是一次性的)
- 打印vm flag
- 获取系统属性等
代码结构



-
VirtualMachine是核心,通过操作它,来进行attach等 -
AttachPermission来负责权限控制功能 -
VirtualMachineDescriptorvm的模型,一个数据对象,包含pid或者vmname信息 -
AttachProvider各个JVM平台,自己负责对接实现。利用SPI给VirtualMachine的接口提供实现,截图中给出了OPENJDK14中默认对Hotspot的实现
模型类
AttachPermisson
用来做权限控制

| 权限目标名称 | 权限允许什么 | 允许此权限的风险 |
| attachVirtualMachine | 能够附加到另一个Java虚拟机并将代理加载到该VM。 | 这使攻击者可以控制目标VM,这可能会导致目标VM行为异常。 |
| createAttachProvider | 能够创建AttachProvider实例。 | 这使攻击者可以创建AttachProvider,该AttachProvider可以潜在地用于附加到其他Java虚拟机。 |
程序员通常不会直接创建AttachPermission对象。而是由安全策略代码基于读取安全策略文件来创建它们。
jvm在启动时可以设置一些权限,比如为了安全考虑禁止attach的行为。这个类就是在attach时被调用来检测是否能够attach。
VirtualMachineDescriptor
JVM信息的Bean。jvm有很多种,用来保存具体jvm的信息。
VirtualMachineDescriptor是用于描述Java虚拟机的容器类。它封装了标识目标虚拟机的标识符,以及AttachProvider在尝试附加到虚拟机时应使用的对它的引用。该标识符取决于实现,但是通常是每个Java虚拟机都在其自己的操作系统进程中运行的进程标识符(或pid)环境。
VirtualMachineDescriptor也有一个displayName。显示名称通常是工具可能显示给用户的人类可读字符串。例如,显示系统上运行的Java虚拟机列表的工具可能会使用显示名称而不是标识符。阿VirtualMachineDescriptor可以在没有被创建的显示名称。在这种情况下,标识符用作显示名称。
VirtualMachineDescriptor实例通常是通过调用该VirtualMachine.list() 方法来创建的。这将返回描述符的完整列表,以描述所有已安装的Java虚拟机attach providers。

AttachProvider
用来attach到jvm的接口。真正被使用的是各个厂家提供的jvm实现,是该类的子类。

核心类VirtualMacine
-
loadAgent
加载JPLTS agent,这个agent会加载agent.jar包 并执行agentMain或者premain方法 -
attach(attach)
依附到目标jvm -
detach离开

在此示例中,我们附加到由进程标识符标识的Java虚拟机2177。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。
VirtualMachine可安全地供多个并发线程使用。
一个Jvm的实现

loadAgent()
加载制作好的agent.jar包,并执行premain domain方法。
加载 JPLTS agent,这个agent会加载agent.jar包 并执行 agentMain或者premain方法
源码就不贴了,这里整个类的执行过程。可以总结为和jvm建立连接后,向jvm发送命令,传输agent.jar的path参数,并触发其中的premain或者domain方法

loadAgentPath()
用来加载 native方法库,即用他语言写的库。可以看出 load cmd 会有字段来标识是不是本地方法

attach()
这个方法的实现最终是在provider中实现,最终的结果是返回一个VirtualMachine对象。可以和jvm通信,让jvm执行指令。

流程
- 检查权限
- 测试是否可以
attach - 成功
attach - 以上检查都没问题,返回
new VirtualMachineImpl(this, vmid);

不同平台实现不同

detach()
hotspot的实现比较直接,直接将fd文件设置为null

监控JVM
如果对JMXBean不陌生的就可以知道,这些bean是用来获取JVM运行信息:堆内存,线程状态,gc次数等。VirtualMacine提供这样的函数,向jvm发送指令,暴露端口。通过这个端口就可以获取JVM运行信息。
startManagementAgent(Properties agentProperties)

startLocalManagementAgent()仅暴露本地端口,最终是从本地fd读取jvm信息。

AgentBuilder 构建
前面的代码几乎都是利用javassist来实现的,使用javassist虽说能实现我们的目的但是不够便利,下面我们尝试用Byte Buddy,便捷地创建 Java Agent
关于 Byte Buddy 的使用之前整理过基本用法
入门例子
JVM启动前替换
操作监控方法字节码
MyAgent
public class MyAgent {
//JVM 首先尝试在代理类上调用以下方法
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("this is my agent:" + agentArgs);
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
return builder
.method(ElementMatchers.any()) // 拦截任意方法
.intercept(MethodDelegation.to(MethodCostTime.class)); // 委托
};
AgentBuilder.Listener listener = new AgentBuilder.Listener() {
@Override
public void onDiscovery(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {
}
@Override
public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b, DynamicType dynamicType) {
}
@Override
public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b) {
}
@Override
public void onError(String s, ClassLoader classLoader, JavaModule javaModule, boolean b, Throwable throwable) {
}
@Override
public void onComplete(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {
}
};
new AgentBuilder
.Default()
.type(ElementMatchers.nameStartsWith("com.enmalvi.javaagent.ApiTest2")) // 指定需要拦截的类
.transform(transformer)
.with(listener)
.installOn(inst);
}
//如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
public static void premain(String agentArgs) {
}
}
MethodCostTime
public class MethodCostTime {
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
try {
// 原有函数执行
return callable.call();
} finally {
System.out.println(method + " 方法耗时: " + (System.currentTimeMillis() - start) + "ms");
}
}
}
测试方法
public class ApiTest2 {
public static void main(String[] args) throws InterruptedException {
ApiTest2 apiTest = new ApiTest2();
apiTest.echoHi2();
}
private void echoHi2() throws InterruptedException {
System.out.println("hi agent");
Thread.sleep((long) (Math.random() * 500));
}
}
pom修改
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.enmalvi.javaagent.JavaAgnet02.MyAgent</Premain-Class>
<Agent-Class>com.enmalvi.javaagent.JavaAgnet01.Myattah</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
添加启动包
-javaagent:/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar
结果
this is my agent:null
hi agent
private void com.enmalvi.javaagent.ApiTest2.echoHi2() throws java.lang.InterruptedException 方法耗时: 493ms
public static void com.enmalvi.javaagent.ApiTest2.main(java.lang.String[]) throws java.lang.InterruptedException 方法耗时: 493ms
ThreadLocal链路追踪
当业务程序代码在线上运行时,实例A、实例B、实例C,他们直接可能从上到下依次调用,为了能很好的监控程序的调用链路,我们需要对调用链路进行追踪监控。实例的外部可能是通过RPC、HTTP、SOCKET、WEBSERVICE等方式进行调用,内部是方法逻辑依次执行。外部例如http可以通过在头部写入追踪ID进行监控,内部使用threadlocal进行保存上下文关系。
ThreadLocal变量特殊的地方在于:对变量值的任何操作实际都是对这个变量在线程中的一份copy进行操作,不会影响另外一个线程中同一个ThreadLocal变量的值。
TrackContext
public class TrackContext {
private static final ThreadLocal<String> trackLocal = new ThreadLocal<String>();
public static void clear(){
trackLocal.remove();
}
public static String getLinkId(){
return trackLocal.get();
}
public static void setLinkId(String linkId){
trackLocal.set(linkId);
}
}
TrackManager
/**
* 追踪管控
*/
public class TrackManager {
private static final ThreadLocal<Stack<String>> track = new ThreadLocal<Stack<String>>();
private static String createSpan() {
Stack<String> stack = track.get();
if (stack == null) {
stack = new Stack<>();
track.set(stack);
}
String linkId;
if (stack.isEmpty()) {
linkId = TrackContext.getLinkId();
if (linkId == null) {
linkId = "nvl";
TrackContext.setLinkId(linkId);
}
} else {
linkId = stack.peek();
TrackContext.setLinkId(linkId);
}
return linkId;
}
public static String createEntrySpan() {
String span = createSpan();
Stack<String> stack = track.get();
stack.push(span);
return span;
}
public static String getExitSpan() {
Stack<String> stack = track.get();
if (stack == null || stack.isEmpty()) {
TrackContext.clear();
return null;
}
return stack.pop();
}
public static String getCurrentSpan() {
Stack<String> stack = track.get();
if (stack == null || stack.isEmpty()) {
return null;
}
return stack.peek();
}
}
MyAdvice
public class MyAdvice {
@Advice.OnMethodEnter()
public static void enter(@Advice.Origin("#t") String className, @Advice.Origin("#m") String methodName) {
String linkId = TrackManager.getCurrentSpan();
if (null == linkId) {
linkId = UUID.randomUUID().toString();
TrackContext.setLinkId(linkId);
}
String entrySpan = TrackManager.createEntrySpan();
System.out.println("链路追踪:" + entrySpan + " " + className + "." + methodName);
}
@Advice.OnMethodExit()
public static void exit(@Advice.Origin("#t") String className, @Advice.Origin("#m") String methodName) {
TrackManager.getExitSpan();
}
}
@Advice.OnMethodEnter
被标注此注解的方法会被织入到被bytebudy增强的方法前调用
-
Advice.Argument- 用来获取 被执行对象的field或者参数
-
Advice.This- 是当前advice对象的载体,标注之后,方法内部可以获取目前advice对象。
@Advice.OnMethodExit
被标注此注解的方法会被织入到被bytebudy增强的方法后调用
-
Advice.Argument- 用来获取 被执行对象的field或者参数
-
Advice.This- 是当前advice对象的载体,标注之后,方法内部可以获取目前advice对象。
-
Advice.Return- 给一个参数标识
Advice.Return后可以接收返回结果。
- 给一个参数标识
- Advice.Thrown
- 给一个参数标识
Advice.Thrown后可以接收抛出的异常。如果方法引发异常,则该方法将提前终止。如果由Advice.OnMethodEnter注释的方法引发异常,则不会调用由Advice.OnMethodExit方法注释的方法。如果检测的方法引发异常,则仅当Advice.OnMethodExit.onThrowable()属性设置为true(默认值)时,才调用由Advice.OnMethodExit注释的方法。
- 给一个参数标识
补充内容
这里Advice开头的都是net.bytebuddy.asm包下的,比如@Advice.Origin("#t")注解,而@Origin()注解是net.bytebuddy.implementation.bind.annotation包下的,是不一样的。

关于Advice类是这么描述的
建议包装器复制要在匹配方法之前和/或之后执行的蓝图方法的代码。为此,类的static方法由Advice.OnMethodEnter和/或Advice.OnMethodExit并提供给此类的实例。
也就是说这个是专门用来做前置后置处理的。
使用时也是用不一样的方式来调用
builder.visit(Advice.to(plugin.adviceClass()).on(point.buildMethodsMatcher()));
其实visit也是特殊的方法与Advice是匹配的

得出的结论是Advice是字节码的生成,方法的前置或后置调用,但是原本的intercept是方法的拦截调用生成。
反正就是如果是在方法的前置或者后置那就使用Advice相关的内容,如果是方法的运行时拦截处理,或者是代码生成都是可以用intercept相关的内容。

这两个方法是可以一起使用的,比如
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
builder=builder.method(ElementMatchers.any()).intercept(MethodDelegation.to(MethodCostTime.class));
builder = builder.visit(Advice.to(plugin.adviceClass()).on(point.buildMethodsMatcher()));
return builder;
};
后面回归正题,上面只是补充
MyAgent
public class MyAgent {
//JVM 首先尝试在代理类上调用以下方法
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("基于javaagent链路追踪");
AgentBuilder agentBuilder = new AgentBuilder.Default();
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
builder = builder.visit(
Advice.to(MyAdvice.class)
.on(ElementMatchers.isMethod()
.and(ElementMatchers.any()).and(ElementMatchers.not(ElementMatchers.nameStartsWith("main")))));
return builder;
};
agentBuilder = agentBuilder.type(ElementMatchers.nameStartsWith("com.enmalvi.javaagent.ApiTest04")).transform(transformer);
//监听
AgentBuilder.Listener listener = new AgentBuilder.Listener() {
@Override
public void onDiscovery(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {
}
@Override
public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b, DynamicType dynamicType) {
System.out.println("onTransformation:" + typeDescription);
}
@Override
public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b) {
}
@Override
public void onError(String s, ClassLoader classLoader, JavaModule javaModule, boolean b, Throwable throwable) {
}
@Override
public void onComplete(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {
}
};
agentBuilder.with(listener).installOn(inst);
}
//如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
public static void premain(String agentArgs) {
}
}
测试方法
public class ApiTest04 {
public static void main(String[] args) {
//线程一
new Thread(() -> new ApiTest04().http_lt1()).start();
//线程二
new Thread(() -> {
new ApiTest04().http_lt1();
}).start();
}
public void http_lt1() {
System.out.println("测试结果:hi1");
http_lt2();
}
public void http_lt2() {
System.out.println("测试结果:hi2");
http_lt3();
}
public void http_lt3() {
System.out.println("测试结果:hi3");
}
}
依旧需要修改pom添加启动包
结果
基于javaagent链路追踪
onTransformation:class com.enmalvi.javaagent.ApiTest04
链路追踪:f7dce089-4591-4924-8130-bfe6781d582d com.enmalvi.javaagent.ApiTest04.http_lt1
测试结果:hi1
链路追踪:5b336087-dca9-4eb3-841a-806c19fbf1e9 com.enmalvi.javaagent.ApiTest04.http_lt1
测试结果:hi1
链路追踪:f7dce089-4591-4924-8130-bfe6781d582d com.enmalvi.javaagent.ApiTest04.http_lt2
测试结果:hi2
链路追踪:5b336087-dca9-4eb3-841a-806c19fbf1e9 com.enmalvi.javaagent.ApiTest04.http_lt2
测试结果:hi2
链路追踪:5b336087-dca9-4eb3-841a-806c19fbf1e9 com.enmalvi.javaagent.ApiTest04.http_lt3
链路追踪:f7dce089-4591-4924-8130-bfe6781d582d com.enmalvi.javaagent.ApiTest04.http_lt3
测试结果:hi3
测试结果:hi3
JVM启动后替换实现
Byte Buddy 对 Attach API 作了封装,屏蔽了对 tools.jar 的加载,可以直接使用 ByteBuddyAgent类:
public static void main(String[] args) {
if (args.length < 2) {
System.err.println("Usage: java com.demo.AgentLoader <pid/name> <agent> [options]");
System.exit(0);
}
String jvmPid = args[0];
String agentJar = args[1];
String options = args.length > 2 ? args[2] : null;
for (VirtualMachineDescriptor jvm : VirtualMachine.list()) {
if (jvm.displayName().contains(args[0])) {
jvmPid = jvm.id();
break;
}
}
ByteBuddyAgent.attach(new File(agentJar), jvmPid, options);
}
按照上面的内容还可以写成
public static void main(String[] args) {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().equals("com.enmalvi.javaagent.JavaAgentApplication")) {
ByteBuddyAgent.attach(new File("/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar"), "2963");
}
}
}
其中 com.enmalvi.javaagent.JavaAgentApplication 这个是 SpringBoot 启动后的虚拟机的名字,可以通过它来判断是否要替换该JVM的类,和直接使用JVM的PID是一样的。
Byte Buddy 实现 agentmain:
public static void agentmain(String agentArgs, Instrumentation inst) {
new AgentBuilder.Default()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.disableClassFormatChanges()
.type(ElementMatchers.named("com.demo.App"))
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule module) {
return builder.method(ElementMatchers.named("getGreeting"))
.intercept(FixedValue.value(agentArgs));
}
}).installOn(inst);
}
实现attach关键还是这个两个
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.disableClassFormatChanges()
Byte Buddy 实现启动后动态加载 agent,官方提供了 Advice API 。
Advice API 实现原理上是,在被拦截方法内部的开始和结尾添加代码,如下右图所示。这样只更改了方法体,不更改方法签名,也没添加额外的方法,符合重定义类(redefineClass)和重转换类(retransformClass)的限制。

AgentBuilder的详解
创建一个agent
定义transformer,每有一个类加载时,触发transformer逻辑,对类进行匹配和修改。
bytebuddy提供了Agentbuilder,封装了一系列API来新建一个 agent。
示例
假设我们定义了一个注解ToString,我们匹配所有标注@ToString的类,修改toString方法,让其返回"transformed"。
class ToStringAgent {
public static void premain(String arguments, Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(isAnnotatedWith(ToString.class))
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder transform(DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classloader) {
return builder.method(named("toString"))
.intercept(FixedValue.value("transformed"));
}
}).installOn(instrumentation);
}
}
-
type接受 一个ElementMatcher匹配ToString注解 -
transform接受AgentBuilder.Transformer来描述修改逻辑
// 这里匹配类里面的`toString`方法,使用`intercept`修改返回值
public DynamicType.Builder transform(DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classloader) {
return builder.method(named("toString"))
.intercept(FixedValue.value("transformed"));
}
-
installOn,将修改,应用到instrumentation中。
AgentBuilder 类结构
嵌套的内部类
| order | Modifier and Type | Interface | Description |
| 0 | static interface | AgentBuilder.CircularityLock | 一个循环锁,阻止ClassFileLocator被提前使用。发生在,一个 transformation逻辑可能会导致另外的类被加载。如果不避免这样的循环依赖,就会抛出ClassCircularityError错误,导致类加载失败 |
| 1 | static interface | AgentBuilder.ClassFileBufferStrategy | 关于class定义的字节缓存buffer如何被使用的策略 |
| 2 | static class | AgentBuilder.Default |
AgentBuilder的默认实现。默认情况下,byte buddy 无视任何被bootstrap loader加载的类和合成类。1. Self-injection 和 rebase 模式开启 2. 为了避免class的格式发生改变, AgentBuilder.disableClassFormatChanges()3. 所有的类型解析都是 PoolStrategy.Default#FAST,忽略所有的debug信息 |
| 3 | static interface | AgentBuilder.DescriptionStrategy | 策略,描述当转化和定义一个类时,如何解决TypeDescription定义,这个解决指的是寻找+处理 |
| 4 | static interface | AgentBuilder.FallbackStrategy | 失败策略,允许万一失败时,可以再次进行transformation或者redefine/retransformation。如果这样做了,可能就是会使用typepool,而不是一个已经加载的type description–相当于某个class的备份。如果class loader不能找到所有的依赖的类,会抛出异常。使用 typepool,由于时懒加载,所以会规避异常,直到被使用 |
| 5 | static interface | AgentBuilder.Identified | 用来描述AgentBuilder处理哪几种mathcer,这个就是提供给mather一个标识,便于筛选 |
| 6 | static interface | AgentBuilder.Ignored | 允许声明,忽略那些具体的方法 |
| 7 | static interface | AgentBuilder.InitializationStrategy | 初始化策略,决定LoadedTypeInitializer是如何加载辅助类。agentbuilder不能重用TypeResolutionStrategy策略,是因为Javaagents不能获取到一个被transformation过,已经加载的类。所以所以不同的初始化加载策略更有效 |
| 8 | static interface | AgentBuilder.InjectionStrategy | 将辅助类注入classloader的策略 |
| 9 | static interface | AgentBuilder.InstallationListener | 监听器,安装和重置class file transformer事件会唤醒这个监听器 |
| 10 | static class | AgentBuilder.LambdaInstrumentationStrategy | lambda风格的语法特性开启 |
| 11 | static interface | AgentBuilder.Listener | 一个an instrumentation 运行时会触发一堆事件,这个监听器是用来接受这些事件的 |
| 12 | static interface | AgentBuilder.LocationStrategy | 修改一个类时,描述如何去创建ClassFileLocator的策略 |
| 13 | static interface | AgentBuilder.Matchable<T extends AgentBuilder.Matchable> | 继承了mathcer的一个抽象实现,链式的结构 |
| 14 | static interface | AgentBuilder.PoolStrategy | 描述AgentBuilder如何寻找和加载类TypeDescription的策略 |
| 15 | static interface | AgentBuilder.RawMatcher | 一个matcher,用来匹配类并决定AgentBuilder.Transformer在ClassFileTransformer运行期间是否被使用 |
| 16 | static interface | AgentBuilder.RedefinitionListenable | 允许在redefine的过程中注册一堆监听器 |
| 17 | static class | AgentBuilder.RedefinitionStrategy |
redefinition策略,描述agent如何控制已经被agent 加载到内存里面的类 |
| 18 | static interface | AgentBuilder.Transformer | 应用DynamicType(定义的类修改),将DynamicType,对接到ClassFileTransformer
|
| 19 | static interface | AgentBuilder.TransformerDecorator |
AgentBuilder.Transformer的装饰器 |
| 20 | static interface | AgentBuilder.TypeStrategy | 描述创建一个要被修改的类型,如何创建的方式 |
方法
相当于API的翻译,会有简单解释。
- 加载过 往往意味值
loaded,就是指classloader已经加载过目标类。bytebuddy通常就是感知类的加载,并且返回一个加工过的类。如此完成字节码修改的作用。
| order | return type | method | 描述 |
| 0 | AgentBuilder | with(ByteBuddy byteBuddy) |
ByteBuddy是用来创建DynamicType的。这里就是接受并生成一个AgentBuilder |
| 1 | AgentBuilder | with(Listener listener) | 注册监听器,创建agent的时候,会唤醒这个Listener。注意的是可以注册多个,如果早已经注册,也会重复唤醒,不会只唤醒一个 |
| 2 | AgentBuilder | with(CircularityLock circularityLock) | 一个循环锁,被用于被执行的代码会加载新的类时,会获取这个锁。 当锁被获取时,任何 classfiletransformer都不能transforming 任何类,默认,所有被创建的agent都使用一个CircularityLock,避免造成一个ClassCircularityError。就是避免被执行的代码加载新类,同时其他agent也在转化这个类,造成一个循环依赖的一个锁 |
| 3 | AgentBuilder | with(PoolStrategy poolStrategy) | 加载的类时的策略 |
| 4 | AgentBuilder | with(LocationStrategy locationStrategy) | 寻找类的位置,利用 class name 加载类二进制数据的策略 |
| 5 | AgentBuilder | with(TypeStrategy typeStrategy) | type 应该如何被transformed,eg, rebased或者redefined |
| 6 | AgentBuilder | with(InitializationStrategy initializationStrategy) | 生成一个类时的初始化策略,一个初始化策略是在类被加载后,启动时生效。初始化行为,必须在transformation行为之后,因为java agent 是在加载一个类之前生效。默认,被注入到对象的初始化逻辑,需要查询一个全局对象来找到所有需要被注入到目标类型的对象 |
| 7 | RedefinitionListenable.WithoutBatchStrategy | with(RedefinitionStrategy redefinitionStrategy) | 明确早已经被修改过且加被加载过类的优先级,目的是来安转transformer会用到。注意定一个redefinition strategy会重置之前的一些列的策略。重要的是,绝大多数JVM不支持被加载过的类,类结构被重新修改,因此默认打开 AgentBuilder#disableClassFormatChanges()
|
| 8 | AgentBuilder | with(LambdaInstrumentationStrategy lambdaInstrumentationStrategy) | Lambda表达式特性,忽虑掉 |
| 9 | AgentBuilder | with(DescriptionStrategy descriptionStrategy)) | 定义被加载类的,解决(resolving)策略。 |
| 10 | AgentBuilder | with(FallbackStrategy fallbackStrategy) | 失败策略,比如转换失败时,允许重试一次 |
| 11 | AgentBuilder | with(ClassFileBufferStrategy classFileBufferStrategy) | 类定义的buffer如何被使用 |
| 12 | AgentBuilder | with(InstallationListener installationListener) | agent安转时,会有安装事件,这个时间会唤醒这个监听器。这个监听器,只有在classfiletransformer安装时会被触发。classfiletransformer的安装就是agent builder通过创建的ResettableClassFileTransformer执行installation methods 和uninstalled |
| 13 | AgentBuilder | with(InjectionStrategy injectionStrategy) | 辅助类注入到classloader的策略 |
| 14 | AgentBuilder | enableNativeMethodPrefix(String prefix) | 给被修改的方法,添加本地方法前缀。注意这个前缀也能给非本地方法用。当Instrumentation安装agent是,也能用这个前缀来注册 |
| 15 | AgentBuilder | disableNativeMethodPrefix() | 关闭一个本地方法的前缀 |
| 16 | AgentBuilder | disableClassFormatChanges() | 禁止所有对class file 的改变,当时用这个策略时,就不存在使用rebase的可能—被修改的方法会完全被替代,而不是被重名。可去搜索rebase和redefine的区别。还有就是在生成类型是,加载初始化动作。 这个行为和设置 InitializationStrategy.NoOp和TypeStrategy.Default#REDEFINE_FROZEN是等价的。也等同于配置ByteBuddy为Implementation.Context.Disabled。使用这个策略是让bute buddy 穿件一个 冻结的 instrumented类型和排除调所有的配置行为 |
| 17 | AgentBuilder | assureReadEdgeTo(Instrumentation instrumentation, Class<?>… type) | module ,jdk9的概念,不用管 |
| 18 | AgentBuilder | assureReadEdgeTo(Instrumentation instrumentation, JavaModule… module) | |
| 19 | AgentBuilder | assureReadEdgeTo(Instrumentation instrumentation, Collection<? extends JavaModule> modules) | |
| 20 | AgentBuilder | assureReadEdgeFromAndTo(Instrumentation instrumentation, Class<?>… type) | |
| 21 | AgentBuilder | assureReadEdgeFromAndTo(Instrumentation instrumentation, JavaModule… module) | |
| 22 | AgentBuilder | assureReadEdgeFromAndTo(Instrumentation instrumentation, Collection<? extends JavaModule> modules) | 都是moudle权限控制相关的 |
| 23 | Identified.Narrowabler | type(ElementMatcher<? super TypeDescription> typeMatcher) | 每一个类被加载时会触发一个事件,被agent感知到。这个方法就是用来配一个被加载的类型,目的是应用AgentBuilder.Transformer的修改,在这个类型被加载之前。如果有几个matcher都命中一个类型,那么最后一个生效。 如果这个matcher是链式的,即还有下一个matcher,那么 matcher执行的顺序就是注册的顺序,后面matcher对应的transformations会覆盖前面的。如果不想被覆盖可以注册为 terminal最后一个Identified.Extendable#asTerminalTransformation(),这样就不有有下一个matcher被使用。注意: AgentBuilder#ignore(ElementMatcher)会在mather之前被应用,来排除忽略的类。 |
| 24 | Identified.Narrowabler | type(ElementMatcher<? super TypeDescription> typeMatcher, ElementMatcher<? super ClassLoader> classLoaderMatcher) | 唯一的不同是添加了classLoaderMatcher参数,这个classLoader被用来加载目标类的loader |
| 25 | Identified.Narrowabler | Identified.Narrowable type(ElementMatcher typeMatcher,ElementMatcher classLoaderMatcher,ElementMatcher moduleMatcher); | moduleMatcher jdk9的语法,只是多了层判断 |
| 26 | Identified.Narrowabler | type(RawMatcher matcher); | 文档只说用来决定Transformer是否被使用 |
| 27 | Ignored | ignore(ElementMatcher<? super TypeDescription> typeMatcher); | 无视方法的matcher |
| 28 | Ignored | ignore(ElementMatcher<? super TypeDescription> typeMatcher, ElementMatcher<? super ClassLoader> classLoaderMatcher) | |
| 29 | Ignored | ignore(RawMatcher rawMatcher) | |
| 30 | ClassFileTransformer | makeRaw() |
ClassFileTransformer是jdk instrument包的类。这个方法是创建一个原生的这样的类。当时用原生的ClassFileTransformer,InstallationListener回调就不会生效,而且RedefinitionStrategy的策略也不会应用到当前加载的类中 |
| 31 | ResettableClassFileTransformer | installOn(Instrumentation instrumentation) | 单纯使用instrument时,往往是在premain或者agentmain函数里,执行instrumentation.addTransformer(ClassFileTransformer),installOn 就是将创建一个ResettableClassFileTransformer添加进instrumentation。如果 retransformation是打开的,那么retransformed时让ClassFileTransformer再次生效。意思是ClassFileTransformer是链式的,每个ClassFileTransformer被添加的时候有个canRetransformed属性。retransformed,就是修改已经加载内存里面的类,执行时会再次触发ClassFileTransformer,这时只会触发canRetransformed为true的ClassFileTransformer
|
| 32 | ResettableClassFileTransformer | ResettableClassFileTransformer installOn(Instrumentation instrumentation, TransformerDecorator transformerDecorator); | installOn时接受额外的Transformer
|
| 33 | ResettableClassFileTransformer | installOnByteBuddyAgent() |
ByteBuddyAgent是bytebutty对jdk.attach和agentmain的一个封装。根据目标jvm实例的pid,在运行中attach上去,发出加载agent.jar包命令 |
| 34 | ResettableClassFileTransformer | installOnByteBuddyAgent(TransformerDecorator transformerDecorator); |
