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