使用ContentCachingRequestWrapper读取请求体为空的问题求助
问题
我正在构建一个Spring Boot项目,要实现Controller请求日志记录机制,用@Aspect拦截日志。由于HttpServletRequest的getInputStream()默认只能读取一次请求体,我采用ContentCachingRequestWrapper包装请求以实现重复读取,但方案未生效。尝试通过requestWrapper.getReader()转换请求体为空字符串,甚至自定义HttpServletRequestWrapper子类也无改善。
AppFilter 代码
import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.util.ContentCachingRequestWrapper; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component public class AppFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest currentRequest = (HttpServletRequest) servletRequest; ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest); filterChain.doFilter(wrappedRequest, servletResponse); } }
RequestLogger 代码
import com.google.gson.Gson; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.util.ContentCachingRequestWrapper; import javax.servlet.http.HttpServletRequest; import javax.validation.constraints.NotNull; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Aspect @Component @Log4j2 public class RequestLogger { @Pointcut("@annotation(Loggers)") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) { } @AfterReturning(value = "webLog()", returning = "ret") public void doAfterReturning(Object ret) { } @Around("webLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { Object response = joinPoint.proceed(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { return response; } HttpServletRequest request = attributes.getRequest(); Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); this.log(joinPoint, request, method, response); return response; } private void log(ProceedingJoinPoint joinPoint, HttpServletRequest request, Method method, Object response) { RequestInfo requestInfo = this.getRequestInfo(joinPoint, request, method, response); String requestBody = ""; try { ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request; requestBody = requestWrapper.getReader().lines().collect(Collectors.joining("\n")); // 此处为空字符串 } catch (Throwable e) { e.printStackTrace(); } ResponseEntity res = (ResponseEntity) response; Gson gson = new Gson(); String message = String.format("Done %s to %s with status %s request: %s response: %s", requestInfo.getRequestMethod(), requestInfo.getRequestPath(), requestInfo.getResponseStatus(), requestBody, gson.toJson(res.getBody()) ); doLog(requestInfo, message); } @NotNull private RequestInfo getRequestInfo( ProceedingJoinPoint joinPoint, HttpServletRequest request, Method method, Object response ) { // 格式化请求信息 return new RequestInfo(); // 假设该类已定义,此处补全返回 } private Object getParameter(Method method, Object[] args) { // 读取请求参数 return null; } private void doLog(RequestInfo requestInfo, String message) { // 日志输出逻辑 log.info(message); } }
请求日志输出
2023-04-19 09:12:12.517 INFO 12548 --- [nio-8080-exec-2] v.k.infrastructure.logger.RequestLogger : Done POST to /api/v1/sms/invitation with status 200 request: response: {"message":"OK","code":200,"timeStamp":1681870332503}
解决方案
问题根源
- 读取方式错误:
ContentCachingRequestWrapper的设计是通过内部缓存字节数组存储请求体,而非允许重复调用getReader()/getInputStream()。你调用getReader()时,流已经被Spring MVC读取到末尾,返回空字符串。 - Filter执行顺序可能滞后:如果Filter在
DispatcherServlet之后执行,请求体已经被读取,包装类无法完成缓存。
修复步骤
1. 替换请求体读取方式
改用ContentCachingRequestWrapper提供的getContentAsByteArray()方法直接获取缓存的字节数组,转换为字符串:
private void log(ProceedingJoinPoint joinPoint, HttpServletRequest request, Method method, Object response) { RequestInfo requestInfo = this.getRequestInfo(joinPoint, request, method, response); String requestBody = ""; try { if (request instanceof ContentCachingRequestWrapper) { ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request; byte[] contentBytes = requestWrapper.getContentAsByteArray(); if (contentBytes.length > 0) { requestBody = new String(contentBytes, request.getCharacterEncoding()); } } } catch (Throwable e) { e.printStackTrace(); } // 后续日志逻辑不变 }
2. 确保Filter优先执行
给Filter添加@Order注解,让它在所有Filter中最先执行,保证请求被提前包装:
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class AppFilter extends GenericFilterBean { // 原有代码不变 }
3. 额外说明
- 当Controller使用
@RequestBody时,Spring MVC会自动读取请求体,此时ContentCachingRequestWrapper已完成缓存,在joinPoint.proceed()之后读取缓存是可行的。 - 若自定义
HttpServletRequestWrapper,需自行实现请求体的缓存逻辑:在构造方法中读取请求体到字节数组,重写getInputStream()和getReader()方法返回基于该数组的流。
内容的提问来源于stack exchange,提问作者Thành Đặng
相关产品推荐
相关产品推荐

