参考文章:

  1. Title Unavailable | Site Unreachable
  2. blog.51cto.com/panyujie/9668824?
  3. Site Unreachable
  4. Byte Buddy入门:Java运行时代码生成神器Byte Buddy是一个操作字节码的Java类库,运行期间生成、修 - 掘金

附录

代码仓库:GitHub - baiyz0825/java-agent-demo: 示范演示java-agent的基本demo和遇到的问题解决
PPT:[[[Java Agent技术深度解析与实战应用.pptx]]]

基本概念

本质上就是一种Java程序,目的是希望在Java程序启动时或者运行时候动态修改、监测Class字节码文件,方便调试监控等。

Java Agent 是一种基于 java.lang.instrument 包的技术,可以在 JVM 启动时或运行时加载代理代码,修改目标类的字节码。它主要用于 AOP(面向切面编程)、性能监控、日志收集、测试工具、应用安全等场景。

历史起源

JVMTI

JVMTI(JVM Tool Interface)是Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。

Instrumentation

Instrumentation接口设计初衷是为了收集Java程序运行时的数据,用于监控运行程序状态,记录日志,分析代码用的。

  • java.lang.instrument (Java Platform SE 8 )
    在Java SE 5之前,要实现一个Agent只能通过编写Native代码来实现。
    从Java SE 5开始,可以使用Java的Instrumentation接口(java.lang.instrument包)来编写Agent。
    从而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到Instrumentation的目的。并且从Java SE 6开始,可以向native method插桩。

本质上:它们的工作都是借助JVMTI来进行完成

/**
* 该类提供了插桩Java编程语言代码所需的服务。
* 插桩是指向方法添加字节码,目的是收集可供工具使用的数据。
* 由于这些更改纯粹是附加性的,因此这些工具不会修改应用程序的状态或行为。
* 此类良性工具的示例包括监控代理、性能分析器、覆盖率分析器和事件记录器。
*
* <P>
* 有两种方式可以获取<code>Instrumentation</code>接口的实例:
*
* <ol>
* <li><p> 当JVM以指示代理类的方式启动时。在这种情况下,<code>Instrumentation</code>实例
* 会传递给代理类的<code>premain</code>方法。</p></li>
* <li><p> 当JVM提供在JVM启动后某个时间启动代理的机制时。在这种情况下,<code>Instrumentation</code>
* 实例会传递给代理代码的<code>agentmain</code>方法。</p> </li>
* </ol>
* <p>
* 这些机制在{@linkplain java.lang.instrument package specification}中有描述。
* <p>
* 一旦代理获取了<code>Instrumentation</code>实例,
* 代理可以随时调用该实例上的方法。
*
* @apiNote 此接口不打算在java.instrument模块之外实现。
*
* @since 1.5
*/

  1. addTransformer(ClassFileTransformer transformer, boolean canRetransform
    • 用途: 注册类文件转换
    • 功能: 将转换器添加到JVM中,所有未来的类定义都会经过该转换器处理
    • 参数:
      • transformer: 类文件转换器
      • canRetransform: 是否支持重转换
  2. addTransformer(ClassFileTransformer transformer)
    • 用途: 注册类文件转换器(简化版)
    • 功能: 等同于 addTransformer(transformer, false)
  3. removeTransformer(ClassFileTransformer transformer)
    • 用途: 注销类文件转换器
    • 功能: 从JVM中移除指定的转换器
  4. isRetransformClassesSupported()
    • 用途: 检查是否支持类重转换
    • 功能: 返回当前JVM配置是否支持对已加载类进行重转换
  5. retransformClasses(Class<?>... classes)
    • 用途: 重转换指定的类
    • 功能: 对已加载的类重新运行转换过程,应用新的字节码修改
  6. isRedefineClassesSupported()
    • 用途: 检查是否支持类重定义
    • 功能: 返回当前JVM配置是否支持类重定义功能
  7. redefineClasses(ClassDefinition... definitions)
    • 用途: 重定义指定的类
    • 功能: 使用新的类文件字节码替换类的定义,用于热修复等场景
  8. isModifiableClass(Class<?> theClass)
    • 用途: 检查类是否可修改
    • 功能: 判断指定的类是否可以通过重转换或重定义进行修改
  9. getAllLoadedClasses()
    • 用途: 获取所有已加载的类
    • 功能: 返回JVM中当前加载的所有类和接口的数组
  10. getInitiatedClasses(ClassLoader loader)
    • 用途: 获取类加载器初始化的类
    • 功能: 返回指定类加载器能够通过名称找到的所有类
  11. getObjectSize(Object objectToSize)
    • 用途: 估算对象大小
    • 功能: 返回对象占用的存储空间近似值(实现相关)
  12. appendToBootstrapClassLoaderSearch(JarFile jarfile)
    • 用途: 添加到引导类加载器搜索路径
    • 功能: 将JAR文件添加到引导类加载器的搜索路径中
  13. appendToSystemClassLoaderSearch(JarFile jarfile)
    • 用途: 添加到系统类加载器搜索路径
    • 功能: 将JAR文件添加到系统类加载器的搜索路径中
  14. isNativeMethodPrefixSupported()
    • 用途: 检查是否支持本地方法前缀设置
    • 功能: 返回当前JVM是否支持为本地方法设置前缀
  15. setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)
    • 用途: 设置本地方法前缀
    • 功能: 为指定的转换器设置本地方法前缀,用于包装本地方法
  16. redefineModule() (Java 9+)
    • 用途: 重定义模块
    • 功能: 扩展模块的读取集、导出包、开放包或服务提供
  17. isModifiableModule(Module module) (Java 9+)
    • 用途: 检查模块是否可修改
    • 功能: 判断指定的模块是否可以通过重定义进行修改

