知識庫 / Spring RSS 訂閱

Spring REST API 性能指標

REST,Spring
HongKong
5
04:11 AM · Dec 06 ,2025

1. 概述

本教程將整合基本指標到 Spring REST API 中。

我們將首先使用簡單的 Servlet 過濾器構建指標功能,然後使用 Spring Boot Actuator 模塊。

2. Web.xml

讓我們首先在我們的應用程序的 web.xml 中註冊一個過濾器 – "MetricFilter":

<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>

請注意我們如何將過濾器映射到所有傳入的請求上——,當然,它完全可配置。

3. 自定義 Servlet 過濾器

現在,讓我們創建一個自定義過濾器:

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);
    }
}

由於過濾器不是標準 Bean,因此我們不會注入 metricService,而是通過 ServletContext 檢索它。

此外,我們通過調用 doFilter API 繼續執行過濾器鏈。

4. 指標 – 狀態碼計數

接下來,讓我們來查看我們的簡單 InMemoryMetricService

@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;
    }
}

我們使用內存中的 ConcurrentMap 來存儲每個 HTTP 狀態碼的計數。

現在 – 為了顯示此基本指標 – 我們將其映射到一個 Controller 方法上:

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

以下是一個示例響應:

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

5. 按請求 – 請求計數指標

接下來,讓我們記錄按請求的計數指標

@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;
    }
}

我們將通過 API 顯示指標結果:

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

以下是這些指標的顯示效果:

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

根據以上示例,API 的活動如下:

  • “7” 個請求發送到 “GET /users”
  • 其中“6” 個請求返回了 “200” 狀態碼響應,只有“1” 個返回了 “409” 狀態碼

6. 指標 – 時間序列數據

總體計數在應用程序中具有一定用處,但如果系統運行時間較長——很難判斷這些指標的實際含義

需要時間上下文,以便數據具有意義並易於解釋。

現在讓我們構建一個簡單的基於時間的指標,我們將記錄每分鐘的狀態碼計數,如下所示:

@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);
    }
}

以及

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; 
}

我們現在將此映射到 API:

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

最後,我們將使用 Google 圖表將其渲染出來:

<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 1.x Actuator 的功能來展示我們的指標。

首先,我們需要將 actuator 依賴添加到我們的 pom.xml 中:

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

7.1. 指標過濾器

接下來,我們可以將 指標過濾器 變成一個實際的 Spring Bean:

@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);
    }
}

當然,這只是一個輕微的簡化——但為了擺脱之前手動配置依賴關係,這樣做仍然值得一做。

7.2. 使用 CounterService

現在,讓我們使用 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);
        }
    }
}

7.3. 使用 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);
    }
}

請注意,我們存儲的是每分鐘的狀態碼數量

7.4. Spring Boot PublicMetrics

我們可以使用 Spring Boot PublicMetrics 來導出指標,而不是使用我們自己的過濾器——如下所示:

首先,我們有用於每分鐘導出指標的計劃任務:

@Autowired
private MetricReaderPublicMetrics publicMetrics;

private List<List<Integer>> statusMetricsByMinute;
private List<String> statusList;
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

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

我們當然需要初始化 HTTP 狀態碼列表:

private List<Integer> initializeStatuses(int size) {
    List<Integer> counterList = new ArrayList<>();
    for (int i = 0; i < size; i++) {
        counterList.add(0);
    }
    return counterList;
}

然後,我們將實際更新指標,包括狀態碼計數

private void updateMetrics(Metric<?> counterMetric, List<Integer> statusCount) {

    if (counterMetric.getName().contains("counter.status.")) {
        String status = counterMetric.getName().substring(15, 18); // example 404, 200
        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);
    }
}

private void appendStatusIfNotExist(String status, List<Integer> statusCount) {
    if (!statusList.contains(status)) {
        statusList.add(status);
        statusCount.add(0);
    }
}

請注意:

  • PublicMetics 狀態計數器名稱以“”開頭,例如“
  • 我們記錄每分鐘的狀態計數數據,存儲在列表

我們可以將我們收集的數據導出,以便繪製成圖表 – 如下所示:

