在 Spring Boot 中可通过继承 OncePerRequestFilter 并使用 ContentCachingRequestWrapper/ResponseWrapper 包装请求响应,实现安全、可重复读取的全量请求/响应日志记录,需注意流复用、敏感信息脱敏、内容截断及响应体回写。

在 Java 的 Spring Boot 项目中,可以通过实现 Filter 接口,在请求进入 Controller 前、响应返回客户端后,统一记录完整的 Request(含 URL、Method、Headers、Body)和 Response 信息。关键在于正确读取并复用请求/响应流,避免因流被消费导致后续处理失败。
使用 OncePerRequestFilter 保证单次执行
OncePerRequestFilter 是 Spring 提供的包装类,能确保每个请求只被该 Filter 执行一次(尤其在异步或转发场景下),比直接实现 Filter 更安全可靠。
- 继承
OncePerRequestFilter,重写doFilterInternal - 将原始
HttpServletRequest和HttpServletResponse包装为可重复读取的自定义 wrapper(如ContentCachingRequestWrapper和ContentCachingResponseWrapper) - 调用
chain.doFilter()后再读取缓存内容,避免 body 流已关闭
正确读取 Request Body 需要包装请求对象
原生 HttpServletRequest 的 input stream 只能读一次。不包装直接调用 getInputStream() 或 getReader(),会导致 Controller 拿不到数据。
- 使用
ContentCachingRequestWrapper:Spring 自带,自动缓存 body 到内存(注意不要用于超大文件上传) - 读取时用
byte[] content = wrapper.getContentAsByteArray(),再转成字符串(注意字符编码,默认 UTF-8) - 对 GET 请求,body 通常为空,应优先记录 query string;POST/PUT 等才需关注 body
记录日志内容建议包含的关键字段
一条完整请求日志应至少涵盖以下信息,便于问题排查和审计:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
-
时间戳:精确到毫秒,例如
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) -
请求 ID:通过
UUID.randomUUID()或 MDC 注入 traceId(配合 Sleuth 更佳) -
客户端 IP:用
request.getRemoteAddr(),若经 Nginx,应读X-Forwarded-For头 -
HTTP 方法 + 路径 + 查询参数:如
POST /api/users?size=10 - Headers(敏感头如 Authorization、Cookie 建议脱敏)
- Request Body(限长打印,如前 1024 字符,避免日志爆炸)
- 响应状态码 + 响应耗时(ms)
- Response Body(同理限长,且仅记录成功或 debug 级别)
示例代码片段(Spring Boot 2.x+)
注册一个全局日志 Filter:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestLoggingFilter extends OncePerRequestFilter {
<pre class='brush:java;toolbar:false;'>private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
// 包装请求和响应
ContentCachingRequestWrapper wrappedRequest =
new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse =
new ContentCachingResponseWrapper(response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
long duration = System.currentTimeMillis() - startTime;
logRequest(wrappedRequest, wrappedResponse, duration);
// 必须调用 copyBodyToResponse,否则响应内容不会写回客户端
wrappedResponse.copyBodyToResponse();
}
}
private void logRequest(ContentCachingRequestWrapper request,
ContentCachingResponseWrapper response,
long duration) {
String method = request.getMethod();
String uri = request.getRequestURL().toString();
String queryString = request.getQueryString();
if (queryString != null) uri += "?" + queryString;
String clientIp = getClientIpAddress(request);
String requestBody = getRequestBody(request);
String responseBody = getResponseBody(response);
log.info("[{}] {} {} | {}ms | IP: {} | Body: {} | Resp: {}",
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
method, uri, duration, clientIp,
truncate(requestBody, 512), truncate(responseBody, 512));
}
private String getClientIpAddress(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
return xff != null && !xff.isEmpty() ? xff.split(",")[0].trim() : request.getRemoteAddr();
}
private String getRequestBody(ContentCachingRequestWrapper request) {
byte[] content = request.getContentAsByteArray();
if (content.length == 0) return "";
try {
return new String(content, StandardCharsets.UTF_8);
} catch (Exception e) {
return "[INVALID-UTF8]";
}
}
private String getResponseBody(ContentCachingResponseWrapper response) {
byte[] content = response.getContentAsByteArray();
if (content.length == 0) return "";
try {
return new String(content, StandardCharsets.UTF_8);
} catch (Exception e) {
return "[INVALID-UTF8]";
}
}
private String truncate(String s, int limit) {
return s != null && s.length() > limit ? s.substring(0, limit) + "..." : s;
}}

















