-
某些接口偶尔变慢,但日志看不出问题;
-
方法调用次数不透明,性能瓶颈难找;
-
线上出现失败/超时,但缺乏统计维度;
-
想要监控,却不想引入重量级的 APM 方案。
-
方法调用次数:统计某方法被调用了多少次
-
耗时指标:平均耗时、最大耗时、最小耗时
-
成功/失败次数:区分正常与异常调用
-
多维排序:支持按调用次数、平均耗时、失败次数等维度排序
-
时间段过滤:选择时间范围(如最近 5 分钟、1 小时、1 天)查看数据
-
接口搜索:快速定位特定接口的性能数据
-
可视化控制台:实时展示接口调用统计
-
最近5分钟:秒级精度统计
-
最近1小时:分钟级聚合
-
最近24小时:小时级聚合
-
最近7天:天级聚合
-
时间桶数据模型
@Data
publicclassTimeBucket {
privatefinalAtomicLongtotalCount=newAtomicLong(0);
privatefinalAtomicLongsuccessCount=newAtomicLong(0);
privatefinalAtomicLongfailCount=newAtomicLong(0);
privatefinalLongAddertotalTime=newLongAdder();
privatevolatilelongmaxTime=0;
privatevolatilelongminTime= Long.MAX_VALUE;
privatefinallong bucketStartTime;
privatevolatilelong lastUpdateTime;
publicTimeBucket(long bucketStartTime) {
this.bucketStartTime = bucketStartTime;
this.lastUpdateTime = System.currentTimeMillis();
}
publicsynchronizedvoidrecord(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();
}
publicdoublegetAvgTime() {
longtotal= totalCount.get();
return total == 0 ? 0.0 : (double) totalTime.sum() / total;
}
publicdoublegetSuccessRate() {
longtotal= totalCount.get();
return total == 0 ? 0.0 : (double) successCount.get() / total * 100;
}
}
-
分级采样指标模型
@Data
publicclassHierarchicalMethodMetrics {
// 基础统计信息
privatefinalAtomicLongtotalCount=newAtomicLong(0);
privatefinalAtomicLongsuccessCount=newAtomicLong(0);
privatefinalAtomicLongfailCount=newAtomicLong(0);
privatefinalLongAddertotalTime=newLongAdder();
privatevolatilelongmaxTime=0;
privatevolatilelongminTime= Long.MAX_VALUE;
privatefinal String methodName;
// 分级时间桶
privatefinal ConcurrentHashMap<Long, TimeBucket> secondBuckets = newConcurrentHashMap<>(); // 最近5分钟,秒级
privatefinal ConcurrentHashMap<Long, TimeBucket> minuteBuckets = newConcurrentHashMap<>(); // 最近1小时,分钟级
privatefinal ConcurrentHashMap<Long, TimeBucket> hourBuckets = newConcurrentHashMap<>(); // 最近24小时,小时级
privatefinal ConcurrentHashMap<Long, TimeBucket> dayBuckets = newConcurrentHashMap<>(); // 最近7天,天级
publicsynchronizedvoidrecord(long duration, boolean success) {
longcurrentTime= System.currentTimeMillis();
// 更新基础统计
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);
}
}
-
AOP 切面统计
@Slf4j
@Aspect
@Component
publicclassMethodMetricsAspect {
privatefinal ConcurrentHashMap<String, HierarchicalMethodMetrics> metricsMap = newConcurrentHashMap<>();
@Around("@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Controller)")
public Object recordMetrics(ProceedingJoinPoint joinPoint)throws Throwable {
StringmethodName= buildMethodName(joinPoint);
longstartTime= System.nanoTime();
booleansuccess=true;
try {
Objectresult= joinPoint.proceed();
return result;
} catch (Throwable throwable) {
success = false;
throw throwable;
} finally {
longduration= (System.nanoTime() - startTime) / 1_000_000; // Convert to milliseconds
metricsMap.computeIfAbsent(methodName, HierarchicalMethodMetrics::new)
.record(duration, success);
}
}
private String buildMethodName(ProceedingJoinPoint joinPoint) {
StringclassName= joinPoint.getTarget().getClass().getSimpleName();
StringmethodName= joinPoint.getSignature().getName();
return className + "." + methodName + "()";
}
public Map<String, HierarchicalMethodMetrics> getMetricsSnapshot() {
returnnewConcurrentHashMap<>(metricsMap);
}
}
-
数据查询接口
@RestController
@RequestMapping("/api/metrics")
@RequiredArgsConstructor
publicclassMetricsController {
privatefinal 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 = newHashMap<>();
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.TimeRangeMetricstimeRangeMetrics=
metrics.queryTimeRange(startTime, endTime);
Map<String, Object> metricData = buildTimeRangeMetricData(timeRangeMetrics);
result.put(methodName, metricData);
});
} else {
// 全量数据
snapshot.forEach((methodName, metrics) -> {
Map<String, Object> metricData = buildMetricData(metrics);
result.put(methodName, metricData);
});
}
return result;
}
@GetMapping("/recent/{minutes}")
public Map<String, Object> getRecentMetrics(
@PathVariableint minutes,
@RequestParam(required = false) String methodFilter) {
longendTime= System.currentTimeMillis();
longstartTime= 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) {
// 汇总统计逻辑
Map<String, HierarchicalMethodMetrics> snapshot = metricsAspect.getMetricsSnapshot();
// ... 汇总计算
return summary;
}
}
-
定时清理服务
@Service
@RequiredArgsConstructor
publicclassMetricsCleanupService {
privatefinal MethodMetricsAspect metricsAspect;
@Value("${dashboard.metrics.max-age:3600000}")
privatelong maxAge;
@Scheduled(fixedRateString = "${dashboard.metrics.cleanup-interval:300000}")
publicvoidcleanupStaleMetrics() {
try {
metricsAspect.removeStaleMetrics(maxAge);
intcurrentMethodCount= metricsAspect.getMetricsSnapshot().size();
log.info("Metrics cleanup completed. Current methods being monitored: {}", currentMethodCount);
} catch (Exception e) {
log.error("Error during metrics cleanup", e);
}
}
}
functionmetricsApp() {
return {
metrics: {},
summary: {},
timeRange: 'all',
methodFilter: '',
// 时间范围设置
setTimeRange(range) {
this.timeRange = range;
this.updateTimeRangeText();
if (range !== 'custom') {
this.fetchMetrics();
this.fetchSummary();
}
},
// 构建API查询URL
buildApiUrl(endpoint) {
let url = `/api/metrics${endpoint}`;
const params = newURLSearchParams();
// 添加时间参数
if (this.timeRange !== 'all') {
if (this.timeRange === 'custom') {
if (this.customStartTime && this.customEndTime) {
params.append('startTime', newDate(this.customStartTime).getTime());
params.append('endTime', newDate(this.customEndTime).getTime());
}
} else {
const endTime = Date.now();
const startTime = endTime - (this.timeRange * 60 * 1000);
params.append('startTime', startTime);
params.append('endTime', endTime);
}
}
// 添加搜索参数
if (this.methodFilter.trim()) {
params.append('methodFilter', this.methodFilter.trim());
}
return params.toString() ? url + '?' + params.toString() : url;
},
// 获取监控数据
asyncfetchMetrics() {
this.loading = true;
try {
const response = awaitfetch(this.buildApiUrl(''));
this.metrics = await response.json();
this.lastUpdate = newDate().toLocaleTimeString();
} catch (error) {
console.error('Failed to fetch metrics:', error);
} finally {
this.loading = false;
}
}
};
}
server:
port:8080
spring:
application:
name:springboot-api-dashboard
aop:
auto:true
proxy-target-class:true
# 监控配置
dashboard:
metrics:
cleanup-interval:300000# 清理间隔:5分钟
max-age:3600000 # 最大存活时间:1小时
debug-enabled:false # 调试模式
logging:
level:
com.example.dashboard:INFO
<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
# 获取所有监控数据
curl http://localhost:8080/api/metrics
# 获取最近5分钟的数据
curl http://localhost:8080/api/metrics/recent/5
# 按时间范围和接口名筛选
curl "http://localhost:8080/api/metrics?startTime=1640995200000&endTime=1641000000000&methodFilter=user"
# 获取汇总统计
curl http://localhost:8080/api/metrics/summary
# 清空监控数据
curl -X DELETE http://localhost:8080/api/metrics
-
支持方法级监控和时间段筛选
-
提供直观的可视化界面和搜索功能
-
具备良好的性能表现和稳定性
-
开箱即用,适合中小型项目快速集成
-
https://github.com/yuboon/java-examples/tree/master/springboot-api-dashboard
往期推荐
13 秒插入 30 万条数据,这才是批量插入正确的姿势!
7款颜值当道的 Linux 系统
IDEA 源码阅读利器,你居然还不会?
面试官:说一下SSO 单点登录和 OAuth2.0 的区别
SpringBoot 实现无痕调试注入器,线上问题定位的新利器
开源项目 | 一款强大的桌面远程控制软件

