Spring Boot Actuator 自定义记录 HTTP 服务请求
1. 概述
在 Spring Boot 项目中,如果需要完整记录 HTTP 服务请求,例如:
- 请求方式:GET、POST、PUT、DELETE
- 请求地址
- QueryString
- Request Body
- Response Body
- HTTP 状态码
- 请求耗时
- 客户端 IP
- User-Agent
- TraceId
- 异常请求
推荐采用:
text
Spring Boot Actuator
+
OncePerRequestFilter
+
ContentCachingRequestWrapper
+
ContentCachingResponseWrapper
需要特别注意:
Spring Boot Actuator 本身并不适合直接记录完整的 Request Body 和 Response Body。
Actuator 更适合提供:
- 健康检查
- JVM 指标
- HTTP 请求指标
- 自定义监控端点
而 HTTP 请求、响应内容的采集建议通过 Filter 完成。
最终架构如下:
text
HTTP Request
│
▼
┌─────────────────────┐
│ RequestLogFilter │
│ OncePerRequestFilter│
└──────────┬──────────┘
│
▼
ContentCachingRequestWrapper
ContentCachingResponseWrapper
│
▼
Spring MVC Controller
│
▼
Service层
│
▼
Response
│
▼
Filter获取Body
│
▼
RequestLog
│
▼
异步保存 / 日志系统
│
▼
/actuator/request-logs
2. 添加 Spring Boot Actuator
Maven 添加:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
如果项目同时使用 Spring MVC:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
3. 配置 Actuator
application.yml:
yaml
management:
endpoints:
web:
exposure:
include:
- health
- info
- metrics
- loggers
endpoint:
health:
show-details: always
启动项目后访问:
text
http://localhost:8080/actuator
可以看到 Actuator 暴露的端点。
例如:
text
/actuator
/actuator/health
/actuator/info
/actuator/metrics
/actuator/loggers
4. 为什么不能直接读取 HttpServletRequest Body
很多人会在 Filter 中直接:
java
InputStream inputStream = request.getInputStream();
String body = new String(
inputStream.readAllBytes(),
StandardCharsets.UTF_8
);
这种方式存在严重问题。
HTTP Request Body 本质上是一个输入流。
如果 Filter 首先读取:
text
HTTP Request
│
▼
Filter
│
▼
读取 InputStream
│
▼
Controller
│
▼
@RequestBody
那么 Controller 再读取 Body 时可能已经没有数据。
例如:
java
@PostMapping("/user")
public User add(@RequestBody User user) {
return user;
}
可能出现:
text
Request Body 已经被 Filter 消耗
↓
@RequestBody 无法读取
↓
JSON 解析失败
因此应该使用:
java
ContentCachingRequestWrapper
5. ContentCachingRequestWrapper
Spring 提供:
java
org.springframework.web.util.ContentCachingRequestWrapper
它可以在 Request Body 被业务代码读取后,将内容缓存起来。
执行过程:
text
HTTP Request
│
▼
ContentCachingRequestWrapper
│
├──────────────► Controller读取Body
│
▼
缓存Request Body
│
▼
Filter读取缓存
因此 Filter 可以在业务代码执行完成之后:
java
requestWrapper.getContentAsByteArray()
获取 Request Body。
6. ContentCachingResponseWrapper
Response 同样存在类似问题。
Spring 提供:
java
org.springframework.web.util.ContentCachingResponseWrapper
它可以缓存 Controller 返回的 Response Body。
使用:
java
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper(response);
业务执行:
java
filterChain.doFilter(requestWrapper, responseWrapper);
然后:
java
byte[] body = responseWrapper.getContentAsByteArray();
获取 Response Body。
最后必须:
java
responseWrapper.copyBodyToResponse();
否则可能导致客户端收不到正常的响应内容。
7. 最简单的 RequestLogFilter
创建:
text
src/main/java/com/example/demo/filter/RequestLogFilter.java
代码:
java
package com.example.demo.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// 不记录 Actuator 自身请求
return request.getRequestURI().startsWith("/actuator/");
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper =
new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper(response);
long startTime = System.currentTimeMillis();
try {
// 执行业务代码
filterChain.doFilter(
requestWrapper,
responseWrapper
);
} finally {
long cost =
System.currentTimeMillis() - startTime;
String queryString =
requestWrapper.getQueryString();
String requestBody =
getRequestBody(requestWrapper);
String responseBody =
getResponseBody(responseWrapper);
System.out.println("================ HTTP REQUEST ================");
System.out.println(
"Method : "
+ requestWrapper.getMethod()
);
System.out.println(
"URI : "
+ requestWrapper.getRequestURI()
);
System.out.println(
"QueryString : "
+ queryString
);
System.out.println(
"RequestBody : "
+ requestBody
);
System.out.println(
"Status : "
+ responseWrapper.getStatus()
);
System.out.println(
"ResponseBody: "
+ responseBody
);
System.out.println(
"Cost : "
+ cost
+ " ms"
);
System.out.println("==============================================");
// 非常重要
responseWrapper.copyBodyToResponse();
}
}
/**
* 获取 Request Body
*/
private String getRequestBody(
ContentCachingRequestWrapper request) {
byte[] content =
request.getContentAsByteArray();
if (content.length == 0) {
return "";
}
return new String(
content,
StandardCharsets.UTF_8
);
}
/**
* 获取 Response Body
*/
private String getResponseBody(
ContentCachingResponseWrapper response) {
byte[] content =
response.getContentAsByteArray();
if (content.length == 0) {
return "";
}
return new String(
content,
StandardCharsets.UTF_8
);
}
}
8. GET 请求记录
例如:
http
GET /api/user/list?page=1&size=20
Filter 可以获取:
text
Method : GET
URI : /api/user/list
QueryString : page=1&size=20
RequestBody :
如果需要完整 URL:
java
String url = requestWrapper.getRequestURL().toString();
String queryString = requestWrapper.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
url += "?" + queryString;
}
最终:
text
http://localhost:8080/api/user/list?page=1&size=20
9. POST JSON 请求
例如:
http
POST /api/user/add
Content-Type: application/json
Request Body:
json
{
"name": "张三",
"age": 20
}
Filter 可以记录:
text
Method : POST
URI : /api/user/add
QueryString :
RequestBody : {"name":"张三","age":20}
Status : 200
ResponseBody: {"code":200,"message":"success"}
Cost : 18 ms
10. QueryString + Body
如果要求:
请求参数 = QueryString + Request Body
建议不要简单拼接成字符串,而是结构化保存。
例如:
json
{
"method": "POST",
"uri": "/api/user/add",
"queryString": "source=web&version=1",
"requestBody": {
"name": "张三",
"age": 20
},
"responseBody": {
"code": 200,
"message": "success"
}
}
这样后续存储到:
- MySQL
- Elasticsearch
- MongoDB
- Loki
- ClickHouse
都会更加方便。
11. 定义 RequestLog 对象
建议定义统一的日志对象:
java
package com.example.demo.model;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class RequestLog {
/**
* 请求时间
*/
private LocalDateTime requestTime;
/**
* 请求方式
*/
private String method;
/**
* 请求地址
*/
private String uri;
/**
* QueryString
*/
private String queryString;
/**
* Request Body
*/
private String requestBody;
/**
* Response Body
*/
private String responseBody;
/**
* HTTP状态码
*/
private Integer status;
/**
* 请求耗时
*/
private Long cost;
/**
* 客户端IP
*/
private String clientIp;
/**
* User-Agent
*/
private String userAgent;
/**
* TraceId
*/
private String traceId;
}
12. 在 Filter 中构建 RequestLog
java
RequestLog log = RequestLog.builder()
.requestTime(LocalDateTime.now())
.method(requestWrapper.getMethod())
.uri(requestWrapper.getRequestURI())
.queryString(requestWrapper.getQueryString())
.requestBody(getRequestBody(requestWrapper))
.responseBody(getResponseBody(responseWrapper))
.status(responseWrapper.getStatus())
.cost(cost)
.clientIp(requestWrapper.getRemoteAddr())
.userAgent(requestWrapper.getHeader("User-Agent"))
.traceId(getTraceId(requestWrapper))
.build();
然后交给:
java
requestLogService.save(log);
13. 获取客户端 IP
最简单:
java
request.getRemoteAddr();
但是如果系统部署在:
text
Nginx
↓
Gateway
↓
Spring Boot
那么:
java
request.getRemoteAddr()
可能得到的是:
text
127.0.0.1
而不是用户真实 IP。
可以根据实际代理配置读取:
java
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip;
}
生产环境需要结合网关/Nginx的可信代理配置,避免客户端伪造这些 Header。
14. TraceId
微服务系统强烈建议增加:
text
TraceId
例如:
text
用户请求
│
▼
Gateway
│
│ traceId=abc123
▼
User Service
│
▼
Order Service
│
▼
Payment Service
所有服务日志:
text
traceId=abc123
这样可以快速定位一次完整请求经过了哪些服务。
如果项目使用:
text
Micrometer Tracing
可以直接接入 TraceId。
15. 文件上传必须特殊处理
不能直接记录:
http
POST /upload
Content-Type: multipart/form-data
因为文件可能达到:
text
10 MB
100 MB
1 GB
如果全部缓存:
java
getContentAsByteArray()
可能造成:
text
内存暴涨
↓
GC频繁
↓
OOM
建议:
java
private boolean isFileUpload(
HttpServletRequest request) {
String contentType =
request.getContentType();
return contentType != null
&& contentType.startsWith("multipart/");
}
记录:
text
RequestBody: [FILE_UPLOAD]
而不是实际文件内容。
16. Body 大小限制
建议限制最大记录长度。
例如:
java
private static final int MAX_BODY_SIZE = 10 * 1024;
实现:
java
private String getRequestBody(
ContentCachingRequestWrapper request) {
byte[] content =
request.getContentAsByteArray();
if (content.length == 0) {
return "";
}
if (content.length > MAX_BODY_SIZE) {
return new String(
content,
0,
MAX_BODY_SIZE,
StandardCharsets.UTF_8
) + "...[TRUNCATED]";
}
return new String(
content,
StandardCharsets.UTF_8
);
}
Response Body 同样建议限制。
17. 敏感数据脱敏
这是生产环境必须考虑的问题。
例如请求:
json
{
"username": "zhangsan",
"password": "123456",
"token": "eyJhbGci...",
"phone": "13800138000"
}
不能直接保存成日志。
应该:
json
{
"username": "zhangsan",
"password": "***",
"token": "***",
"phone": "138****8000"
}
建议至少处理:
text
password
passwd
token
access_token
refresh_token
authorization
cookie
secret
apiKey
idCard
phone
bankCard
18. Authorization 不建议记录
例如:
http
Authorization: Bearer eyJhbGciOi...
这个 Header 本质上可能就是用户的认证凭证。
因此不建议把:
text
Authorization
Cookie
Set-Cookie
直接写入请求日志。
如果确实需要记录 Header,应当做白名单:
java
private static final Set<String> ALLOWED_HEADERS =
Set.of(
"Content-Type",
"Accept",
"User-Agent"
);
而不是记录所有 Header。
19. 不要在 Filter 中同步写数据库
不推荐:
java
finally {
requestLogRepository.save(log);
}
请求执行过程:
text
HTTP Request
↓
业务代码
↓
业务数据库
↓
请求日志数据库
↓
HTTP Response
这样会增加接口响应时间。
更严重的是:
text
1000 QPS
意味着日志数据库可能同时承受:
text
1000 次/秒
的写入。
20. 使用 @Async 异步保存
简单方案可以:
java
@Service
public class RequestLogService {
@Async
public void save(RequestLog log) {
requestLogRepository.save(log);
}
}
启动:
java
@EnableAsync
@SpringBootApplication
public class Application {
}
Filter:
java
requestLogService.save(log);
这样 HTTP 请求线程不需要等待日志数据库操作完成。
21. 生产环境更推荐消息队列
如果系统请求量比较大:
text
HTTP
│
▼
RequestLogFilter
│
▼
RequestLog
│
▼
Kafka / RabbitMQ / Redis Stream
│
▼
Log Consumer
│
├── MySQL
├── Elasticsearch
└── ClickHouse
优势:
text
业务请求
│
▼
快速完成日志投递
│
▼
异步消费
│
▼
持久化
避免日志数据库异常直接影响业务请求。
22. 自定义 Actuator Endpoint
如果希望通过:
text
/actuator/request-logs
查看请求日志,可以创建自定义 Endpoint。
java
package com.example.demo.actuator;
import com.example.demo.model.RequestLog;
import com.example.demo.service.RequestLogService;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Endpoint(id = "request-logs")
public class RequestLogEndpoint {
private final RequestLogService requestLogService;
public RequestLogEndpoint(
RequestLogService requestLogService) {
this.requestLogService = requestLogService;
}
@ReadOperation
public List<RequestLog> logs() {
return requestLogService.findLatest(100);
}
}
23. 暴露自定义 Endpoint
application.yml:
yaml
management:
endpoints:
web:
exposure:
include:
- health
- info
- metrics
- loggers
- request-logs
然后访问:
text
GET /actuator/request-logs
返回:
json
[
{
"requestTime": "2026-09-04T08:30:12",
"method": "POST",
"uri": "/api/user",
"queryString": "id=100",
"requestBody": "{\"name\":\"张三\"}",
"responseBody": "{\"code\":200}",
"status": 200,
"cost": 32,
"clientIp": "192.168.1.10",
"traceId": "abc123"
}
]
24. Spring Boot Actuator 的职责
需要明确区分:
text
HTTP监控体系
│
┌──────────────┴──────────────┐
│ │
▼ ▼
请求日志采集 Actuator
│ │
▼ ▼
Filter + Wrapper Metrics / Health
│ │
▼ ▼
Request Body 请求数量
Response Body 请求耗时
URL JVM
QueryString 数据库连接
Status 系统健康
Actuator 更适合:
text
/actuator/health
/actuator/info
/actuator/metrics
/actuator/loggers
而完整 HTTP Body 日志应该由:
text
Filter
完成。
25. 推荐的最终项目结构
建议:
text
src/main/java
└── com.example.demo
│
├── actuator
│ └── RequestLogEndpoint.java
│
├── filter
│ └── RequestLogFilter.java
│
├── model
│ └── RequestLog.java
│
├── service
│ └── RequestLogService.java
│
├── mapper
│ └── RequestLogMapper.java
│
└── Application.java
26. 推荐的完整请求日志字段
生产环境建议至少保存:
| 字段 | 说明 |
|---|---|
| id | 日志ID |
| request_time | 请求时间 |
| trace_id | 链路ID |
| method | 请求方式 |
| uri | 请求地址 |
| query_string | QueryString |
| request_body | Request Body |
| response_body | Response Body |
| status | HTTP状态码 |
| cost | 请求耗时 |
| client_ip | 客户端IP |
| user_agent | User-Agent |
| user_id | 当前用户ID |
| exception | 异常信息 |
| created_at | 创建时间 |
27. 数据库表设计示例
如果使用 MySQL:
sql
CREATE TABLE sys_request_log (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
request_time DATETIME NOT NULL COMMENT '请求时间',
trace_id VARCHAR(64) DEFAULT NULL COMMENT 'TraceId',
method VARCHAR(20) DEFAULT NULL COMMENT '请求方式',
uri VARCHAR(1000) DEFAULT NULL COMMENT '请求地址',
query_string TEXT COMMENT 'QueryString',
request_body MEDIUMTEXT COMMENT 'Request Body',
response_body MEDIUMTEXT COMMENT 'Response Body',
status INT DEFAULT NULL COMMENT 'HTTP状态码',
cost BIGINT DEFAULT NULL COMMENT '请求耗时,单位ms',
client_ip VARCHAR(128) DEFAULT NULL COMMENT '客户端IP',
user_agent VARCHAR(1000) DEFAULT NULL COMMENT 'User-Agent',
user_id VARCHAR(64) DEFAULT NULL COMMENT '用户ID',
exception TEXT COMMENT '异常信息',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_trace_id (trace_id),
KEY idx_request_time (request_time),
KEY idx_uri (uri),
KEY idx_status (status)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COMMENT='HTTP请求日志';
28. 异常请求也必须记录
Filter 推荐使用:
java
try {
filterChain.doFilter(
requestWrapper,
responseWrapper
);
} finally {
// 记录日志
responseWrapper.copyBodyToResponse();
}
不要只在:
java
try
里面记录。
因为 Controller 可能出现:
text
RuntimeException
BusinessException
NullPointerException
SQLException
如果日志只写在正常流程:
java
filterChain.doFilter();
saveLog();
异常情况下:
text
Controller异常
↓
saveLog()不会执行
最终最需要排查的异常请求反而没有日志。
因此建议:
text
try
↓
执行请求
↓
finally
↓
无论成功失败都记录
29. 不建议记录的请求
建议配置 URL 黑名单:
text
/actuator/**
/swagger-ui/**
/v3/api-docs/**
/favicon.ico
/static/**
例如:
java
@Override
protected boolean shouldNotFilter(
HttpServletRequest request) {
String uri = request.getRequestURI();
return uri.startsWith("/actuator/")
|| uri.startsWith("/swagger-ui/")
|| uri.startsWith("/v3/api-docs/")
|| uri.equals("/favicon.ico");
}
30. Body 类型建议过滤
不是所有请求都适合记录 Body。
建议:
记录
text
application/json
application/xml
application/x-www-form-urlencoded
text/plain
不记录
text
multipart/form-data
application/octet-stream
image/*
video/*
audio/*
可以:
java
private boolean shouldRecordBody(
HttpServletRequest request) {
String contentType =
request.getContentType();
if (contentType == null) {
return false;
}
return contentType.startsWith("application/json")
|| contentType.startsWith("application/xml")
|| contentType.startsWith("application/x-www-form-urlencoded")
|| contentType.startsWith("text/");
}
31. 推荐最终架构
对于一般企业级 Spring Boot 项目,推荐:
text
Client
│
▼
Nginx / Gateway
│
▼
┌─────────────────────┐
│ Spring Boot │
│ │
│ RequestLogFilter │
│ │ │
│ ▼ │
│ Request Wrapper │
│ │ │
│ ▼ │
│ Controller │
│ │ │
│ ▼ │
│ Service │
│ │ │
│ ▼ │
│ Response │
└────────┬────────────┘
│
▼
RequestLog
│
┌───────┴────────┐
│ │
▼ ▼
脱敏处理 Body限制
│ │
└───────┬────────┘
│
▼
异步队列
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
MySQL Elasticsearch Loki
│
▼
Actuator Endpoint
│
▼
/actuator/request-logs
32. 最终推荐方案
如果只是开发环境:
text
Spring Boot
+
OncePerRequestFilter
+
ContentCachingRequestWrapper
+
ContentCachingResponseWrapper
即可。
如果是生产环境:
text
Spring Boot
+
RequestLogFilter
+
ContentCachingRequestWrapper
+
ContentCachingResponseWrapper
+
Body大小限制
+
URL过滤
+
Content-Type过滤
+
敏感数据脱敏
+
TraceId
+
异步日志
+
MySQL / Elasticsearch
+
Actuator
33. 核心结论
整个方案可以总结成:
text
Actuator
│
┌────────────┴────────────┐
│ │
▼ ▼
Metrics 自定义Endpoint
│ │
│ ▼
│ /actuator/request-logs
│
▼
HTTP请求指标
HTTP请求
│
▼
RequestLogFilter
│
├── Method
├── URI
├── QueryString
├── Request Body
├── Response Body
├── Status
├── Cost
├── Client IP
├── User ID
└── TraceId
│
▼
脱敏 + 大小限制
│
▼
异步保存
│
▼
MySQL / ES / Loki
核心技术点只有两个:
java
ContentCachingRequestWrapper
负责缓存 Request Body。
java
ContentCachingResponseWrapper
负责缓存 Response Body。
然后通过:
java
OncePerRequestFilter
统一采集请求信息。
而:
text
Spring Boot Actuator
负责提供监控指标和自定义管理端点。
因此,不建议把"完整 HTTP 请求日志"理解成 Actuator 的功能,而应该理解成"Filter 采集 + Actuator 管理/暴露"。
34. 下一步完善建议
如果用于实际生产项目,建议继续完善以下能力:
- 完整可运行的 Spring Boot 3.x + JDK 17 示例
- MyBatis-Plus 持久化
- MySQL 建表 + Entity + Mapper + Service
- Request/Response JSON 自动格式化
- 密码、Token、手机号、身份证号自动脱敏
- Request/Response Body 最大长度限制
- 文件上传自动跳过
- 异常请求完整记录
- TraceId 自动生成和传递
- 用户 ID 自动获取
- URL 白名单/黑名单
- 异步写入,避免影响接口性能
- 自定义
/actuator/request-logs - 按 URI、状态码、时间范围查询
- 日志自动清理/保留策略
- 高并发下的性能优化
- ELK / Loki 日志方案
对于正式生产系统,建议最终采用:
text
HTTP
↓
RequestLogFilter
↓
采集请求/响应
↓
脱敏
↓
TraceId
↓
异步队列
↓
Elasticsearch / Loki
↓
Grafana / Kibana
而不是让每一次 HTTP 请求直接同步写 MySQL。