全自動(dòng)!Spring Boot 實(shí)現(xiàn)接口請(qǐng)求/響應(yīng)日志記錄的高效方案
在當(dāng)今微服務(wù)體系中,記錄接口調(diào)用日志不僅是排查問題的關(guān)鍵手段,也是保障系統(tǒng)合規(guī)與可審計(jì)的重要一環(huán)。然而傳統(tǒng)做法往往需要在攔截器或過濾器中編寫大量樣板代碼,以實(shí)現(xiàn)請(qǐng)求體緩存、響應(yīng)內(nèi)容重復(fù)讀取等功能,既繁瑣又容易遺漏。
好在 Spring Boot 已經(jīng)提供了更現(xiàn)代化的方案 —— 利用 Actuator 內(nèi)置功能,可以快速集成請(qǐng)求追蹤機(jī)制。本文將完整演示如何構(gòu)建輕量、可擴(kuò)展的 API 調(diào)用日志系統(tǒng)。
背景說明
在微服務(wù)網(wǎng)關(guān)、后端服務(wù)、BFF 層等多個(gè)場(chǎng)景下,開發(fā)者都需要了解:
- 用戶請(qǐng)求了什么接口?
- 響應(yīng)結(jié)果是否正常?
- 調(diào)用耗時(shí)多少?
- 有沒有異常發(fā)生?
Spring Boot Actuator 提供了 /actuator/httptrace 或 /ac/httpexchanges(自定義路徑)等端點(diǎn),可以追蹤最近的 HTTP 請(qǐng)求,但默認(rèn)并不記錄請(qǐng)求體、響應(yīng)體等關(guān)鍵內(nèi)容。
接下來我們就基于 Spring Boot + Actuator 構(gòu)建一套 API 日志系統(tǒng),包含:
- 基于內(nèi)存的請(qǐng)求記錄方案;
- 自定義持久化日志記錄方案(如 Redis);
- 自定義日志數(shù)據(jù)結(jié)構(gòu) HttpLog;
- 完整示例代碼與調(diào)用效果展示。
實(shí)戰(zhàn)部署
添加依賴模塊
在 pom.xml 中引入 Actuator 核心依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
啟用配置項(xiàng)
在 application.yml 中啟用 Actuator 的 HTTP 請(qǐng)求跟蹤端點(diǎn):
management:
endpoints:
web:
base-path: /ac
httpexchanges:
recording:
enabled: true
這一步允許通過 /ac/httpexchanges 查詢接口調(diào)用情況,但要啟用實(shí)際記錄,還需要注冊(cè) Repository Bean。
啟用請(qǐng)求記錄組件
內(nèi)存版 Repository 配置
創(chuàng)建類 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); // 默認(rèn)100條,這里改為20條
repository.setReverse(true); // 最新記錄排在前面
return repository;
}
}
此配置用于在內(nèi)存中保存最近的請(qǐng)求記錄,適合調(diào)試使用。
模擬接口請(qǐng)求測(cè)試
創(chuàng)建接口 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 查看請(qǐng)求元數(shù)據(jù)(請(qǐng)求地址、狀態(tài)碼、耗時(shí)等)。
自定義持久化日志(Redis)
內(nèi)存方案不適合生產(chǎn)系統(tǒng),我們需要把日志存儲(chǔ)到 Redis 等持久介質(zhì)中。
Redis 日志實(shí)現(xiàn)類
路徑: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();
}
}
日志結(jié)構(gòu)對(duì)象定義
路徑: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
}
總結(jié)
盡管 /ac/httpexchanges 接口能快速調(diào)試查看請(qǐng)求歷史,但它提供的數(shù)據(jù)相對(duì)有限,無法滿足生產(chǎn)需求。
如果你希望:
- 記錄完整請(qǐng)求體/響應(yīng)體;
- 保存歷史數(shù)據(jù)供 ELK/SLS 分析;
- 接入自定義日志分析服務(wù);