运行模式

主程序可以指定的agent的数量是没有任何限制的,但是会根据指定的先后顺序依次执行各个agent的逻辑。但是agent抛出异常,会导致主程序的启动失败

Java Agent 可以在两种模式下运行:

原理

静态加载过程

agent 中的 class 由 system calss loader(默认AppClassLoader) 加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI(JVMTI的内容将在后面补充),在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发ClassFileTransformer.transform(),最终修改 class 的字节码。

![[Agent基本原理 2025-12-01 19.59.25.excalidraw]]
Instrument agent启动时加载会实现Agent_OnLoad方法,具体实现逻辑如下:

  1. 创建并初始化JPLISAgent
  2. 监听VMInit事件,在vm初始化完成之后执行下面逻辑
    1. 创建Instrumentation接口的实例,也就是InstrumentationImpl对象
    2. 监听ClassFileLoadHook事件(类加载事件)
    3. 调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
  3. 解析MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的内容

应用场景

Clipboard_Screenshot_1764591517.png

  1. 性能监控和调优
    • 通过在运行时收集类加载信息、方法执行时间、内存使用情况等,可以用于监控应用的性能。
    • 实现更深入的性能分析工具,如探查器(Profiler),来定位性能瓶颈。
  2. 日志增强
    • 在不修改源代码的情况下,为已有系统添加或修改日志记录逻辑。
    • 可以动态地向特定的方法中插入日志输出语句,帮助调试和跟踪问题。
  3. 字节码操作和AOP(面向切面编程)
    • 动态地在方法执行前后插入额外的逻辑,实现横切关注点(cross-cutting concerns),比如事务管理、安全检查等。
  4. 热修复(Hotfixing)
    • 在生产环境中直接修改正在运行的应用程序的行为,而不需要重新部署或重启服务。
    • 通过修改字节码来临时解决紧急的问题,等待正式修复版本上线。
  5. 依赖注入框架的支持
    • 部分依赖注入框架可能利用 Java Agent 来实现在运行时自动配置和管理 Bean 的功能。
  6. 安全性加强
    • 实施额外的安全措施,例如加密敏感数据、防止反序列化攻击等。
    • 监控并限制某些潜在危险的操作,提升系统的安全性。
  7. 资源管理
    • 自动管理资源释放,确保没有泄露发生,比如数据库连接、文件句柄等。
  8. 测试支持
    • 提供了在测试阶段对应用程序内部状态进行检查的能力,有助于编写更加健壮的测试用例。
    • 模拟异常条件,验证系统的容错能力。

Clipboard_Screenshot_1764591733.png
**

