简介:面向需要对进程进行动态控制与监控的Java开发者,这份资料以“代理(Agent)技术”为主线,围绕代理框架设计、部署、动态监控与操作执行等环节,整理出一个完整可运行的工程方案,适用于系统管理、性能优化、安全防护和自动化测试等场景。压缩包共含32个文件,核心为15个Java源文件,覆盖代理核心逻辑与进程操作实现;另配套properties、xml等配置文件,以及css、js、html前端资源,还包含pom.xml、mvnw等Maven构建脚本和jar依赖,整体约2.14MB,目录结构清晰,便于直接导入IDE分析学习。内容预览中出现的java0323、src/test/main分层、.mvn/wrapper配置等,说明工程保留了标准Maven项目结构,读者可快速定位代码入口与测试用例。目前已有37人学习下载,适合研究智能体控制进程、或希望用代理机制实现动态资源调度的进阶开发者作为参考模板。
1. 基于agent技术实现动态操作目标进程:一份可运行的 Java Agent 工程
大多数人听到“agent”,第一反应是人工智能里的智能体,但在这份基于agent技术实现动态操作目标进程.zip里,它是 Java 世界里更接地气的东西——javaagent。用 Instrumentation API 在 JVM 运行时挂到目标进程上,改它的字节码、查它的状态、调它的参数,全程不需要重启目标服务。这个能力在线上故障排查、压测流量注入、热更新场景里非常实用。这份资源是一个完整的 Maven 工程,带pom.xml、src/test、mvnw.cmd,意味着你拿到后不是看概念,而是可以直接编译、打包、attach 到自己的进程上跑通全流程。适合已经写过 Java 但没碰过 Instrumentation 的开发者,也适合需要在生产环境做动态干预的运维和中间件开发。
2. Agent 机制与工程结构:先搞懂 premain 和 agentmain 两条路径
2.1 Java Agent 为什么能“动态操作目标进程”
Java Agent 的底层依赖 JVM 的 Instrumentation 接口,它允许你在类加载之后修改字节码。这里的关键点是“之后”——不是编译期改源码,也不是启动时改启动参数,而是 JVM 已经在跑,你从外部把一段逻辑注入进去。实现方式有两条路径:
- premain:在 JVM 启动时通过
-javaagent:xxx.jar参数加载,在main方法执行前完成 instrumentation 注册。适合做启动期增强,比如云平台在应用启动时自动挂监控探针。 - agentmain:通过 Attach API 连接到一个已经在运行的 JVM 进程,动态加载 agent。这是“动态操作目标进程”的核心路径,允许你运行时挂载,不需要重启。
这份工程代码里同时能看到两条路径的痕迹,src/main下的入口类和pom.xml里的 manifest 配置对应了这两种使用方式。新手最容易犯的错误是只知道 premain,不知道 agentmain,结果一遇到“进程已经在跑不能重启”就懵了。
从 JDK 9 开始,Attach API 被挪进了jdk.attach模块,而com.sun.tools.attach.VirtualMachine类在较早版本里依赖tools.jar。这个差异直接决定了你能不能成功 attach,后面避坑章节会专门讲。
2.2 工程文件逐个拆解:这个包里面装了什么
拿到压缩包解压后,你会看到一个标准的 Maven 项目,不要被那堆文件吓到,核心只有几个。我按“必须看”和“顺便了解”给你分个类:
| 文件 | 作用 | 优先级 |
|---|---|---|
pom.xml | Maven 构建配置,定义了打包方式、入口类、依赖 | 必须看 |
src/main/java | agent 核心代码,包括 premain/agentmain 入口和字节码增强逻辑 | 必须看 |
src/test/java | 测试代码,通常包含一个可被 attach 的目标进程 | 建议看 |
mvnw.cmd/.mvn/wrapper | Maven Wrapper,避免本地环境 Maven 版本不一致 | 可忽略 |
.gitignore/README.md | 工程元信息 | 顺手看 |
mvnw.cmd的存在说明作者希望你在没有全局 Maven 环境的情况下也能构建,Windows 下直接执行.\mvnw.cmd package就行。如果你本机已经有 Maven 3.6+,可以直接用mvn命令,效果一致。
2.3 Agent 与目标进程的关系模型
要理解这套机制怎么运转,我建议你脑子里建立这么一张图:目标进程是一个黑匣子,它在运行但你不方便停;agent 是你派出去的探子,它被 JVM 用 Instrumentation 接口接纳后,可以查看类加载情况,可以注册 ClassFileTransformer 来改写类字节码,还可以调用retransformClasses对已加载的类做热替换。
目标JVM(运行中) ↑ attach VirtualMachine(Attach API) ↓ loadAgent agentmain() 或 premain() ↓ 获取 Instrumentation 实例 ↓ 注册 ClassFileTransformer(字节码增强)这个模型理顺了,后面看代码就不费力。探测类和附加逻辑都是围绕这条链路展开的。接下来进入正题,看看 pom.xml 怎么配才能让这段链路真正跑起来。
3. Maven 构建与打包配置:Premain-Class / Agent-Class 缺一不可
3.1 pom.xml 里的关键配置项
这份工程的pom.xml里,最核心的不是依赖,而是maven-jar-plugin的 manifest 配置。Agent 的 jar 包必须在MANIFEST.MF里声明入口类,JVM 才能识别。缺少这个声明,你拿到的只是一个普通的 jar,装上后 JVM 根本不认。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.example.agent.AgentMain</Premain-Class> <Agent-Class>com.example.agent.AgentMain</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>参数说明:
Premain-Class:启动时加载代理的入口,对应-javaagent使用方式Agent-Class:运行时动态加载的入口,对应 Attach API 的loadAgent方式Can-Redefine-Classes:是否允许在启动后重定义类,设置为true才支持热更新Can-Retransform-Classes:是否允许在启动后重新转换类,这个如果不开,retransformClasses会直接抛异常
写这段配置时最容易踩的坑是只声明了Premain-Class忘了Agent-Class。结果 premain 跑通了,一用VirtualMachine.loadAgent()就报ClassNotFoundException或者直接没反应。两个入口都写上,编码时两套逻辑都实现,用起来才不致于卡住。
3.2 完整构建流程:从源码到可用的 Agent Jar
构建一个可用的 agent jar 需要两步,第一步是编译,第二步是确认 manifest 正确。我习惯用下面这套命令:
# 使用项目自带的 Maven Wrapper 构建 ./mvnw clean package -DskipTests # 确认 MANIFEST.MF 内容是否正确 unzip -p target/agent-demo.jar META-INF/MANIFEST.MF执行完你应该能在target目录下看到对应的 jar 包。此刻不要急着 attach,先看一下MANIFEST.MF的内容,里面必须包含上一节提到的四个属性。这一步是很多人的盲区——构建报错知道看,构建成功了就以为万事大吉,结果运行时才暴露 manifest 缺失。提前检查,一分钟的事能省排查两小时。
如果你在 Windows 环境没有unzip命令,可以用压缩软件打开查看,或者用 JDK 自带的jar xf命令:
jar xf target/agent-demo.jar META-INF/MANIFEST.MF type META-INF\MANIFEST.MF3.3 准备一个可以被 attach 的目标进程
做动态 attach 实验,你得先有一个目标进程。最简单的方式是写一个死循环程序,让它一直跑着,然后从外部挂 agent。这份工程里src/test目录下应该有类似的样例,如果没有,自己建一个也很容易:
public class TargetApp { public static void main(String[] args) throws Exception { System.out.println("Target process started, pid: " + ProcessHandle.current().pid()); while (true) { Thread.sleep(1000); System.out.println("Target app is running..."); } } }这里打印出的 PID 是后面 attach 时要用的关键参数。TargetApp 是哪个进程,就把哪个 PID 传给VirtualMachine.attach(pid)。注意这里的 PID 是 JVM 进程的 PID,不是线程 ID,也不是端口号。
4. Agent 核心代码实现:从 attach 到字节码增强的完整链路
4.1 agentmain 入口:动态挂载的起点
工程里AgentMain类需要同时提供premain和agentmain两个静态方法,JVM 会根据加载方式不同调用对应的方法。下面是完整的入口实现:
package com.example.agent; import java.lang.instrument.Instrumentation; public class AgentMain { // JVM 启动时加载入口:-javaagent:xxx.jar public static void premain(String args, Instrumentation inst) { System.out.println("[Agent] premain called, args: " + args); // 注册转换器,premain 场景下需要传 canRetransform=true inst.addTransformer(new DynamicTransformer(), true); } // 运行时 attach 加载入口:VirtualMachine.loadAgent() public static void agentmain(String args, Instrumentation inst) { System.out.println("[Agent] agentmain called, args: " + args); inst.addTransformer(new DynamicTransformer(), true); } }逻辑说明:两个入口都做了同一件事——注册DynamicTransformer,并把canRetransform置为true。第二个参数是关键,它决定了后来注册的转换器能否对已经加载的类进行重新转换。如果你想在 agent 加载后动态修改一个已经在运行的类的行为,这个参数必须为true,否则只能影响后续加载的类。参数args是外部传入的字符串,比如 attach 时指定的 agent options,可以用来传目标方法名或要修改的配置值。
4.2 ClassFileTransformer:真正干活的字节码转换器
入口只是挂号,真正修改字节码的是ClassFileTransformer。它的transform方法在每个类被加载或重新转换时被回调。你需要在这里判断“这个类是不是我要改的”,然后决定返回原始字节还是修改后的字节。
package com.example.agent; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; public class DynamicTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // className 格式是斜杠分隔,如 com/example/TargetApp if ("com/example/TargetApp".equals(className)) { System.out.println("[Agent] Transforming class: " + className); // 这里用 ASM 或 ByteBuddy 修改字节码 // 返回修改后的 byte[],如果不想修改就返回 null } return null; // 返回 null 表示不修改 } }逻辑说明:参数className是 JVM 内部的类名格式,用斜杠而非点号分隔。返回值null表示不对此类做修改,JVM 会沿用原始字节码。只有当你对特定类感兴趣并完成了字节码改写,才返回新的 byte 数组。参数classBeingRedefined是重定义时才有的值,初始加载时为 null,可以用来区分当前是类加载阶段还是 retransform 阶段。
4.3 用 Attach API 动态连接到目标进程
有了 agent jar 和入口类还不够,你需要一个“发射器”——单独的程序,负责 attach 到目标 JVM 并加载 agent。这一步通常在 agent 外部的独立进程执行:
package com.example.attach; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; public class AttachMain { public static void main(String[] args) throws Exception { // 方式一:列出所有 JVM 进程,找到目标 for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) { System.out.println("JVM: " + descriptor.id() + " - " + descriptor.displayName()); } // 方式二:直接通过目标进程 PID attach String pid = args[0]; // 目标进程 PID String agentJarPath = args[1]; // agent jar 的绝对路径 VirtualMachine vm = VirtualMachine.attach(pid); try { vm.loadAgent(agentJarPath, "optional-args"); System.out.println("[Attach] Agent loaded successfully to process " + pid); } finally { vm.detach(); } } }逻辑说明:VirtualMachine.attach(pid)建立到目标 JVM 的通信信道,loadAgent(agentJarPath, args)让目标 JVM 加载 agent jar 并执行其中的agentmain方法。第二个字符串参数会原样传给agentmain的args参数。finally块里的detach()很关键——attach 成功后要主动断开连接,否则会占着目标进程的资源。
刚才这段只是完成了“挂载”。挂载后能不能真正操作进程,取决于 transformer 里对字节码改写了什么。如果你想不依赖 ASM 手写字节码,可以在 transformer 里配合 ByteBuddy 做方法拦截,这比直接操作字节码容易得多:
// ByteBuddy 方式:拦截 TargetApp 的 printStatus 方法 new AgentBuilder.Default() .type(ElementMatchers.named("com.example.TargetApp")) .transform((builder, typeDescription, classLoader, module) -> builder.method(ElementMatchers.named("printStatus")) .intercept(FixedValue.value("[Agent] method intercepted!")) ) .installOn(inst);这种方法让动态操作目标进程的门槛从“你必须懂 JVM 字节码规范”降到了“你会用 API 做方法匹配”,是现在的主流做法。
4.4 测试用例:模拟真实 attach 流程
工程里src/test/java下的测试代码一般会模拟“启动目标进程 → attach → 验证行为变化”的完整链路。核心思路是先把目标进程启动为子进程,拿到 PID,再反射调用 attach 逻辑:
@Test public void testDynamicAttach() throws Exception { // 启动一个 Java 子进程作为目标 ProcessBuilder pb = new ProcessBuilder( "java", "-cp", System.getProperty("java.class.path"), "com.example.TargetApp" ); Process targetProcess = pb.start(); // 读取子进程 PID long pid = targetProcess.pid(); System.out.println("Target process started with PID: " + pid); // 等待目标进程初始化完成 Thread.sleep(3000); // 执行 attach VirtualMachine vm = VirtualMachine.attach(String.valueOf(pid)); vm.loadAgent("/path/to/agent-demo.jar", "test-args"); vm.detach(); // 验证目标进程行为发生变化 // 这里通常通过查看子进程输出来判断是否增强成功 }测试逻辑里有个隐含问题:agent jar 的路径必须是绝对路径。如果写成相对路径,attach API 在不同平台下的解析行为不一致,Windows 下尤其容易翻车。另外目标进程刚启动时可能还没完成类加载,如果 attach 太早,transformer 注册后可能错过目标类的加载时机。我在实际中一般会加 2~3 秒的等待,或者监听目标进程的某个启动完成的输出。行到这一步,你已经掌握了“如何让 agent 挂上目标进程”的全部要点,但离“稳定使用”还差一批实际踩过的坑。
5. 避坑指南:attach 失败与字节码增强翻车的四个常见原因
5.1 加载 agent 后没有任何输出,仿佛石沉大海
- 现象:
vm.loadAgent()调用成功没有抛异常,但目标进程控制台完全没有 agent 的打印信息。 - 原因:agent 的
MANIFEST.MF里只配了Premain-Class没配Agent-Class。loadAgent走的是agentmain路径,找不到入口类时 JVM 选择静默忽略,不会向你报告错误。 - 解决:把两个入口类都声明在
manifestEntries里,并确保两个静态方法都真正实现了。我之前遇到过一次agentmain方法体是空的,也是同样表现。
5.2VirtualMachine.attach()抛出 IOException: Can't attach to the process
- 现象:在 JDK 8 环境下 attach 报
IOException,或提示tools.jar找不到。 - 原因:Attach API 在 JDK 8 及更早版本里依赖
tools.jar,而这个 jar 在 JRE 环境里不存在。如果项目跑在 JRE 上,com.sun.tools.attach包直接不可用。 - 解决:确保编译和运行都使用 JDK 而非 JRE。IDEA 里检查 Project SDK 是否选了 JDK;命令行用
java -version确认版本后,再确认JAVA_HOME指向的是 JDK 目录。JDK 9 之后这个问题自动消失,因为 attach 已经模块化。
5.3retransformClasses抛 UnsupportedOperationException
- 现象:注册 transformer 后调用
inst.retransformClasses(Class),JVM 直接抛异常,提示不支持重新转换。 - 原因:
addTransformer的第二个参数canRetransform传了false,或者 manifest 里Can-Retransform-Classes没设为true。这两个开关任何一个关着,retransform 都不生效。 - 解决:代码里调用
inst.addTransformer(transformer, true),同时确认 pom.xml 的 manifest 配置中Can-Retransform-Classes为true。注意addTransformer的第二个参数是 Java 9 才有的重载,如果你在 Java 8 环境,要检查 JDK 版本是否支持。
5.4 字节码增强后目标类报 NoSuchMethodError
- 现象:agent 成功挂载,transformer 也执行了,但目标进程调用被增强的方法时抛
NoSuchMethodError或VerifyError。 - 原因:修改字节码时引入了目标类中不存在的方法引用,或者改写的字节码违反了 JVM 校验规则。常见做法是用 ASM 时把方法名或描述符写错——比如把
printStatus写成printstatus,JVM 在运行到该方法时才做校验,错误就延迟爆发了。 - 解决:先用
javap -c查看目标类的原始字节码,确认方法签名和描述符。如果用 ByteBuddy,优先用ElementMatchers.named("printStatus")配合方法签名匹配,不要手工拼字符串。改动后记得跑一遍目标进程的原有功能测试,验证增强逻辑没有破坏原始行为。另一种隐性风险是目标类由不同的 ClassLoader 加载,你在 transformer 里引用的辅助类必须对目标类的 ClassLoader 可见,必要时用 ClassLoader 上下文做处理,否则会莫名 ClassNotFound。
6. 进阶用法与验证手段:把 attach 能力接入日常运维流程
6.1 结合 JDK 自带工具做验证
attach 是否成功不要凭日志猜,用jcmd或jstack从旁观者视角验证。当 agent 挂载后,JVM 会多出一些动态加载的类,用下面命令可以直接看到:
# 查看目标进程已加载的 agent 类 jcmd <pid> VM.class_hierarchy com.example.agent # 查看目标进程的系统属性,确认 agent 是否注入了内容 jcmd <pid> VM.system_properties | grep agent如果VM.class_hierarchy能列出com.example.agent下的类,说明 agent 确实被加载到了目标 JVM 的类体系里,而不是只在外部 attach 程序里存在。这个验证方法比看日志更可靠,因为有些 logger 配置会吞掉 agent 的 System.out 输出。我在临时排查线上问题时,遇到 console 没输出,第一反应就是怀疑 logger 配置,然后直接用 jcmd 验证,比瞎猜快得多。
6.2 把 agent 做成可配置的“运维后门”
动态 attach 最大的价值在于持续运维。生产环境不能动不动重启,但有些痛点必须运行中解决,比如:临时开启某个接口的耗时日志、动态调整连接池大小、给某个类打点。Agent 的args参数可以传 JSON 字符串,让 attach 时指定要操作的目标类和注入参数:
java -cp attach-tool.jar com.example.attach.AttachMain \ 12345 \ /opt/agent/agent-demo.jar \ '{"className":"com.example.Service","method":"query","logEnabled":true}'agentmain里解析这个 JSON,决定注册哪个 transformer、拦截哪些方法。这样一来,同一个 agent jar 可以被当成一把多功能的瑞士军刀,按需加载不同逻辑,而不是每个场景都重新打个 jar。工程里的pom.xml把 Premain-Class 和 Agent-Class 都声明为同一个类,正是为了同时支持启动时装载和运行时 attach 两种模式。
6.3 动态操作的安全边界与权限收敛
动态 attach 威力大,风险也成正比。能从外部改写运行中进程的字节码,等于把 JVM 的安全边界打开了一个口子。生产环境我有几个习惯性约束:attach 工具单独放在跳板机,不随业务应用分发;目标进程启动时用-Djdk.attach.allowAttachSelf=false禁止自 attach;agent 里不允许执行任意代码路径,只接受白名单内的指令参数。这套约束不是我凭空加的,是从实战里吃到过苦头总结的——有一次 agent 里留了一个调试入口,参数没做校验,结果一个误传的参数把线上服务的某个方法拦截掉了,整条链路行为异常排查了大半天才发现是 agent 干的。从那以后,我每次部署 agent 之前都强制走一遍“白名单校验 + 灰度验证 + 可回滚方案”三道流程,宁可挂载慢一点,也不允许它成为线上新的不可控因素。希望这篇拆解能帮你在自己的环境里安全地把 attach 能力跑起来,少走几趟我走过的弯路。
本文还有配套的精品资源,点击获取