public Object[][] getGraphData() {
    Date current = new Date();
    int colCount = statusList.size() + 1;
    int rowCount = statusMetricsByMinute.size() + 1;
    Object[][] result = new Object[rowCount][colCount];
    result[0][0] = "Time";
    int j = 1;

    for (String status : statusList) {
        result[0][j] = status;
        j++;
    }

    for (int i = 1; i < rowCount; i++) {
        result[i][0] = dateFormat.format(
          new Date(current.getTime() - (60000L * (rowCount - i))));
    }

    List<Integer> minuteOfStatuses;
    List<Integer> last = new ArrayList<Integer>();

    for (int i = 1; i < rowCount; i++) {
        minuteOfStatuses = statusMetricsByMinute.get(i - 1);
        for (j = 1; j <= minuteOfStatuses.size(); j++) {
            result[i][j] = 
              minuteOfStatuses.get(j - 1) - (last.size() >= j ? last.get(j - 1) : 0);
        }
        while (j < colCount) {
            result[i][j] = 0;
            j++;
        }
        last = minuteOfStatuses;
    }
    return result;
}

7.5. 使用指標繪製圖表

最後,讓我們通過二維數組來表示這些指標,以便我們可以繪製圖表:

public Object[][] getGraphData() {
    Date current = new Date();
    int colCount = statusList.size() + 1;
    int rowCount = statusMetric.size() + 1;
    Object[][] result = new Object[rowCount][colCount];
    result[0][0] = "Time";

    int j = 1;
    for (String status : statusList) {
        result[0][j] = status;
        j++;
    }

    ArrayList<Integer> temp;
    for (int i = 1; i < rowCount; i++) {
        temp = statusMetric.get(i - 1);
        result[i][0] = dateFormat.format
          (new Date(current.getTime() - (60000L * (rowCount - i))));
        for (j = 1; j <= temp.size(); j++) {
            result[i][j] = temp.get(j - 1);
        }
        while (j < colCount) {
            result[i][j] = 0;
            j++;
        }
    }

    return result;
}

以下是我們的 Controller 方法 getMetricData():

@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
    return metricService.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 中,Spring Actuator 的 API 經歷了重大變更。 Spring 本身的指標已被 Micrometer 替換。 因此,讓我們用 Micrometer 編寫上述相同的指標示例。

8.1. 替換 CounterServiceMeterRegistry

由於我們的 Spring Boot 應用程序已經依賴於 Actuator starter,因此 Micrometer 已經自動配置。我們可以注入 MeterRegistry,而不是 CounterService。我們可以使用不同類型的 Meter 來捕獲指標。 Counter 是其中之一。

@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);
    }
}

8.2. 查看自定義指標

由於我們的指標已與 Micrometer 註冊,首先,請在應用程序配置中啓用它們。 這樣,您可以通過導航到 Actuator 端點 /actuator/metrics 來查看它們。

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

我們可以在這裏看到我們的 counter.status.200 指標列出了標準 Actuator 指標中的一項。此外,通過在 URI 中提供選擇器,還可以獲取該指標的最新值,例如:/actuator/metrics/counter.status.200

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

8.3. 使用 MeterRegistry 導出計數

在 Micrometer 中,可以使用 MeterRegistry 導出 Counter 的值。

@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(counter != null ? ((int) counter.count()) : 0);
             registry.remove(counter);
         }
    }
    statusMetricsByMinute.add(statusCount);
}

8.3. 使用 Meters 的發佈指標

現在,我們還可以使用 MeterRegistryMeters 發佈指標。

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

    for (Meter counterMetric : publicMetrics.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); // example 404, 200
        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 Web 應用程序中構建一些基本指標能力的方法。

請注意,這些計數器不可保證線程安全——因此,在不使用諸如原子數字之類的東西時,它們可能不準確。這主要是因為增量應該很小,100% 的準確性並不是目標——而是要儘早發現趨勢。

當然,還有更成熟的方法可以在應用程序中記錄 HTTP 指標,但這種方法簡單、輕量級且非常實用,無需使用完整的工具帶來的額外複雜性。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.