You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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}
解决方案

问题根源

  1. 读取方式错误:ContentCachingRequestWrapper的设计是通过内部缓存字节数组存储请求体,而非允许重复调用getReader()/getInputStream()。你调用getReader()时,流已经被Spring MVC读取到末尾,返回空字符串。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 09:37:52