当前位置:首页>java>Java agent探针运用

Java agent探针运用

  • 2026-10-21 13:27:44
Java agent探针运用

作者:苏夏

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.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的观测。

最新文章

随机文章