阅读完需:约 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-Classes
true表示能重定义此代理所需的类,默认值为 false(可选)该属性用来指定该agent是否针对redefineClass
产生作用 -
Can-Retransform-Classes
true表示能重转换此代理所需的类,默认值为 false (可选)该属性用来指定该agent是否针对retransformClass
产生作用 -
Can-Set-Native-Method-Prefix
true表示能设置此代理所需的本机方法前缀,默认值为 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
来负责权限控制功能 -
VirtualMachineDescriptor
vm的模型,一个数据对象,包含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); |