Spring Boot REST接口反序列化前如何记录指定端点原始请求体
可行实现方案(均不破坏原有@RequestBody自动反序列化与@Valid参数校验能力)
方案1:使用RequestBodyAdvice(精准匹配指定端点,推荐)
Spring 提供的RequestBodyAdvice接口专门用于增强@RequestBody的处理逻辑,其beforeBodyRead方法会在请求体反序列化为Java对象之前触发,完美匹配需求场景。
实现代码如下:
import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import org.springframework.util.StreamUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ControllerAdvice public class NotificationRequestBodyLogAdvice extends RequestBodyAdviceAdapter { private static final Logger log = LoggerFactory.getLogger(NotificationRequestBodyLogAdvice.class); @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { // 仅匹配目标控制器的目标方法,无额外性能损耗 return methodParameter.getContainingClass() == NotificationController.class && "handleNotification".equals(methodParameter.getMethod().getName()); } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { // 读取原始请求体 String rawBody = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8); // 记录日志,可自行追加链路ID、请求IP等排查字段 log.info("/api/notification 原始请求体: {}", rawBody); // 重新构造请求流返回给后续反序列化、校验逻辑使用 return new HttpInputMessage() { @Override public InputStream getBody() { return new ByteArrayInputStream(rawBody.getBytes(StandardCharsets.UTF_8)); } @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; } }
该方案优势:
- 匹配逻辑精准,仅作用于目标接口,不会影响其他业务端点
- 完全无侵入,不需要修改原有Controller代码
- 触发时机正好在反序列化之前,即使后续反序列化/校验抛出异常也已经完成原始请求体记录
方案2:使用ContentCachingRequestWrapper + 过滤器(适合多端点扩展)
如果后续需要对多个接口增加原始请求体日志能力,可以通过过滤器 + Spring 自带的请求包装类实现,代码如下:
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.util.AntPathMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component public class RawRequestLogFilter extends OncePerRequestFilter { private static final Logger log = LoggerFactory.getLogger(RawRequestLogFilter.class); private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); private static final String TARGET_PATH = "/api/notification"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 包装原生请求,将请求体缓存到内存中,支持多次读取 ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); try { // 继续执行后续请求逻辑(包括反序列化、参数校验、业务处理) filterChain.doFilter(wrappedRequest, response); } finally { // 仅匹配目标端点时输出日志 if (PATH_MATCHER.match(TARGET_PATH, request.getRequestURI())) { String rawBody = new String(wrappedRequest.getContentAsByteArray(), StandardCharsets.UTF_8); log.info("/api/notification 原始请求体: {}", rawBody); } } } }
注意点:
- 日志记录放在finally块中,即使请求处理过程中抛出任何异常也能正常落盘原始请求体,满足链路追溯要求
ContentCachingRequestWrapper会自动处理请求体缓存,不会影响后续@RequestBody读取请求流
内容的提问来源于stack exchange,提问作者Robert Strauch
相关产品推荐
相关产品推荐

