基于SpringBoot實現(xiàn)一個方法級耗時監(jiān)控器
一、需求痛點
線上應用常見問題:
某些接口偶爾變慢,但日志看不出問題;
方法調(diào)用次數(shù)不透明,性能瓶頸難找;
線上出現(xiàn)失敗/超時,但缺乏統(tǒng)計維度;
想要監(jiān)控,卻不想引入重量級的 APM 方案。
常見 APM 工具功能強大,但部署復雜、學習成本高,不適合中小團隊或者單機項目。
那有沒有可能,基于 SpringBoot 實現(xiàn)一個輕量級耗時監(jiān)控器,做到方法級監(jiān)控 + 可視化統(tǒng)計 ?
二、功能目標
我們希望監(jiān)控器能做到:
基礎監(jiān)控能力:
- 方法調(diào)用次數(shù):統(tǒng)計某方法被調(diào)用了多少次
- 耗時指標:平均耗時、最大耗時、最小耗時
- 成功/失敗次數(shù):區(qū)分正常與異常調(diào)用
- 多維排序:支持按調(diào)用次數(shù)、平均耗時、失敗次數(shù)等維度排序
進階功能:
- 時間段過濾:選擇時間范圍(如最近 5 分鐘、1 小時、1 天)查看數(shù)據(jù)
- 接口搜索:快速定位特定接口的性能數(shù)據(jù)
- 可視化控制臺:實時展示接口調(diào)用統(tǒng)計
三、技術(shù)設計
整體思路
方法切面采集 使用 SpringAOP(基于攔截器亦可) 攔截 Controller 方法,在方法執(zhí)行前后記錄時間差、執(zhí)行結(jié)果(成功/失?。?。
分級數(shù)據(jù)存儲 采用分級時間桶策略:
- 最近5分鐘:秒級精度統(tǒng)計
- 最近1小時:分鐘級聚合
- 最近24小時:小時級聚合
- 最近7天:天級聚合
智能查詢 根據(jù)查詢時間范圍,自動選擇最適合的數(shù)據(jù)粒度進行聚合計算。
接口展示 提供 REST API 輸出統(tǒng)計數(shù)據(jù),前端使用 TailwindCSS + Alpine.js 渲染界面。
四、核心實現(xiàn)
1. 時間桶數(shù)據(jù)模型
@Data
public class TimeBucket {
private final AtomicLong totalCount = new AtomicLong(0);
private final AtomicLong successCount = new AtomicLong(0);
private final AtomicLong failCount = new AtomicLong(0);
private final LongAdder totalTime = new LongAdder();
private volatile long maxTime = 0;
private volatile long minTime = Long.MAX_VALUE;
private final long bucketStartTime;
private volatile long lastUpdateTime;
public TimeBucket(long bucketStartTime) {
this.bucketStartTime = bucketStartTime;
this.lastUpdateTime = System.currentTimeMillis();
}
public synchronized void record(long duration, boolean success) {
totalCount.incrementAndGet();
if (success) {
successCount.incrementAndGet();
} else {
failCount.incrementAndGet();
}
totalTime.add(duration);
maxTime = Math.max(maxTime, duration);
minTime = Math.min(minTime, duration);
lastUpdateTime = System.currentTimeMillis();
}
public double getAvgTime() {
long total = totalCount.get();
return total == 0 ? 0.0 : (double) totalTime.sum() / total;
}
public double getSuccessRate() {
long total = totalCount.get();
return total == 0 ? 0.0 : (double) successCount.get() / total * 100;
}
}
2. 分級采樣指標模型
@Data
public class HierarchicalMethodMetrics {
// 基礎統(tǒng)計信息
private final AtomicLong totalCount = new AtomicLong(0);
private final AtomicLong successCount = new AtomicLong(0);
private final AtomicLong failCount = new AtomicLong(0);
private final LongAdder totalTime = new LongAdder();
private volatile long maxTime = 0;
private volatile long minTime = Long.MAX_VALUE;
private final String methodName;
// 分級時間桶
private final ConcurrentHashMap<Long, TimeBucket> secondBuckets = new ConcurrentHashMap<>(); // 最近5分鐘,秒級
private final ConcurrentHashMap<Long, TimeBucket> minuteBuckets = new ConcurrentHashMap<>(); // 最近1小時,分鐘級
private final ConcurrentHashMap<Long, TimeBucket> hourBuckets = new ConcurrentHashMap<>(); // 最近24小時,小時級
private final ConcurrentHashMap<Long, TimeBucket> dayBuckets = new ConcurrentHashMap<>(); // 最近7天,天級
public synchronized void record(long duration, boolean success) {
long currentTime = System.currentTimeMillis();
// 更新基礎統(tǒng)計
totalCount.incrementAndGet();
if (success) {
successCount.incrementAndGet();
} else {
failCount.incrementAndGet();
}
totalTime.add(duration);
maxTime = Math.max(maxTime, duration);
minTime = Math.min(minTime, duration);
// 分級記錄到不同時間桶
recordToTimeBuckets(currentTime, duration, success);
// 清理過期桶
cleanupExpiredBuckets(currentTime);
}
public TimeRangeMetrics queryTimeRange(long startTime, long endTime) {
List<TimeBucket.TimeBucketSnapshot> buckets = selectBucketsForTimeRange(startTime, endTime);
return aggregateSnapshots(buckets, startTime, endTime);
}
}
3. AOP 切面統(tǒng)計
@Slf4j
@Aspect
@Component
public class MethodMetricsAspect {
private final ConcurrentHashMap<String, HierarchicalMethodMetrics> metricsMap = new ConcurrentHashMap<>();
@Around("@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Controller)")
public Object recordMetrics(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = buildMethodName(joinPoint);
long startTime = System.nanoTime();
boolean success = true;
try {
Object result = joinPoint.proceed();
return result;
} catch (Throwable throwable) {
success = false;
throw throwable;
} finally {
long duration = (System.nanoTime() - startTime) / 1_000_000; // Convert to milliseconds
metricsMap.computeIfAbsent(methodName, HierarchicalMethodMetrics::new)
.record(duration, success);
}
}
private String buildMethodName(ProceedingJoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
return className + "." + methodName + "()";
}
public Map<String, HierarchicalMethodMetrics> getMetricsSnapshot() {
return new ConcurrentHashMap<>(metricsMap);
}
}
4. 數(shù)據(jù)查詢接口
@RestController
@RequestMapping("/api/metrics")
@RequiredArgsConstructor
public class MetricsController {
private final MethodMetricsAspect metricsAspect;
@GetMapping
public Map<String, Object> getMetrics(
@RequestParam(required = false) Long startTime,
@RequestParam(required = false) Long endTime,
@RequestParam(required = false) String methodFilter) {
Map<String, Object> result = new HashMap<>();
Map<String, HierarchicalMethodMetrics> snapshot = metricsAspect.getMetricsSnapshot();
// 應用接口名過濾
if (StringUtils.hasText(methodFilter)) {
snapshot = snapshot.entrySet().stream()
.filter(entry -> entry.getKey().toLowerCase().contains(methodFilter.toLowerCase()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
// 時間范圍查詢
if (startTime != null && endTime != null) {
snapshot.forEach((methodName, metrics) -> {
HierarchicalMethodMetrics.TimeRangeMetrics timeRangeMetrics =
metrics.queryTimeRange(startTime, endTime);
Map<String, Object> metricData = buildTimeRangeMetricData(timeRangeMetrics);
result.put(methodName, metricData);
});
} else {
// 全量數(shù)據(jù)
snapshot.forEach((methodName, metrics) -> {
Map<String, Object> metricData = buildMetricData(metrics);
result.put(methodName, metricData);
});
}
return result;
}
@GetMapping("/recent/{minutes}")
public Map<String, Object> getRecentMetrics(
@PathVariable int minutes,
@RequestParam(required = false) String methodFilter) {
long endTime = System.currentTimeMillis();
long startTime = endTime - (minutes * 60L * 1000L);
return getMetrics(startTime, endTime, methodFilter);
}
@GetMapping("/summary")
public Map<String, Object> getSummary(
@RequestParam(required = false) Long startTime,
@RequestParam(required = false) Long endTime,
@RequestParam(required = false) String methodFilter) {
// 匯總統(tǒng)計邏輯
Map<String, HierarchicalMethodMetrics> snapshot = metricsAspect.getMetricsSnapshot();
// ... 匯總計算
return summary;
}
}
5. 定時清理服務
@Service
@RequiredArgsConstructor
public class MetricsCleanupService {
private final MethodMetricsAspect metricsAspect;
@Value("${dashboard.metrics.max-age:3600000}")
private long maxAge;
@Scheduled(fixedRateString = "${dashboard.metrics.cleanup-interval:300000}")
public void cleanupStaleMetrics() {
try {
metricsAspect.removeStaleMetrics(maxAge);
int currentMethodCount = metricsAspect.getMetricsSnapshot().size();
log.info("Metrics cleanup completed. Current methods being monitored: {}", currentMethodCount);
} catch (Exception e) {
log.error("Error during metrics cleanup", e);
}
}
}
五、前端可視化界面
核心功能實現(xiàn):
function metricsApp() {
return {
metrics: {},
summary: {},
timeRange: 'all',
methodFilter: '',
// 時間范圍設置
setTimeRange(range) {
this.timeRange = range;
this.updateTimeRangeText();
if (range !== 'custom') {
this.fetchMetrics();
this.fetchSummary();
}
},
// 構(gòu)建API查詢URL
buildApiUrl(endpoint) {
let url = `/api/metrics${endpoint}`;
const params = new URLSearchParams();
// 添加時間參數(shù)
if (this.timeRange !== 'all') {
if (this.timeRange === 'custom') {
if (this.customStartTime && this.customEndTime) {
params.append('startTime', new Date(this.customStartTime).getTime());
params.append('endTime', new Date(this.customEndTime).getTime());
}
} else {
const endTime = Date.now();
const startTime = endTime - (this.timeRange * 60 * 1000);
params.append('startTime', startTime);
params.append('endTime', endTime);
}
}
// 添加搜索參數(shù)
if (this.methodFilter.trim()) {
params.append('methodFilter', this.methodFilter.trim());
}
return params.toString() ? url + '?' + params.toString() : url;
},
// 獲取監(jiān)控數(shù)據(jù)
async fetchMetrics() {
this.loading = true;
try {
const response = await fetch(this.buildApiUrl(''));
this.metrics = await response.json();
this.lastUpdate = new Date().toLocaleTimeString();
} catch (error) {
console.error('Failed to fetch metrics:', error);
} finally {
this.loading = false;
}
}
};
}
六、配置說明
application.yml 配置
server:
port: 8080
spring:
application:
name: springboot-api-dashboard
aop:
auto: true
proxy-target-class: true
# 監(jiān)控配置
dashboard:
metrics:
cleanup-interval: 300000 # 清理間隔:5分鐘
max-age: 3600000 # 最大存活時間:1小時
debug-enabled: false # 調(diào)試模式
logging:
level:
com.example.dashboard: INFO
Maven 依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
七、使用示例
啟動應用
mvn clean install mvn spring-boot:run
訪問界面
http://localhost:8080/index.html

API 調(diào)用示例
# 獲取所有監(jiān)控數(shù)據(jù) curl http://localhost:8080/api/metrics # 獲取最近5分鐘的數(shù)據(jù) curl http://localhost:8080/api/metrics/recent/5 # 按時間范圍和接口名篩選 curl "http://localhost:8080/api/metrics?startTime=1640995200000&endTime=1641000000000&methodFilter=user" # 獲取匯總統(tǒng)計 curl http://localhost:8080/api/metrics/summary # 清空監(jiān)控數(shù)據(jù) curl -X DELETE http://localhost:8080/api/metrics
八、應用場景
性能分析 快速找到最慢的方法,定位性能瓶頸;
穩(wěn)定性監(jiān)控 發(fā)現(xiàn)失敗次數(shù)多的接口,提前預警;
容量評估 統(tǒng)計高頻調(diào)用方法,輔助系統(tǒng)擴容決策;
問題排查 結(jié)合時間段篩選,精確定位問題發(fā)生時間;
趨勢分析 通過不同時間粒度的數(shù)據(jù),分析接口性能趨勢。
九、優(yōu)勢特點
輕量級部署 無需外部依賴,單個 JAR 包即可運行;
即插即用 添加依賴后自動啟用,無需復雜配置;
資源友好 采用分級采樣策略,內(nèi)存占用可控;
十、總結(jié)
通過 Spring Boot AOP + 分級采樣 + 現(xiàn)代化前端,我們實現(xiàn)了一個功能完整的輕量級 APM 監(jiān)控系統(tǒng):
- 支持方法級監(jiān)控和時間段篩選
- 提供直觀的可視化界面和搜索功能
- 具備良好的性能表現(xiàn)和穩(wěn)定性
- 開箱即用,適合中小型項目快速集成
它不是 SkyWalking、Pinpoint 的替代品,但作為單機自研的小型 APM 解決方案,在簡單性和實用性之間取得了很好的平衡。對于不需要復雜分布式追蹤,但希望有基礎監(jiān)控能力的項目來說,這是一個不錯的選擇。
以上就是基于SpringBoot實現(xiàn)一個方法級耗時監(jiān)控器的詳細內(nèi)容,更多關(guān)于SpringBoot耗時監(jiān)控器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何在?Spring?Boot?中使用?OpenAI?ChatGPT?API
這篇文章主要介紹了如何在Spring?Boot中使用OpenAI?ChatGPT?API,我們探索了 OpenAI ChatGPT API 以生成對提示的響應,我們創(chuàng)建了一個 Spring Boot 應用程序,它調(diào)用 API 來生成對提示的響應,需要的朋友可以參考下2023-08-08
用IDEA創(chuàng)建SpringBoot項目的詳細步驟記錄
Idea有著非常簡便的Spring Boot新建過程,同時依靠pom自動下載依賴,下面這篇文章主要給大家介紹了關(guān)于用IDEA創(chuàng)建SpringBoot項目的詳細步驟,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2022-08-08
基于Java開發(fā)實現(xiàn)ATM系統(tǒng)
這篇文章主要為大家詳細介紹了基于Java開發(fā)實現(xiàn)ATM系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08
Java設計模式之策略模式的使用(Strategy?Pattern)
策略模式是一種行為型設計模式,用于定義一系列算法并將每個算法封裝起來,使它們可以互相替換,從而實現(xiàn)代碼的可維護性和靈活性,策略模式包含策略接口、具體策略類和上下文類,并通過將算法的選擇與使用分離,使得算法可以獨立變化2025-03-03
SpringMVC 通過commons-fileupload實現(xiàn)文件上傳功能
這篇文章主要介紹了SpringMVC 通過commons-fileupload實現(xiàn)文件上傳,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02
MySQL查詢字段實現(xiàn)字符串分割split功能的示例代碼
本文主要介紹了MySQL查詢字段實現(xiàn)字符串分割split功能的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01