常见Java Agent框架

框架 类型 是否可直接用于 Java Agent 是否需要 -javaagent 特点 典型用途
ByteBuddy 字节码操作库(高级) ✅ 是(推荐) ✅ 是(或通过 attach) API 简洁、类型安全、性能好、支持 agent 构建器 APM(如 SkyWalking)、Mock 框架、动态代理
ASM 字节码操作库(底层) ✅ 是 ✅ 是 性能极高、控制精细、学习曲线陡 编译器、框架底层(如 Javassist、CGLIB 底层)
Javassist 字节码操作库(高层) ✅ 是 ✅ 是 用 Java 源码字符串操作字节码,易上手 快速原型、调试工具(如 Arthas)、教学
Spring Instrument Spring 提供的 Agent 工具 ⚠️ 间接使用 ✅ 是(仅用于 LTW) 专为 Spring Load-Time Weaving 设计 在 Spring 中启用 AspectJ LTW

这里用于编写agent比较多的是ByteBuddy api简单

基本示例

使用 javaassitant

demo: java-agent-demo/bytebuddy-agent at main · baiyz0825/java-agent-demo · GitHub

使用 bytebuddy

demo:java-agent-demo/javassist-agent at main · baiyz0825/java-agent-demo · GitHub

类加载原理

[[Jvm类加载原理]]

潜在问题

依赖冲突

agent中使用版本和实际业务代码不同

  1. 同包API不兼容: 类加载顺序有关,在使用agent的时候一般是默认使用appClassLoader进行加载的
  2. classNotFound:应用程序中不包含这个agent的依赖,增强代码是Agent提供,这部分代码与用户代码结合在一起,被AppClassLoader加载。不希望业务也引用这部分依赖代码,不侵入业务

示例

下面用不同版本的gson来表现这里的问题:

组件 版本 说明
demo-app 使用的 Gson 2.2.4 较旧版本,API有限
conflict-failure-demo Agent 2.8.9 最新版本,API丰富
┌─────────────────────────────────────────┐
│ 演示应用 (demo-app) │
│ ┌─────────────────────────────────┐ │
│ │ 依赖: Gson 2.2.4 (旧版本) │ │
│ │ API: 基础功能 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘

应用运行时类加载

┌─────────────────────────────────────────┐
│ Agent: conflict-failure-demo │
│ ┌─────────────────────────────────┐ │
│ │ 依赖: Gson 2.8.9 (新版本) │ │
│ │ API: 包含低版本不支持的功能 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘

❌ NoSuchMethodError
(Agent调用了app不支持的API)

解决方案

委托模式

demo代码:java-agent-demo/conflict-dispatcher-demo at main · baiyz0825/java-agent-demo · GitHub

将 Action 接口和 Dispatcher 注入到 Bootstrap ClassLoader,最终使用Dispatcher 将 Agent 依赖传递给应用代码

基本架构
┌─────────────────────────────────────────────────────────────────┐  
│ Bootstrap ClassLoader │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Action 接口 + Dispatcher 类(注入) ││
│ │ - 全局可见 ││
│ │ - 作为 Agent 和 Application 的桥梁 ││
│ └─────────────────────────────────────────────────────────────┘│
│ ↑ │
│ ┌─────────────────┴─────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Agent ClassLoader │ │ Application ClassLoader │ │
│ │ │ │ │ │
│ │ PrintInfoAction │ ──注册──→ │ UserService (增强后) │ │
│ │ - ObjectUtils │ │ - 调用 Dispatcher │ │
│ │ - StopWatch │ │ - 不直接依赖 Agent │ │
│ └──────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
工作流程

![[Java Agent 2025-12-01 21.23.30.excalidraw]]

