一、需求痛点
线上应用常见问题:
某些接口偶尔变慢,但日志看不出问题;
方法调用次数不透明,性能瓶颈难找;
线上出现失败/超时,但缺乏统计维度;
想要监控,却不想引入重量级的 APM 方案。
常见 APM 工具功能强大,但部署复杂、学习成本高,不适合中小团队或者单机项目。
👉 那有没有可能,基于 SpringBoot 实现一个轻量级耗时监控器,做到方法级监控 + 可视化统计 ?
二、功能目标
我们希望监控器能做到:
基础监控能力:
- 方法调用次数:统计某方法被调用了多少次
- 耗时指标:平均耗时、最大耗时、最小耗时
- 成功/失败次数:区分正常与异常调用
- 多维排序:支持按调用次数、平均耗时、失败次数等维度排序
进阶功能:
- 时间段过滤:选择时间范围(如最近 5 分钟、1 小时、1 天)查看数据
- 接口搜索:快速定位特定接口的性能数据
- 可视化控制台:实时展示接口调用统计
三、技术设计
整体思路
方法切面采集 使用 SpringAOP(基于拦截器亦可) 拦截 Controller 方法,在方法执行前后记录时间差、执行结果(成功/失败)。
分级数据存储 采用分级时间桶策略:
- 最近5分钟:秒级精度统计
- 最近1小时:分钟级聚合
- 最近24小时:小时级聚合
- 最近7天:天级聚合
智能查询 根据查询时间范围,自动选择最适合的数据粒度进行聚合计算。
接口展示 提供 REST API 输出统计数据,前端使用 TailwindCSS + Alpine.js 渲染界面。
四、核心实现
1. 时间桶数据模型
java
@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. 分级采样指标模型
java
@Data
public class HierarchicalMethodMetrics {
// 基础统计信息
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();
// 更新基础统计
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 切面统计
java
@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. 数据查询接口
java
@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 {
// 全量数据
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) {
// 汇总统计逻辑
Map<String, HierarchicalMethodMetrics> snapshot = metricsAspect.getMetricsSnapshot();
// ... 汇总计算
return summary;
}
}
5. 定时清理服务
java
@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);
}
}
}
五、前端可视化界面
核心功能实现:
javascript
function metricsApp() {
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 = new URLSearchParams();
// 添加时间参数
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);
}
}
// 添加搜索参数
if (this.methodFilter.trim()) {
params.append('methodFilter', this.methodFilter.trim());
}
return params.toString() ? url + '?' + params.toString() : url;
},
// 获取监控数据
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 配置
yaml
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
Maven 依赖
xml
<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>
七、使用示例
启动应用
bash
mvn clean install
mvn spring-boot:run
访问界面
http://localhost:8080/index.html

API 调用示例
bash
# 获取所有监控数据
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
八、应用场景
性能分析 快速找到最慢的方法,定位性能瓶颈;
稳定性监控 发现失败次数多的接口,提前预警;
容量评估 统计高频调用方法,辅助系统扩容决策;
问题排查 结合时间段筛选,精确定位问题发生时间;
趋势分析 通过不同时间粒度的数据,分析接口性能趋势。
九、优势特点
轻量级部署 无需外部依赖,单个 JAR 包即可运行;
即插即用 添加依赖后自动启用,无需复杂配置;
资源友好 采用分级采样策略,内存占用可控;
十、总结
通过 Spring Boot AOP + 分级采样 + 现代化前端,我们实现了一个功能完整的轻量级 APM 监控系统:
- 支持方法级监控和时间段筛选
- 提供直观的可视化界面和搜索功能
- 具备良好的性能表现和稳定性
- 开箱即用,适合中小型项目快速集成
它不是 SkyWalking、Pinpoint 的替代品,但作为单机自研的小型 APM 解决方案,在简单性和实用性之间取得了很好的平衡。对于不需要复杂分布式追踪,但希望有基础监控能力的项目来说,这是一个不错的选择。