作者:苏夏
Java Agent 是 Java 提供的一种在 JVM 启动时或运行时动态修改字节码的强大机制,广泛应用于APM 监控(如 SkyWalking、Pinpoint)、热部署(如 JRebel)、代码覆盖率(JaCoCo)、故障注入、安全审计等场景。

核心原理:Instrumentation
Agent 通过java.lang.instrument.Instrumentation接口实现:
retransformClasses():重新转换已加载的类(需类支持 retransformation)redefineClasses():直接替换类的字节码(限制多,不常用)addTransformer():注册ClassFileTransformer,在类加载时修改字节码
快速入门:编写一个简单Agent
步骤1:创建Agent入口类:
package com.example;import com.example.transformer.RestTemplateTraceAdvice;import com.example.transformer.TraceAdvice;import com.fasterxml.jackson.databind.ObjectMapper;import net.bytebuddy.agent.builder.AgentBuilder;import net.bytebuddy.asm.Advice;import net.bytebuddy.description.method.MethodDescription;import net.bytebuddy.matcher.ElementMatcher;import java.lang.instrument.Instrumentation;import java.util.Arrays;import java.util.HashSet;import java.util.Set;import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;import static net.bytebuddy.matcher.ElementMatchers.named;import static net.bytebuddy.matcher.ElementMatchers.takesArguments;public class TraceAgent { private final static String appId ; private static final Set<String> REQUEST_MAPPING_ANNOTATIONS = new HashSet<>(Arrays.asList( "org.springframework.web.bind.annotation.RequestMapping", "org.springframework.web.bind.annotation.GetMapping", "org.springframework.web.bind.annotation.PostMapping", "org.springframework.web.bind.annotation.PutMapping", "org.springframework.web.bind.annotation.DeleteMapping" )); private static CustomAgentListener customListener; static { appId = System.getProperty("appId"); } public static void premain(String agentArgs, Instrumentation inst) { install(inst); } public static void agentmain(String agentArgs, Instrumentation inst) { install(inst); } private static void install(Instrumentation inst) { // 创建自定义监听器,输出到指定文件,只记录指定包的类 customListener = new CustomAgentListener( "/Users/dsy/code/agent-demo/logs/"+appId+"-bytebuddy-agent.log", // 日志文件路径 "com.example" // 只记录 com.example 包下的类 ); new AgentBuilder.Default() .with(customListener) // 👈 关键:输出匹配详情 .disableClassFormatChanges() .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .with(AgentBuilder.DescriptionStrategy.Default.POOL_ONLY) // 👈 启用完整类型解析 .type( isAnnotatedWith(named("org.springframework.stereotype.Controller")) .or(isAnnotatedWith(named("org.springframework.web.bind.annotation.RestController"))) ) .transform((builder, typeDescription, classLoader, module) -> builder.visit(Advice.to(TraceAdvice.class) .on(anyMethodAnnotatedWithRequestMapping()))// builder.method(any()).intercept(MethodDelegation.to(NoOpInterceptor.class)) ) .type(named("org.springframework.web.client.RestTemplate")) .transform((builder, td, cl, module) -> builder.visit(Advice.to(RestTemplateTraceAdvice.class) .on(named("exchange") .and(takesArguments(4)) .or(takesArguments(5)) .or(takesArguments(6)))) ) .installOn(inst); System.out.println("[Agent] Controller tracing agent installed."); } private static ElementMatcher.Junction<MethodDescription> anyMethodAnnotatedWithRequestMapping() { return isAnnotatedWith(named("org.springframework.web.bind.annotation.RequestMapping")) .or(isAnnotatedWith(named("org.springframework.web.bind.annotation.GetMapping"))) .or(isAnnotatedWith(named("org.springframework.web.bind.annotation.PostMapping"))) .or(isAnnotatedWith(named("org.springframework.web.bind.annotation.PutMapping"))) .or(isAnnotatedWith(named("org.springframework.web.bind.annotation.DeleteMapping"))); } // 添加关闭方法,用于清理资源 public static void shutdown() { if (customListener != null) { customListener.close(); } }}
步骤2:实现TraceAdvice
package com.example.transformer;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import net.bytebuddy.asm.Advice;import javax.servlet.http.HttpServletRequest;import java.util.Arrays;import java.util.UUID;import static com.example.transformer.TraceContextHolder.PARENT_APP_ID;import static com.example.transformer.TraceContextHolder.X_TRACE_ID;public class TraceAdvice { public final static String appId ; public final static ObjectMapper objectMapper; static { appId = System.getProperty("appId"); objectMapper = new ObjectMapper(); } @Advice.OnMethodEnter public static void enter(@Advice.AllArgumentsObject[] args) { TraceContextHolder.TraceContext traceContext = TraceContextHolder.traceContext(); // 尝试从参数中提取 HttpServletRequest HttpServletRequest request = null; for (Object arg : args) { if (arg instanceof HttpServletRequest) { request = (HttpServletRequest) arg; break; } } String traceId = null; String parentAppId = null; if (request != null) { // 优先从 Header 中获取 traceId(例如:X-Trace-Id) traceId = request.getHeader(X_TRACE_ID); parentAppId = request.getHeader(PARENT_APP_ID); } if (traceId == null || traceId.trim().isEmpty()) { // 未传入,则生成新 traceId(建议用 UUID 或 Snowflake) traceId = "trace-" + UUID.randomUUID().toString().replace("-", "").substring(0, 32); } if (parentAppId == null || parentAppId.trim().isEmpty()){ parentAppId = "0" ; } traceContext.setTraceId(traceId); traceContext.setArgs(args.toString()); traceContext.setAppId(appId); traceContext.setParentAppId(parentAppId); traceContext.setTraceSpanStartTime(System.currentTimeMillis()); // 绑定到当前线程// TraceContextHolder.setTraceContext(traceContext); System.err.println(">>> Entering method with args: " + Arrays.toString(args)); } @Advice.OnMethodExit public static void exit(@Advice.ReturnObject result) { TraceContextHolder.TraceContext traceContext = TraceContextHolder.traceContext(); traceContext.setTraceSpanEndTime(System.currentTimeMillis()); try { traceContext.setResult(objectMapper.writeValueAsString(result)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } System.err.println("<<< Exiting method, returned: " + traceContext.toSting()); }}
步骤3:用于跟踪调用链的上下文
在src/main/resources/META-INF/MANIFEST.MF中声明:
// com/example/transformer/TraceContextHolder.javapackage com.example.transformer;public class TraceContextHolder { public final static String X_TRACE_ID = "X-Trace-Id"; public final static String PARENT_APP_ID = "X-Parent-APP-Id"; static String FORMAT = "traceId:%s,parentAppId:%s,appId:%s,traceSpanStartTime:%d,traceSpanEndTime:%d,args:%s,result:%s"; private static final ThreadLocal<TraceContext> TRACE = new ThreadLocal<>(); public static void setTraceContext(TraceContext traceContext) { TRACE.set(traceContext); } public static void clear() { TRACE.remove(); } public static TraceContext traceContext() { TraceContext object; if (TRACE.get() != null) { object = TRACE.get(); } else { object = new TraceContext(); TRACE.set(object); } return object; } public static class TraceContext{ private String traceId; private String parentAppId; private String appId; private Long traceSpanStartTime; private Long traceSpanEndTime; private String args; private String result; public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getParentAppId() { return parentAppId; } public void setParentAppId(String parentAppId) { this.parentAppId = parentAppId; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public Long getTraceSpanStartTime() { return traceSpanStartTime; } public void setTraceSpanStartTime(Long traceSpanStartTime) { this.traceSpanStartTime = traceSpanStartTime; } public Long getTraceSpanEndTime() { return traceSpanEndTime; } public void setTraceSpanEndTime(Long traceSpanEndTime) { this.traceSpanEndTime = traceSpanEndTime; } public String getArgs() { return args; } public void setArgs(String args) { this.args = args; } public String getResult() { return result; } public void setResult(String result) { this.result = result; } public String toSting(){ return String.format(FORMAT,traceId,parentAppId,appId,traceSpanStartTime,traceSpanEndTime,args,result); } }}
步骤4:打包&使用
如果用 Maven,可通过maven-jar-plugin自动生成:
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.example</groupId> <artifactId>agent-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>agent3</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- ByteBuddy 核心 --> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.12.10</version> </dependency> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.12.10</version> </dependency> <!-- Spring Web(仅用于类型判断,非强制) --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.3.31</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.0</version> <!-- 使用最新稳定版 --> </dependency> </dependencies> <build> <plugins> <!-- 使用 shade plugin 打包 fat jar --> <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> <transformers> <transformerimplementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Premain-Class>com.example.TraceAgent2</Premain-Class> <Agent-Class>com.example.TraceAgent2</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build></project>

创建两个web应用验证trace到调用生命周期

Web-app
package com.example.demo.conf;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.client.RestTemplate;@Configurationpublic class BeanConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); }}
package com.example.demo.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.HttpMethod;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import javax.annotation.Resource;@RestControllerpublic class HelloController { private static final Logger logger = LoggerFactory.getLogger(HelloController.class); @Resource private RestTemplate restTemplate; @GetMapping("/hello") public String hello(@RequestParam(defaultValue = "World") String name) { logger.info("Processing hello request for: {}", name); String url = "http://localhost:8081/shopping?commodity=香蕉"; String r = restTemplate.exchange(url, HttpMethod.GET,null,String.class).getBody(); return "Hello, " + name + "!" + " commodity = " + r; } @PostMapping("/user") public String createUser(@RequestBody String userData) { logger.info("Creating user with data: {}", userData); return "User created: " + userData; } @GetMapping("/error") public String error() { logger.info("Triggering error"); throw new RuntimeException("Test exception"); }}
Web-app1
package com.example.demo.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class ShoppController { private static final Logger logger = LoggerFactory.getLogger(ShoppController.class); @GetMapping("/shopping") public String hello(@RequestParam(defaultValue = "苹果") String commodity) { logger.info("Processing hello request for: {}", commodity); return "commodity, " + commodity + "!"; } @PostMapping("/user") public String createUser(@RequestBody String userData) { logger.info("Creating user with data: {}", userData); return "User created: " + userData; } @GetMapping("/error") public String error() { logger.info("Triggering error"); throw new RuntimeException("Test exception"); }}
从上面可以看到我们在web-app的应用中的hell接口中调用了web-app1的shopping接口,且web-app的接入方式是无代码入侵形式的RestTemplate,主要是依赖agent对asm对增强能实现对trace调用透传
且web-app和web-app1两个进程起来时要通过-javaagent方式将agent的探针无入侵的方式接入应用中。而-DappId是接入的应用id,用于跟踪trace所在的应用和构建应用的拓扑图

验证
触发接口


至此可以通过Agent的探针实现对应用无入侵式,实现调用链的APM 监控、构建应用的拓扑图,并且基于Agent Advice 的增强方式可以进一步实现对中间件的跟踪和观测,如接入DB的观测。