Action 接口(注入到 Bootstrap)
public interface Action {  
/**
* 方法进入时执行
*/
Object execute(Object[] args);

/**
* 方法退出时执行
*/
Object onExit(Object result);

String getName();
}
```

##### Dispatcher 类(注入到 Bootstrap)

```java
public class Dispatcher {
private static final Map<String, Action> ACTIONS = new ConcurrentHashMap<>();

public static void register(String name, Action action) {
ACTIONS.put(name, action);
}
public static Object dispatch(String name, Object[] args) {
Action action = ACTIONS.get(name);
return action != null ? action.execute(args) : null;
}
public static Object dispatchExit(String name, Object result) {
Action action = ACTIONS.get(name);
return action != null ? action.onExit(result) : result;
}
}
```

##### PrintInfoAction(Agent 实现)

```java
public class PrintInfoAction implements Action {
@Override
public Object execute(Object[] args) {
// 安全地使用 Agent 依赖
StopWatch stopWatch = new StopWatch("createUser");
stopWatch.start("execution");

// 使用 ObjectUtils 进行参数校验
if (args != null && args.length >= 2) {
System.out.println("name isEmpty: " + ObjectUtils.isEmpty(args[0]));
System.out.println("email isEmpty: " + ObjectUtils.isEmpty(args[1]));
}
return "开始执行";
}
@Override
public Object onExit(Object result) {
// 打印执行时间
StopWatch stopWatch = STOP_WATCH_HOLDER.get();
stopWatch.stop();
System.out.println("执行时间: " + stopWatch.getTotalTimeMillis() + " ms");
return result;
}
}
```

##### ByteBuddy Advice(增强代码)

```java
public static class CreateUserAdvice {
@Advice.OnMethodEnter
public static void onEnter(@Advice.AllArguments Object[] args) {
// 只调用 Bootstrap 中的 Dispatcher,不直接使用 Agent 依赖
Class<?> dispatcherClass = Class.forName(
"org.baiyz.demo.conflict.dispatcher.bootstrap.Dispatcher",
true,
null // Bootstrap ClassLoader
);
Method dispatchMethod = dispatcherClass.getMethod("dispatch", String.class, Object[].class);
dispatchMethod.invoke(null, "createUser", args);
}
@Advice.OnMethodExit
public static void onExit(@Advice.Return Object result) {
// 调用 Dispatcher 处理返回值
dispatchExitMethod.invoke(null, "createUser", result);
}
}

maven shade

demo代码:java-agent-demo/conflict-shade-demo at main · baiyz0825/java-agent-demo · GitHub

通过 Maven Shade 插件将依赖的包路径重命名 ,Agent 和应用可以使用不同版本的同名库 ,应用代码无需任何修改

Clipboard_Screenshot_1764595671.png

实现的基本架构:

编译前                              Shade 
┌─────────────────────┐ ┌─────────────────────────────────────┐
│ Agent 代码 │ │ Agent 代码 │
│ import com.google. │ → │ import org.baiyz.agent.shaded.
│ gson.Gson; │ │ com.google.gson.Gson; │
└─────────────────────┘ └─────────────────────────────────────┘

┌─────────────────────┐ ┌─────────────────────────────────────┐
│ Gson 2.8.9 │ │ Gson 2.8.9 (重命名后) │
│ com.google.gson.* │ → │ org.baiyz.agent.shaded.
│ │ │ com.google.gson.* │
└─────────────────────┘ └─────────────────────────────────────┘

工作流程:

┌────────────────────────────────────────────────────────────────┐
│ System ClassLoader │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Agent JAR (Shaded) │ │
│ │ org.baiyz.agent.shaded.com.google.gson.Gson ← 2.8.9 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ demo-app JAR │ │
│ │ com.google.gson.Gson ← 2.2.4 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘

替换了引用类的路径,自己加载自己部分的

核心配置
配置项 说明
shadedArtifactAttached 是否将 shaded JAR 作为附加产物
shadedClassifierName shaded JAR 的分类器名称
relocations 包重命名规则
pattern 原始包名
shadedPattern 重命名后的包名
<plugin>  
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions> <execution>
<phase>package</phase>
<goals> <goal>shade</goal>
</goals>
<configuration> <shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>shaded</shadedClassifierName>
<relocations> <!-- 重命名 Gson 包名 -->
<relocation>
<pattern>com.google.gson</pattern>
<shadedPattern>org.baiyz.agent.shaded.com.google.gson</shadedPattern>
</relocation>
<!-- 重命名 Javassist 包名 -->
<relocation>
<pattern>javassist</pattern>
<shadedPattern>org.baiyz.agent.shaded.javassist</shadedPattern>
</relocation>
</relocations>
<transformers> <transformer implementation="...ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>...ShadeConflictAgent</Premain-Class>
<Agent-Class>...ShadeConflictAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
缺点
  • 必须将所有依赖涉及的类进行重定位及对资源进行排除,且由于 Maven 依赖的传递性,那么需要将依赖引入的依赖树涉及的类都进行重定位,如果 Java Agent 中的依赖重定位声明漏掉了一些类,那么就和我们开始提到的情况一样,在应用依赖的版本不一致的情况下,容易出现类转换等异常
  • 重命名包,会导致不容易进行Debug

