How to Use Java Agent for Non‑Intrusive SpringBoot Monitoring
Learn how to implement a Java Agent that enables non‑intrusive monitoring of SpringBoot applications, covering agent basics, bytecode manipulation with Byte Buddy, metric collection via Micrometer, Prometheus/Grafana integration, and advanced extensions such as JVM metrics, HTTP client tracing, and distributed tracing.
In production environments, monitoring is crucial for diagnosing issues. This article introduces how to use Java Agent technology to achieve non‑intrusive monitoring of SpringBoot applications, allowing developers to obtain runtime metrics without modifying source code.
Java Agent Overview
Java Agent, introduced in JDK 1.5, enables dynamic bytecode modification at JVM startup or runtime, allowing enhancement or monitoring of application behavior without changing source code.
Two loading modes exist: premain (startup) and agentmain (runtime). This guide focuses on premain.
Technical Principles
Java Agent works by enhancing bytecode during class loading. In SpringBoot monitoring, it can intercept key method calls to collect execution time, resource usage, etc.
Java Agent – entry point for bytecode modification
Byte Buddy / ASM / Javassist – bytecode manipulation libraries
SpringBoot – target application framework
Micrometer – metric collection and exposure
Implementation Steps
1. Create the Agent project
<?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>demo</groupId>
<artifactId>springboot-agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>agent</artifactId>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.5</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.5</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.10.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.example.agent.MonitorAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>2. Implement the Agent main class
package com.example.agent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MonitorAgent {
private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public static void premain(String arguments, Instrumentation instrumentation) {
System.out.println("SpringBoot监控Agent已启动...");
log();
new AgentBuilder.Default()
.type(ElementMatchers.nameEndsWith("Controller"))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.method(ElementMatchers.isAnnotatedWith(
ElementMatchers.named("org.springframework.web.bind.annotation.RequestMapping")
.or(ElementMatchers.named("org.springframework.web.bind.annotation.GetMapping"))
.or(ElementMatchers.named("org.springframework.web.bind.annotation.PostMapping"))
.or(ElementMatchers.named("org.springframework.web.bind.annotation.PutMapping"))
.or(ElementMatchers.named("org.springframework.web.bind.annotation.DeleteMapping"))
))
.intercept(MethodDelegation.to(ControllerInterceptor.class))
)
.installOn(instrumentation);
}
private static void log(){
executorService.scheduleAtFixedRate(() -> {
String text = MetricsCollector.scrape();
System.out.println("===============");
System.out.println(text);
}, 0, 5, TimeUnit.SECONDS);
}
}3. Implement the interceptor
package com.example.agent;
import net.bytebuddy.implementation.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class ControllerInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable,
@AllArguments Object[] args) throws Exception {
long startTime = System.currentTimeMillis();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
try {
return callable.call();
} catch (Exception e) {
MetricsCollector.recordException(className, methodName, e);
throw e;
} finally {
long executionTime = System.currentTimeMillis() - startTime;
MetricsCollector.recordExecutionTime(className, methodName, executionTime);
}
}
}4. Implement metric collection
package com.example.agent;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class MetricsCollector {
private static final Map<String, AtomicLong> executionTimeMap = new ConcurrentHashMap<>();
private static final Map<String, AtomicLong> invocationCountMap = new ConcurrentHashMap<>();
private static final Map<String, AtomicLong> exceptionCountMap = new ConcurrentHashMap<>();
public static void recordExecutionTime(String className, String methodName, long executionTime) {
String key = className + "." + methodName;
executionTimeMap.computeIfAbsent(key, k -> new AtomicLong(0)).addAndGet(executionTime);
invocationCountMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
System.out.printf("Controller执行: %s, 耗时: %d ms%n", key, executionTime);
}
public static void recordException(String className, String methodName, Exception e) {
String key = className + "." + methodName;
exceptionCountMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
System.out.printf("Controller异常: %s, 异常类型: %s, 消息: %s%n", key, e.getClass().getName(), e.getMessage());
}
// Additional methods for SQL metrics omitted for brevity
public static String scrape() {
// Return Prometheus formatted metrics (implementation omitted)
return "";
}
}5. Optional Prometheus & Grafana integration
Add Micrometer Prometheus dependency and register metrics in MetricsCollector using PrometheusMeterRegistry. The collector then exposes data via registry.scrape() for Prometheus to pull.
6. Deploy the Agent
java -javaagent:/path/to/springboot-monitor-agent.jar -jar your-springboot-app.jarAdvanced Extensions
1. JVM metric monitoring
private static void monitorJvmMetrics(MeterRegistry registry) {
new JvmMemoryMetrics().bindTo(registry);
new JvmGcMetrics().bindTo(registry);
new JvmThreadMetrics().bindTo(registry);
}2. HTTP client monitoring
new AgentBuilder.Default()
.type(ElementMatchers.nameContains("RestTemplate")
.or(ElementMatchers.nameContains("HttpClient")))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.method(ElementMatchers.named("execute")
.or(ElementMatchers.named("doExecute"))
.or(ElementMatchers.named("exchange")))
.intercept(MethodDelegation.to(HttpClientInterceptor.class))
)
.installOn(instrumentation);3. Distributed tracing integration
public static void recordTraceInfo(String className, String methodName, String traceId, String spanId) {
MDC.put("traceId", traceId);
MDC.put("spanId", spanId);
// processing logic...
}Benefits and Considerations
Non‑intrusive : No source code changes required.
Flexibility : Dynamically select classes and methods to monitor.
Universality : Works with any SpringBoot application.
Runtime monitoring : Real‑time data collection.
Performance impact : Bytecode enhancement adds overhead; choose monitoring points wisely.
Compatibility : Ensure the Agent matches the JDK version of the application.
Stability : Agent exceptions should not affect the main application flow.
Security : Collected data may contain sensitive information; handle securely.
Conclusion
By customizing the Agent, developers can achieve fine‑grained monitoring and integrate it with existing observability stacks to build a complete performance monitoring system for SpringBoot applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
