Java Agent入门:从零开始实现方法耗时打印,小白程序员必备,收藏学习!
本文介绍了Java Agent的核心概念和应用场景,包括APM、代码覆盖率工具、热部署、安全审计等。通过静态加载和动态加载两种方式,详细讲解了如何使用Byte Buddy库对Spring MVC项目中的Controller类方法进行耗时打印。静态加载在程序入口之前执行,可进行任意字节码转换;动态加载则在运行时附加,转换能力受限。文章提供了完整的代码示例和运行日志,帮助读者快速掌握Java Agent的使用方法。
概述
你是否好奇,像 OpenTelemetry、SkyWalking 这样的工具是如何在不修改源码的情况下监控 Java 应用的?它们的背后核心技术就是 Java Agent
Java Agent 是 JVM 提供的一种强大机制,允许我们在类加载前后动态修改字节码,实现无侵入的性能监控、日志增强、故障注入等功能
本文将带你进行 Java Agent 的入门,对一个 SpringMVC 项目中的 Controller 类中的方法进行耗时打印
应用场景
- APM:应用性能监控。如 SkyWalking、OpenTelemetry 等通过 Agent 插桩收集调用链、耗时等
- 代码覆盖率工具:如 JaCoCo 在测试时注入探针统计覆盖情况
- 热部署 / 热更新:如 JRebel 利用 Agent 修改类定义实现无需重启生效
- 安全审计 / 日志增强:自动为敏感方法添加日志或权限检查
- 故障注入 / 混沌工程:动态修改行为模拟异常
静态加载示例
时机
在目标程序的 main() 入口方法之前执行
加载方式
通过添加 JVM 参数【-javaagent:Agent-Jar包全路径=参数】 来加载
参数的格式一般使用【key1=value1,keyN=valueN】
类加载状态
所有应用类都尚未加载,Agent 有完全的控制权
能力
可以执行任意字节码转换,包括添加字段、方法等
重启要求
需要重启 JVM
代码与运行示例
Agent模块:依赖坐标
<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.17.8</version></dependency><dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.17.8</version></dependency>Agent模块:pom.xml 插件配置
<build> <plugins> <!-- Java Agent插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.1</version> <executions> <execution> <!-- 插件在打包时才执行 --> <phase>package</phase> <!-- 插件执行时执行maven-shade-plugin的shade操作 --> <goals> <goal>shade</goal> </goals> <configuration> <!-- 不生成精简pom文件,保持完整的依赖树 --> <createDependencyReducedPom>false</createDependencyReducedPom> <transformers> <!-- 生成jar包时,添加Agent需要的MANIFEST.MF条目 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <!-- 入口配置 --> <manifestEntries> <!-- 静态加载Agent的入口类 --> <Premain-Class>wxw.mengyuan.DemoAgent</Premain-Class> <!-- 动态加载Agent的入口类 --> <Agent-Class>wxw.mengyuan.DemoAgent</Agent-Class> <!-- 允许重转换已加载类 --> <Can-Retransform-Classes>true</Can-Retransform-Classes> <!-- 允许重新定义类结构 --> <Can-Redefine-Classes>true</Can-Redefine-Classes> <!-- 设置native方法前缀 --> <Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix> </manifestEntries> </transformer> <!-- 合并所有依赖中 META-INF/services 目录下的文件 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> <!-- 排除数字签名文件 --> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> <!-- 重命名依赖避免冲突 --> <relocations> <relocation> <pattern>com.google</pattern> <shadedPattern>com.yourcompany.shaded.google</shadedPattern> </relocation> <relocation> <pattern>org.apache</pattern> <shadedPattern>com.yourcompany.shaded.apache</shadedPattern> </relocation> </relocations> </configuration> </execution> </executions> </plugin> </plugins></build>Agent模块:方法拦截处理器
package wxw.mengyuan; import net.bytebuddy.implementation.bind.annotation.*; import java.lang.reflect.Method;import java.util.concurrent.Callable; public class AgentInterceptor { @RuntimeType public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Throwable { long start = System.currentTimeMillis(); try { return callable.call(); } finally { System.out.println("方法【" + method.getDeclaringClass().getName() + "." + method.getName() + "】执行耗时:" + (System.currentTimeMillis() - start) + "ms"); } } }Agent模块:入口类
package wxw.mengyuan; import net.bytebuddy.agent.builder.AgentBuilder;import net.bytebuddy.description.type.TypeDescription;import net.bytebuddy.dynamic.DynamicType;import net.bytebuddy.implementation.MethodDelegation;import net.bytebuddy.matcher.ElementMatchers;import net.bytebuddy.utility.JavaModule; import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain; public class DemoAgent { / * JVM启动时加载 * @param agentArgs 参数 * @param inst 该对象可以操作一些类里面的方法 */ public static void premain(String agentArgs, Instrumentation inst) { System.out.println("静态加载:当前方法执行在main入口方法之前"); System.out.println("参数:" + agentArgs); // 创建转换器 AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, ProtectionDomain protectionDomain) { return builder // 拦截哪些方法 // 仅此仅拦截 public 修饰符的方法 .method(ElementMatchers.isPublic()) // 声明拦截器 // 使用哪个类处理拦截逻辑 .intercept(MethodDelegation.to(AgentInterceptor.class)); } }; // 开始构建 new AgentBuilder.Default() // 拦截哪些类 // 此处仅拦截自身项目下、带有注解 @Controller、@RestController 的类 .type(ElementMatchers.nameStartsWith("wxw.mengyuan").and( ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.web.bind.annotation.RestController")) .or(ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.stereotype.Controller"))) )) // 设置转换器 .transform(transformer) // 开始安装 .installOn(inst); System.out.println("静态加载:加载完成"); } / * JVM运行时动态加载 * @param agentArgs 参数 * @param inst 该对象可以操作一些类里面的方法 */ public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("JVM运行时动态加载"); } }Spring MVC应用:Controller类
package wxw.mengyuan.banner; import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; import javax.annotation.PostConstruct; @RestController@RequestMapping("/demo")public class DemoController { / * 此方法会被拦截增强 */ @PostConstruct public void init() { System.out.println("Controller初始化完毕"); } / * 此方法会被拦截增强 */ @RequestMapping("demoTest") public String demoTest() { print(); return "娃哈哈~"; } / * 私有方法:此方法不会被拦截增强 */ private void print() { System.out.println("进入了Controller方法"); } }Spring MVC应用:JVM参数
-javaagent:D:/Projects/DEMO/Demo-Agent/target/demo-agent-1.0.0.jar=key1=value1Spring MVC应用:启动日志
Spring MVC应用:接口调用日志
动态加载示例
时机
在目标程序运行时动态附加
加载方式
通过 Attach API 加载
类加载状态
大部分应用类已加载,转换受限
能力
只能进行受限的字节码转换
重启要求
不需要重启 JVM
代码与运行示例
Agent模块:依赖坐标
<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.17.8</version></dependency><dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.17.8</version></dependency>Agent模块:pom.xml 插件配置
<build> <plugins> <!-- Java Agent插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.1</version> <executions> <execution> <!-- 插件在打包时才执行 --> <phase>package</phase> <!-- 插件执行时执行maven-shade-plugin的shade操作 --> <goals> <goal>shade</goal> </goals> <configuration> <!-- 不生成精简pom文件,保持完整的依赖树 --> <createDependencyReducedPom>false</createDependencyReducedPom> <transformers> <!-- 生成jar包时,添加Agent需要的MANIFEST.MF条目 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <!-- 入口配置 --> <manifestEntries> <!-- 静态加载Agent的入口类 --> <Premain-Class>wxw.mengyuan.DemoAgent</Premain-Class> <!-- 动态加载Agent的入口类 --> <Agent-Class>wxw.mengyuan.DemoAgent</Agent-Class> <!-- 允许重转换已加载类 --> <Can-Retransform-Classes>true</Can-Retransform-Classes> <!-- 允许重新定义类结构 --> <Can-Redefine-Classes>true</Can-Redefine-Classes> <!-- 设置native方法前缀 --> <Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix> </manifestEntries> </transformer> <!-- 合并所有依赖中 META-INF/services 目录下的文件 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> <!-- 排除数字签名文件 --> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> <!-- 重命名依赖避免冲突 --> <relocations> <relocation> <pattern>com.google</pattern> <shadedPattern>com.yourcompany.shaded.google</shadedPattern> </relocation> <relocation> <pattern>org.apache</pattern> <shadedPattern>com.yourcompany.shaded.apache</shadedPattern> </relocation> </relocations> </configuration> </execution> </executions> </plugin> </plugins></build>Agent模块:构造方法拦截处理器
package wxw.mengyuan; import net.bytebuddy.asm.Advice; import java.lang.reflect.Constructor; public class ConstructorAdvice { / * 进入构造方法时调用 * @param constructor 构造方法 * @return 传给 @Advice.OnMethodExit 标注方法中的数据 */ @Advice.OnMethodEnter static long enter(@Advice.Origin Constructor<?> constructor) { System.out.println("进入构造方法: " + constructor.getDeclaringClass().getName()); return System.currentTimeMillis(); } / * 退出构造方法时调用 * @param constructor 构造方法 * @param start @Advice.OnMethodEnter 标注方法的返回值 */ @Advice.OnMethodExit static void exit(@Advice.Origin Constructor<?> constructor, @Advice.Enter long start) { System.out.println("构造方法【" + constructor.getDeclaringClass().getName() + "】执行耗时:" + (System.currentTimeMillis() - start) + "ms"); } }Agent模块:非构造方法拦截处理器
package wxw.mengyuan; import net.bytebuddy.asm.Advice;import net.bytebuddy.implementation.bytecode.assign.Assigner; import java.lang.reflect.Method; public class NotConstructorAdvice { / * 进入方法时调用 * @param method 方法 * @return 传给 @Advice.OnMethodExit 标注方法中的数据 */ @Advice.OnMethodEnter static long enter(@Advice.Origin Method method) { System.out.println("进入方法: " + method.getDeclaringClass().getName() + "." + method.getName()); return System.currentTimeMillis(); } / * 退出方法时调用 * @param method 方法 * @param start @Advice.OnMethodEnter 标注方法的返回值 * @param returnValue 方法返回结果 * @param throwable 异常 */ @Advice.OnMethodExit(onThrowable = Throwable.class) static void exit(@Advice.Origin Method method, @Advice.Enter long start, @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object returnValue, @Advice.Thrown Throwable throwable) { String methodInfo = method.getDeclaringClass().getName() + "." + method.getName(); if (throwable != null) { System.err.println("方法【" + methodInfo + "】执行耗时:" + (System.currentTimeMillis() - start) + "ms,异常:" + throwable.getMessage()); } else { System.out.println("方法【" + methodInfo + "】执行耗时:" + (System.currentTimeMillis() - start) + "ms"); } } }Agent模块:入口类
package wxw.mengyuan; import net.bytebuddy.agent.builder.AgentBuilder;import net.bytebuddy.asm.Advice;import net.bytebuddy.description.type.TypeDescription;import net.bytebuddy.dynamic.DynamicType;import net.bytebuddy.matcher.ElementMatchers;import net.bytebuddy.utility.JavaModule; import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain; public class DemoAgent { / * JVM启动时加载 * @param agentArgs 参数 * @param inst 该对象可以操作一些类里面的方法 */ public static void premain(String agentArgs, Instrumentation inst) { System.out.println("静态加载:当前方法执行在main入口方法之前"); } / * JVM运行时动态加载 * @param agentArgs 参数 * @param inst 该对象可以操作一些类里面的方法 */ public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("动态加载:JVM运行过程中加载"); System.out.println("参数:" + agentArgs); // 创建转换器 AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, ProtectionDomain protectionDomain) { return builder // 对构造方法的拦截处理 .visit(Advice.to(ConstructorAdvice.class).on(ElementMatchers.isConstructor())) // 对非构造方法的拦截处理 // 仅此仅拦截 public 修饰符的方法 .visit(Advice.to(NotConstructorAdvice.class).on(ElementMatchers.isPublic().and(ElementMatchers.not(ElementMatchers.isConstructor())))); } }; // 开始构建 new AgentBuilder.Default() .disableClassFormatChanges() .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) // 打印调试日志 .with(AgentBuilder.Listener.StreamWriting.toSystemOut().withErrorsOnly()) // 拦截哪些类 // 此处仅拦截自身项目下、带有注解 @Controller、@RestController 的类 .type(ElementMatchers.nameStartsWith("wxw.mengyuan").and( ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.web.bind.annotation.RestController")) .or(ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.stereotype.Controller"))) )) // 设置转换器 .transform(transformer) // 开始安装 .installOn(inst); System.out.println("动态加载:加载完成"); } }Spring MVC应用:Controller类
package wxw.mengyuan.banner; import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; import javax.annotation.PostConstruct; @RestController@RequestMapping("/demo")public class DemoController { / * 此方法会被拦截增强 */ @PostConstruct public void init() { System.out.println("Controller初始化完毕"); } / * 此方法会被拦截增强 */ @RequestMapping("demoTest") public String demoTest() { print(); return "娃哈哈~"; } / * 私有方法:此方法不会被拦截增强 */ private void print() { System.out.println("进入了Controller方法"); } }Spring MVC应用:启动日志
测试工程:依赖坐标
<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.17.8</version></dependency><dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.17.8</version></dependency> <dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.18.1</version> <scope>compile</scope></dependency><dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna-platform</artifactId> <version>5.18.1</version> <scope>compile</scope></dependency>测试工程:测试类(编写后运行)
package com.mengyuan; import net.bytebuddy.agent.VirtualMachine; import java.io.IOException; public class DemoMain { public static void main(String[] args) { try { // 参数为目标JVM的PID VirtualMachine vm = VirtualMachine.ForHotSpot.attach("56900"); vm.loadAgent("D://Projects//DEMO//Demo-Agent//target//demo-agent-1.0.0.jar=key1=value1"); System.out.println("运行时动态加载成功"); } catch (IOException e) { throw new RuntimeException(e); } } }Spring MVC应用:动态加载日志
Spring MVC应用:接口调用日志
载成功"); } catch (IOException e) { throw new RuntimeException(e); } }
}
小白/程序员如何系统学习大模型LLM?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。