自定义类加载器

demo:java-agent-demo/conflict-bootstrap-demo at main · baiyz0825/java-agent-demo · GitHub

  • 自定义 ClassLoader:创建 Parent 为 Bootstrap ClassLoader 的自定义类加载器
  • 依赖隔离:Agent 依赖与应用依赖完全隔离
  • 全局可见:Bootstrap 级别的类对所有 ClassLoader 可见
  • 无包重命名:保持原始包名,便于调试
基本架构
┌─────────────────────────────────────────────────────────────────┐  
Bootstrap ClassLoader │
│ (JDK 核心类) │
│ │ │
│ ┌─────────────────┴─────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ CustomBootstrap │ │ Extension ClassLoader │ │
│ │ ClassLoader │ │ │ │
│ │ (Agent 依赖) │ └──────────────────────────┘ │
│ │ - Gson 2.8.9 │ │ │
│ │ - SLF4J │ ▼ │
│ └──────────────────┘ ┌──────────────────────────┐ │
│ │ System ClassLoader │ │
│ │ (应用 + Agent 代码) │ │
│ │ - demo-app │ │
│ │ - Gson 2.2.4 │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

上述描述的隔离原理:

  1. 创建 CustomBootstrapClassLoader:Parent 设置为 null(即 Bootstrap ClassLoader)
  2. 加载 Agent 依赖:通过 CustomBootstrapClassLoader 加载 Gson 等依赖
  3. 类隔离
    • Agent 的 Gson 由 CustomBootstrapClassLoader 加载
    • 应用的 Gson 由 System ClassLoader 加载
    • 两者是不同的类(由不同 ClassLoader 加载的同名类是不同的类)
工作流程
// 1. 创建自定义 ClassLoader  
CustomBootstrapClassLoader customCL = new CustomBootstrapClassLoader(
new URL[] { agentJarUrl },
null // parent = null 表示 Bootstrap ClassLoader
);

// 2. 通过自定义 ClassLoader 加载依赖
Class<?> gsonClass = customCL.loadClass("com.google.gson.Gson");
Object gson = gsonClass.getDeclaredConstructor().newInstance();

// 3. 使用反射调用(因为类型不兼容)
Method toJsonMethod = gsonClass.getMethod("toJson", Object.class);
String json = (String) toJsonMethod.invoke(gson, data);
QA
Q1: 为什么要用反射?

不同 ClassLoader 加载的同名类是不同的类型:

// CustomBootstrapClassLoader 加载的 Gson  
Class<?> agentGson = customCL.loadClass("com.google.gson.Gson");

// System ClassLoader 加载的 Gson
Class<?> appGson = Class.forName("com.google.gson.Gson");

// 它们是不同的类!
agentGson == appGson // false
```

###### Q2: 如何传递对象?

使用序列化或基本类型:

```java
// 方式1:转为 JSON 字符串传递
String json = (String) toJsonMethod.invoke(gson, data);

// 方式2:使用基本类型或 JDK 类型
// JDK 类由 Bootstrap 加载,所有 ClassLoader 共享
```

###### Q3: 与 appendToBootstrapClassLoaderSearch 的区别?

```java
// appendToBootstrapClassLoaderSearch - 直接注入到系统 Bootstrap
inst.appendToBootstrapClassLoaderSearch(jarFile);
// 问题:污染全局 Bootstrap,可能影响其他应用

// CustomBootstrapClassLoader - 创建独立的 ClassLoader
new CustomBootstrapClassLoader(urls, null);
// 优点:隔离性好,不污染全局