SpringBoot 方法级耗时监控器

一、需求痛点

线上应用常见问题:

某些接口偶尔变慢,但日志看不出问题;

方法调用次数不透明,性能瓶颈难找;

线上出现失败/超时,但缺乏统计维度;

想要监控,却不想引入重量级的 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 解决方案,在简单性和实用性之间取得了很好的平衡。对于不需要复杂分布式追踪,但希望有基础监控能力的项目来说,这是一个不错的选择。

github.com/yuboon/java...

相关推荐
深圳蔓延科技2 小时前
Kafka + Spring Boot 终极整合指南
后端·kafka
Sui_Network2 小时前
GraphQL RPC 与通用索引器公测介绍:为 Sui 带来更强大的数据层
javascript·人工智能·后端·rpc·去中心化·区块链·graphql
BingoGo2 小时前
PHP serialize 序列化完全指南
后端·php
枫叶V2 小时前
Go 实现大文件分片上传与断点续传
后端·go
Cache技术分享2 小时前
186. Java 模式匹配 - Java 21 新特性:Record Pattern(记录模式匹配)
前端·javascript·后端
福大大架构师每日一题2 小时前
2025-09-12:删除元素后 K 个字符串的最长公共前缀。用go语言,给定一个字符串数组 words 和一个整数 k。对于数组中每个位置 i,先把下标为 i
后端
Python私教3 小时前
Django全栈班v1.01 Python简介与特点 20250910
后端·python·django
AAA修煤气灶刘哥3 小时前
从 Timer 到 XXL-Job,定时任务调度的 “进化史”,看完再也不怕漏跑任务~
java·后端·架构
zjjuejin3 小时前
Docker Swarm 完全指南:从原理到实战
后端·docker