全自動!Spring Boot 實現接口請求/響應日志記錄的高效方案
在當今微服務體系中,記錄接口調用日志不僅是排查問題的關鍵手段,也是保障系統合規與可審計的重要一環。然而傳統做法往往需要在攔截器或過濾器中編寫大量樣板代碼,以實現請求體緩存、響應內容重復讀取等功能,既繁瑣又容易遺漏。
好在 Spring Boot 已經提供了更現代化的方案 —— 利用 Actuator 內置功能,可以快速集成請求追蹤機制。本文將完整演示如何構建輕量、可擴展的 API 調用日志系統。
背景說明
在微服務網關、后端服務、BFF 層等多個場景下,開發者都需要了解:
- 用戶請求了什么接口?
- 響應結果是否正常?
- 調用耗時多少?
- 有沒有異常發生?
Spring Boot Actuator 提供了 /actuator/httptrace 或 /ac/httpexchanges(自定義路徑)等端點,可以追蹤最近的 HTTP 請求,但默認并不記錄請求體、響應體等關鍵內容。
接下來我們就基于 Spring Boot + Actuator 構建一套 API 日志系統,包含:
- 基于內存的請求記錄方案;
- 自定義持久化日志記錄方案(如 Redis);
- 自定義日志數據結構 HttpLog;
- 完整示例代碼與調用效果展示。
實戰部署
添加依賴模塊
在 pom.xml 中引入 Actuator 核心依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
啟用配置項
在 application.yml 中啟用 Actuator 的 HTTP 請求跟蹤端點:
management:
endpoints:
web:
base-path: /ac
httpexchanges:
recording:
enabled: true
這一步允許通過 /ac/httpexchanges 查詢接口調用情況,但要啟用實際記錄,還需要注冊 Repository Bean。
啟用請求記錄組件
內存版 Repository 配置
創建類 HttpTraceConfig.java:
package com.icoderoad.logtrace.config;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HttpTraceConfig {
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
InMemoryHttpExchangeRepository repository = new InMemoryHttpExchangeRepository();
repository.setCapacity(20); // 默認100條,這里改為20條
repository.setReverse(true); // 最新記錄排在前面
return repository;
}
}
此配置用于在內存中保存最近的請求記錄,適合調試使用。
模擬接口請求測試
創建接口 ApiController.java:
package com.icoderoad.logtrace.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/{id}")
public ResponseEntity<User> query(@PathVariable Long id) {
return ResponseEntity.ok(new User(id, "姓名 - " + id));
}
@GetMapping
public ResponseEntity<List<User>> list(@RequestParam String name) {
return ResponseEntity.ok(List.of(
new User(1L, name + " - 1"),
new User(2L, name + " - 2")
));
}
@GetMapping("/s/{type}")
public ResponseEntity<String> s(@PathVariable String type, @RequestParam String name) {
return ResponseEntity.ok(String.format("type: %s, name: %s", type, name));
}
public record User(Long id, String name) {}
}
通過訪問 /api/1、/api?name=test、/api/s/debug?name=test 等接口后,可訪問 /ac/httpexchanges 查看請求元數據(請求地址、狀態碼、耗時等)。
自定義持久化日志(Redis)
內存方案不適合生產系統,我們需要把日志存儲到 Redis 等持久介質中。
Redis 日志實現類
路徑:RedisHttpExchangeRepository.java
package com.icoderoad.logtrace.repository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import java.time.ZoneId;
import java.util.List;
@Component
public class RedisHttpExchangeRepository implements HttpExchangeRepository {
private final StringRedisTemplate redis;
private final ObjectMapper objectMapper;
public RedisHttpExchangeRepository(StringRedisTemplate redis, ObjectMapper objectMapper) {
this.redis = redis;
this.objectMapper = objectMapper;
}
@Override
public void add(HttpExchange exchange) {
try {
HttpLog log = new HttpLog();
log.setTimestamp(exchange.getTimestamp().atZone(ZoneId.systemDefault()).toLocalDateTime());
log.setTimeTaken(exchange.getTimeTaken());
log.setPrincipal(exchange.getPrincipal());
HttpLog.Request req = new HttpLog.Request();
BeanUtils.copyProperties(exchange.getRequest(), req);
log.setRequest(req);
HttpLog.Response resp = new HttpLog.Response();
BeanUtils.copyProperties(exchange.getResponse(), resp);
log.setResponse(resp);
redis.opsForList().leftPush("http:request:list", objectMapper.writeValueAsString(log));
} catch (JsonProcessingException e) {
// ignore
}
}
@Override
public List<HttpExchange> findAll() {
List<String> rawList = redis.opsForList().range("http:request:list", 0, -1);
return rawList.stream().map(record -> {
try {
HttpLog log = objectMapper.readValue(record, HttpLog.class);
HttpExchange.Request request = new HttpExchange.Request(
log.getRequest().getUri(),
log.getRequest().getRemoteAddress(),
log.getRequest().getMethod(),
log.getRequest().getHeaders()
);
HttpExchange.Response response = new HttpExchange.Response(
log.getResponse().getStatus(),
log.getResponse().getHeaders()
);
return new HttpExchange(
log.getTimestamp().atZone(ZoneId.systemDefault()).toInstant(),
request, response, log.getPrincipal(), null, log.getTimeTaken()
);
} catch (Exception e) {
return null;
}
}).filter(e -> e != null).toList();
}
}
日志結構對象定義
路徑:HttpLog.java
package com.icoderoad.logtrace.model;
import java.net.URI;
import java.security.Principal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
public class HttpLog {
private LocalDateTime timestamp;
private Request request;
private Response response;
private Principal principal;
private Duration timeTaken;
public static class Request {
private URI uri;
private String remoteAddress;
private String method;
private Map<String, List<String>> headers;
// Getters and Setters
}
public static class Response {
private int status;
private Map<String, List<String>> headers;
// Getters and Setters
}
// Getters and Setters for HttpLog
}
總結
盡管 /ac/httpexchanges 接口能快速調試查看請求歷史,但它提供的數據相對有限,無法滿足生產需求。
如果你希望:
- 記錄完整請求體/響應體;
- 保存歷史數據供 ELK/SLS 分析;
- 接入自定義日志分析服務;