Java Agent 是 Java 提供的一种在 JVM 启动时或运行时动态修改字节码的强大机制,广泛应用于 APM 监控(如 SkyWalking、Pinpoint)、热部署(如 JRebel)、代码覆盖率(JaCoCo)、故障注入、安全审计等场景。
一、Java Agent 的两种模式
| 模式 | 加载时机 | 典型用途 |
|---|---|---|
| Premain Agent | JVM 启动时(-javaagent) | APM 探针、性能监控、字节码增强 |
| Attach Agent | JVM 运行时动态 attach | 线上诊断(如 Arthas)、动态开关 |
二、核心原理: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.AllArguments Object[] 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.Return Object 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.java
package 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"?>
<project xmlns="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>
<transformer implementation="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到调用生命周期
1、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;
@Configuration
public 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;
@RestController
public 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");
}
}
2、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;
@RestController
public 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,用于跟踪tarce所在的应用和构建应用的拓扑图
五、验证
触发接口
至此可以通过Agent的探针实现对应用无入侵式,实现调用链的APM 监控、构建应用的拓扑图,
并切基于Agent Advice 的增强方式可以进一步实现对中间件的跟踪和观测,如接入DB的观测。