SpringBoot基于Java Agent實現項目監(jiān)控
在生產環(huán)境中,監(jiān)控對于項目問題的分析排查變得尤為重要。
本文將介紹如何利用Java Agent技術實現對SpringBoot應用的無侵入式監(jiān)控,幫助開發(fā)人員在不修改源碼的情況下獲取應用運行時的關鍵指標。
Java Agent簡介
Java Agent是JDK 1.5引入的特性,它允許我們在JVM啟動時或運行時動態(tài)地修改已加載的類字節(jié)碼,從而實現對應用行為的增強或監(jiān)控。
Java Agent的核心優(yōu)勢在于能夠在不修改源代碼的情況下,對應用進行功能擴展。
Java Agent主要有兩種使用方式:
啟動時加載(premain) 運行時加載(agentmain)
本文將主要關注啟動時加載的方式。
技術原理
Java Agent的工作原理基于字節(jié)碼增強技術,通過在類加載過程中修改字節(jié)碼來實現功能增強。
在SpringBoot應用監(jiān)控場景中,我們可以利用Java Agent攔截關鍵方法的調用,收集執(zhí)行時間、資源使用情況等指標。
主要技術棧:
- Java Agent:提供字節(jié)碼修改的入口
- Byte Buddy/ASM/Javassist:字節(jié)碼操作庫
- SpringBoot:目標應用框架
- Micrometer:指標收集與暴露
實現步驟
1. 創(chuàng)建Agent項目
首先,我們需要創(chuàng)建一個獨立的Maven項目用于開發(fā)Java Agent:
<?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. 實現Agent主類
創(chuàng)建MonitorAgent類,實現premain方法:
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監(jiān)控Agent已啟動...");
log();
// 使用ByteBuddy攔截SpringBoot的Controller方法
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. 實現攔截器
創(chuàng)建Controller攔截器:
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. 實現指標收集
創(chuàng)建MetricsCollector類用于收集和暴露監(jiān)控指標:
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();
// 輸出日志,實際項目中可能會發(fā)送到監(jiān)控系統(tǒng)
System.out.printf("Controller執(zhí)行: %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());
}
public static void recordSqlExecutionTime(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("SQL執(zhí)行: %s, 耗時: %d ms%n", key, executionTime);
}
public static void recordSqlException(String className, String methodName, Exception e) {
String key = className + "." + methodName;
exceptionCountMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
System.out.printf("SQL異常: %s, 異常類型: %s, 消息: %s%n",
key, e.getClass().getName(), e.getMessage());
}
// 獲取各種指標的方法,可以被監(jiān)控系統(tǒng)調用
public static Map<String, AtomicLong> getExecutionTimeMap() {
return executionTimeMap;
}
public static Map<String, AtomicLong> getInvocationCountMap() {
return invocationCountMap;
}
public static Map<String, AtomicLong> getExceptionCountMap() {
return exceptionCountMap;
}
}
5. 集成Prometheus與Grafana(可選)
為了更好地可視化監(jiān)控數據,我們可以將收集到的指標暴露給Prometheus,并使用Grafana進行展示。首先,添加Micrometer相關依賴:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.10.0</version>
</dependency>
然后,修改MetricsCollector類,將收集到的指標注冊到Micrometer:
package com.example.agent;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class MetricsCollector {
private static final PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
private static final Map<String, Timer> timers = new ConcurrentHashMap<>();
private static final Map<String, Counter> exceptionCounters = new ConcurrentHashMap<>();
public static void recordExecutionTime(String className, String methodName, long executionTime) {
String key = className + "." + methodName;
getOrCreateTimer(key, "controller").record(executionTime, TimeUnit.MILLISECONDS);
System.out.printf("Controller執(zhí)行: %s, 耗時: %d ms%n", key, executionTime);
}
public static void recordException(String className, String methodName, Exception e) {
String key = className + "." + methodName;
getOrCreateExceptionCounter(key, "controller", e.getClass().getSimpleName()).increment();
System.out.printf("Controller異常: %s, 異常類型: %s, 消息: %s%n",
key, e.getClass().getName(), e.getMessage());
}
public static void recordSqlExecutionTime(String className, String methodName, long executionTime) {
String key = className + "." + methodName;
getOrCreateTimer(key, "sql").record(executionTime, TimeUnit.MILLISECONDS);
System.out.printf("SQL執(zhí)行: %s, 耗時: %d ms%n", key, executionTime);
}
public static void recordSqlException(String className, String methodName, Exception e) {
String key = className + "." + methodName;
getOrCreateExceptionCounter(key, "sql", e.getClass().getSimpleName()).increment();
System.out.printf("SQL異常: %s, 異常類型: %s, 消息: %s%n",
key, e.getClass().getName(), e.getMessage());
}
private static Timer getOrCreateTimer(String name, String type) {
return timers.computeIfAbsent(name, k ->
Timer.builder("app.execution.time")
.tag("name", name)
.tag("type", type)
.register(registry)
);
}
private static Counter getOrCreateExceptionCounter(String name, String type, String exceptionType) {
String key = name + "." + exceptionType;
return exceptionCounters.computeIfAbsent(key, k ->
Counter.builder("app.exception.count")
.tag("name", name)
.tag("type", type)
.tag("exception", exceptionType)
.register(registry)
);
}
// 獲取Prometheus格式的指標數據
public static String scrape() {
return registry.scrape();
}
// 獲取注冊表,可以被其他組件使用
public static MeterRegistry getRegistry() {
return registry;
}
}
6. 啟動Agent并應用到SpringBoot應用
編譯并打包Agent項目后,可以通過JVM參數將Agent添加到SpringBoot應用中:
java -javaagent:/path/to/springboot-monitor-agent.jar -jar your-springboot-app.jar
進階擴展
除了基本的監(jiān)控功能外,我們還可以對Agent進行以下擴展:
1. JVM指標監(jiān)控
監(jiān)控JVM的內存使用、GC情況、線程數等指標:
private static void monitorJvmMetrics(MeterRegistry registry) {
// 注冊JVM內存指標
new JvmMemoryMetrics().bindTo(registry);
// 注冊GC指標
new JvmGcMetrics().bindTo(registry);
// 注冊線程指標
new JvmThreadMetrics().bindTo(registry);
}
2. HTTP客戶端監(jiān)控
監(jiān)控應用發(fā)起的HTTP請求:
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. 分布式追蹤集成
與Zipkin或Jaeger等分布式追蹤系統(tǒng)集成,實現全鏈路追蹤:
public static void recordTraceInfo(String className, String methodName, String traceId, String spanId) {
// 記錄追蹤信息
MDC.put("traceId", traceId);
MDC.put("spanId", spanId);
// 處理邏輯...
}
優(yōu)勢與注意事項
優(yōu)勢
- 無侵入性:不需要修改應用源代碼
- 靈活性:可以動態(tài)決定要監(jiān)控的類和方法
- 通用性:適用于任何基于SpringBoot的應用
- 運行時監(jiān)控:可以實時收集應用運行數據
注意事項
- 性能影響:字節(jié)碼增強會帶來一定的性能開銷,需要合理選擇監(jiān)控點
- 兼容性:需要確保Agent與應用的JDK版本兼容
- 穩(wěn)定性:Agent本身的異常不應影響應用主流程
- 安全性:收集的數據可能包含敏感信息,需要注意數據安全
總結
在實際使用中,我們可以根據具體需求,對Agent進行定制化開發(fā),實現更加精細化的監(jiān)控。
同時,可以將Agent與現有的監(jiān)控系統(tǒng)集成,構建完整的應用性能監(jiān)控體系。
到此這篇關于SpringBoot基于Java Agent實現項目監(jiān)控的文章就介紹到這了,更多相關SpringBoot Java Agent項目監(jiān)控內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Spring的@PreAuthorize注解自定義權限校驗詳解
這篇文章主要介紹了Spring的@PreAuthorize注解自定義權限校驗詳解,由于項目中,需要對外開放接口,要求做請求頭校驗,不做其他權限控制,所以準備對開放的接口全部放行,不做登錄校驗,需要的朋友可以參考下2023-11-11
如何基于spring security實現在線用戶統(tǒng)計
這篇文章主要介紹了如何基于spring security實現在線用戶統(tǒng)計,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06
解決Spring Boot和Feign中使用Java 8時間日期API(LocalDate等)的序列化問題
這篇文章主要介紹了解決Spring Boot和Feign中使用Java 8時間日期API(LocalDate等)的序列化問題,需要的朋友可以參考下2018-03-03
java連接postgresql數據庫代碼及maven配置方式
這篇文章主要介紹了java連接postgresql數據庫代碼及maven配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09

