1. 概述

本文将带你为 Spring REST API 集成基础的监控指标(Metrics)功能

我们会先通过 Servlet Filter 实现一套简单的指标收集机制,然后再使用 Spring Boot Actuator 模块来实现更标准、更强大的方案。整个过程从零开始,适合想快速落地 API 监控的团队参考。

✅ 适用场景:生产环境 API 调用统计、状态码分布、趋势分析
❌ 不适用:高精度实时监控、分布式链路追踪(需搭配 Prometheus + Grafana 等)


2. web.xml 配置 Filter

如果你还在使用传统 Spring MVC(非 Boot),可以在 web.xml 中注册一个自定义的 Filter 来拦截所有请求:

<filter>
    <filter-name>metricFilter</filter-name>
    <filter-class>org.baeldung.metrics.filter.MetricFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>metricFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

⚠️ 注意:/* 表示拦截所有请求路径,可根据实际需要调整为 /api/* 等更细粒度的规则。

这个配置非常直接,但关键在于 MetricFilter 的实现逻辑。


3. 自定义 Servlet Filter

public class MetricFilter implements Filter {

    private MetricService metricService;

    @Override
    public void init(FilterConfig config) throws ServletException {
        metricService = (MetricService) WebApplicationContextUtils
         .getRequiredWebApplicationContext(config.getServletContext())
         .getBean("metricService");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws java.io.IOException, ServletException {
        HttpServletRequest httpRequest = ((HttpServletRequest) request);
        String req = httpRequest.getMethod() + " " + httpRequest.getRequestURI();

        chain.doFilter(request, response);

        int status = ((HttpServletResponse) response).getStatus();
        metricService.increaseCount(req, status);
    }
}

关键点解析:

  • ❗ Filter 不是 Spring Bean,无法使用 @Autowired,必须通过 WebApplicationContextUtils 手动获取 Bean。
  • chain.doFilter() 必须调用,否则请求会被阻断。
  • ⚠️ 状态码获取放在 chain.doFilter() 之后,确保响应已生成。

这是典型的“环绕式”拦截思路,适合做耗时统计、异常捕获、指标记录等操作。


4. 基础指标:HTTP 状态码计数

我们先实现一个简单的内存版 MetricService,记录各状态码出现次数:

@Service
public class MetricService {

    private Map<Integer, Integer> statusMetric;

    public MetricService() {
        statusMetric = new ConcurrentHashMap<>();
    }
    
    public void increaseCount(String request, int status) {
        Integer statusCount = statusMetric.get(status);
        if (statusCount == null) {
            statusMetric.put(status, 1);
        } else {
            statusMetric.put(status, statusCount + 1);
        }
    }

    public Map getStatusMetric() {
        return statusMetric;
    }
}

使用 ConcurrentHashMap 保证线程安全,虽然精度不是 100%,但在大多数场景下足够用了(后文会提到踩坑点)。

暴露一个接口查看数据:

@GetMapping(value = "/status-metric")
@ResponseBody
public Map getStatusMetric() {
    return metricService.getStatusMetric();
}

返回示例:

{  
    "404": 1,
    "200": 6,
    "409": 1
}

简单粗暴,一眼看出 404 出现了一次,可能是某个资源不存在。


5. 进阶指标:按请求维度统计状态码

只看状态码不够,我们更关心“哪个接口”返回了什么状态。于是升级 MetricService

@Service
public class MetricService {

    private Map<String, Map<Integer, Integer>> metricMap;

    public void increaseCount(String request, int status) {
        Map<Integer, Integer> statusMap = metricMap.get(request);
        if (statusMap == null) {
            statusMap = new ConcurrentHashMap<>();
        }

        Integer count = statusMap.get(status);
        if (count == null) {
            count = 1;
        } else {
            count++;
        }
        statusMap.put(status, count);
        metricMap.put(request, statusMap);
    }

    public Map getFullMetric() {
        return metricMap;
    }
}

新增一个接口暴露数据:

@GetMapping(value = "/metric")
@ResponseBody
public Map getMetric() {
    return metricService.getFullMetric();
}

返回示例:

{
    "GET /users": {
        "200": 6,
        "409": 1
    },
    "GET /users/1": {
        "404": 1
    }
}

解读:

  • GET /users 被调用了 7 次,6 次成功,1 次冲突(409)
  • GET /users/1 被调用 1 次,返回 404

这种结构非常适合做接口健康度分析。


6. 时间序列指标:每分钟状态码统计

纯累计值时间久了就没意义了。我们需要带时间维度的数据,比如“每分钟各状态码出现次数”。

改造 MetricService

@Service
public class MetricService {

    private static final SimpleDateFormat DATE_FORMAT = 
      new SimpleDateFormat("yyyy-MM-dd HH:mm");
    private Map<String, Map<Integer, Integer>> timeMap;

    public void increaseCount(String request, int status) {
        String time = DATE_FORMAT.format(new Date());
        Map<Integer, Integer> statusMap = timeMap.get(time);
        if (statusMap == null) {
            statusMap = new ConcurrentHashMap<>();
        }

        Integer count = statusMap.get(status);
        if (count == null) {
            count = 1;
        } else {
            count++;
        }
        statusMap.put(status, count);
        timeMap.put(time, statusMap);
    }
}

每分钟一个 key,比如 "2025-04-05 10:30",value 是该分钟内各状态码的计数。

提供一个方法生成图表所需数据:

public Object[][] getGraphData() {
    int colCount = statusMetric.keySet().size() + 1;
    Set<Integer> allStatus = statusMetric.keySet();
    int rowCount = timeMap.keySet().size() + 1;
    
    Object[][] result = new Object[rowCount][colCount];
    result[0][0] = "Time";

    int j = 1;
    for (int status : allStatus) {
        result[0][j] = status;
        j++;
    }
    int i = 1;
    Map<Integer, Integer> tempMap;
    for (Entry<String, Map<Integer, Integer>> entry : timeMap.entrySet()) {
        result[i][0] = entry.getKey();
        tempMap = entry.getValue();
        for (j = 1; j < colCount; j++) {
            result[i][j] = tempMap.get(result[0][j]);
            if (result[i][j] == null) {
                result[i][j] = 0;
            }
        }
        i++;
    }

    for (int k = 1; k < result[0].length; k++) {
        result[0][k] = result[0][k].toString();
    }

    return result; 
}

前端接口:

@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
    return metricService.getGraphData();
}

最后用 Google Charts 渲染图表:

<html>
<head>
<title>Metric Graph</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages : [ "corechart" ]});

function drawChart() {
$.get("/metric-graph-data",function(mydata) {
    var data = google.visualization.arrayToDataTable(mydata);
    var options = {title : 'Website Metric',
                   hAxis : {title : 'Time',titleTextStyle : {color : '#333'}},
                   vAxis : {minValue : 0}};

    var chart = new google.visualization.AreaChart(document.getElementById('chart_div'));
    chart.draw(data, options);

});
}
</script>
</head>
<body onload="drawChart()">
    <div id="chart_div" style="width: 900px; height: 500px;"></div>
</body>
</html>

✅ 效果:折线图/面积图展示每分钟状态码趋势,直观发现异常波动。


7. 使用 Spring Boot 1.x Actuator

Spring Boot 提供了 spring-boot-starter-actuator 模块,内置了丰富的监控能力。我们来整合它。

7.1 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

7.2 将 Filter 改造成 Spring Bean

之前手动获取 metricService 太麻烦,现在可以直接注入:

@Component
public class MetricFilter implements Filter {

    @Autowired
    private MetricService metricService;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws java.io.IOException, ServletException {
        chain.doFilter(request, response);

        int status = ((HttpServletResponse) response).getStatus();
        metricService.increaseCount(status);
    }
}

✅ 简洁多了,Spring 全权管理生命周期。

7.3 使用 CounterService 记录指标

Actuator 提供了 CounterService,专门用于计数:

@Service
public class MetricService {

    @Autowired
    private CounterService counter;

    private List<String> statusList;

    public void increaseCount(int status) {
        counter.increment("status." + status);
        if (!statusList.contains("counter.status." + status)) {
            statusList.add("counter.status." + status);
        }
    }
}

计数器名称自动加上 counter. 前缀,如 counter.status.200

7.4 使用 MetricRepository 导出指标

我们可以通过 MetricRepository 获取计数器值,并按分钟导出:

@Service
public class MetricService {

    @Autowired
    private MetricRepository repo;

    private List<List<Integer>> statusMetric;
    private List<String> statusList;
    
    @Scheduled(fixedDelay = 60000)
    private void exportMetrics() {
        Metric<?> metric;
        List<Integer> statusCount = new ArrayList<>();
        for (String status : statusList) {
            metric = repo.findOne(status);
            if (metric != null) {
                statusCount.add(metric.getValue().intValue());
                repo.reset(status);
            } else {
                statusCount.add(0);
            }
        }
        statusMetric.add(statusCount);
    }
}

⚠️ 注意:repo.reset(status) 重置计数器,避免重复累加。

7.5 使用 PublicMetrics 获取指标

更推荐的方式是使用 PublicMetrics 接口,它能获取所有公开指标:

@Autowired
private MetricReaderPublicMetrics publicMetrics;

private List<List<Integer>> statusMetricsByMinute;
private List<String> statusList;

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    List<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());
    for (Metric<?> counterMetric : publicMetrics.metrics()) {
        updateMetrics(counterMetric, lastMinuteStatuses);
    }
    statusMetricsByMinute.add(lastMinuteStatuses);
}

private void updateMetrics(Metric<?> counterMetric, List<Integer> statusCount) {
    if (counterMetric.getName().contains("counter.status.")) {
        String status = counterMetric.getName().substring(15, 18);
        appendStatusIfNotExist(status, statusCount);
        int index = statusList.indexOf(status);
        int oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
        statusCount.set(index, counterMetric.getValue().intValue() + oldCount);
    }
}

最终通过 getGraphData() 返回二维数组供前端绘图。

返回示例:

[
    ["Time","counter.status.302","counter.status.200","counter.status.304"],
    ["2015-03-26 19:59",3,12,7],
    ["2015-03-26 20:00",0,4,1]
]

8. 使用 Spring Boot 2.x Actuator

Spring Boot 2.x 彻底重构了 Actuator,原生指标系统被替换为 Micrometer。这是目前的主流做法。

8.1 用 MeterRegistry 替代 CounterService

Micrometer 是事实上的 Java 应用指标标准,自动集成在 spring-boot-starter-actuator 中。

@Autowired
private MeterRegistry registry;

private List<String> statusList;

@Override
public void increaseCount(int status) {
    String counterName = "counter.status." + status;
    registry.counter(counterName).increment(1);
    if (!statusList.contains(counterName)) {
        statusList.add(counterName);
    }
}

MeterRegistry 是 Micrometer 的核心接口,支持多种指标类型(Counter、Gauge、Timer 等)。

8.2 查看自定义指标

先确保在 application.yml 中启用 metrics 接口:

management:
  endpoints:
    web:
      exposure:
        include: metrics

访问 http://localhost:8080/actuator/metrics 可看到所有指标名:

{
  "names": [
    "application.ready.time",
    "application.started.time",
    "counter.status.200",
    "disk.free",
    "disk.total"
  ]
}

再访问 http://localhost:8080/actuator/metrics/counter.status.200 查看具体值:

{
  "name": "counter.status.200",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 2
    }
  ],
  "availableTags": []
}

8.3 使用 MeterRegistry 导出计数

定时任务中导出每分钟数据:

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    List<Integer> statusCount = new ArrayList<>();
    for (String status : statusList) {
        Search search = registry.find(status);
        Counter counter = search.counter();
         if (counter == null) {
             statusCount.add(0);
         } else {
             statusCount.add((int) counter.count());
             registry.remove(counter); // 清零
         }
    }
    statusMetricsByMinute.add(statusCount);
}

8.4 使用 Meters 获取所有指标

类似 PublicMetrics,Micrometer 提供 registry.getMeters() 获取所有 Meter:

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    List<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());

    for (Meter counterMetric : registry.getMeters()) {
        updateMetrics(counterMetric, lastMinuteStatuses);
    }
    statusMetricsByMinute.add(lastMinuteStatuses);
}

private void updateMetrics(Meter counterMetric, List<Integer> statusCount) {
    String metricName = counterMetric.getId().getName();
    if (metricName.contains("counter.status.")) {
        String status = metricName.substring(15, 18);
        appendStatusIfNotExist(status, statusCount);
        int index = statusList.indexOf(status);
        int oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
        statusCount.set(index, (int)((Counter) counterMetric).count() + oldCount);
    }
}

9. 总结

本文从零实现了一套 Spring REST API 的监控指标系统,涵盖:

  • ✅ 基于 Filter 的请求拦截
  • ✅ 状态码统计(全局、按接口、按时间)
  • ✅ 集成 Spring Boot Actuator(1.x 和 2.x)
  • ✅ 使用 Micrometer 构建现代指标体系
  • ✅ 前端图表展示

踩坑提醒:

  • ⚠️ 计数器非绝对线程安全,高并发下可能有微小误差,但趋势分析足够。
  • ⚠️ 内存存储不适合长期运行,建议定期持久化或对接 Prometheus。
  • ✅ 推荐生产使用 Micrometer + Prometheus + Grafana 组合,本文方案适合快速验证。

完整代码已开源:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-actuator


原始标题:Metrics for Your Spring REST API