SpringBoot解决Request和Response的内容多次读取的问题

在开发web服务的时候,一般都会对Request或Response进行多次读取,比如在过滤器或拦截器中,统一打印收到request和响应reponse的内容日志,方便查询问题。但是如果是使用SpringBoot Web默认的HttpServletRequest或HttpServletResponse时,这两个对象的数据流只能被读取一次,比如如果在拦截器中读取了request的内容,那么在Controller中取参数时,数据变为成空字符串了。现在SpringBoot也意识到了这个问题,提供了两个Request和Reponse的包装类,可以解决这个问题,但是也有几个小坑,需要注意回避一下。

请求包装类:ContentCachingRequestWrapper

这个是对HttpServletRequest的包装类,在使用它的时候,一般会调用它的这两个方法:

但是可能会让你失望,如果是在拦截器的preHandler里面直接调用,返回是空字符串或字节长度是0的数组,因为只有它包括的类中的getInputStream被读取之后,包装类才会有缓存,如果是在Controller调用之后再调用,是没有问题的。

为了解决在preHandler中调用返回为空的问题,需要稍等修改一下,重写一下getInputStream方法:

复制代码
package cn.jw.starter.web.core.interceptors;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import org.springframework.util.StreamUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;

/**
 *
 * @author 王广帅
 * @version 1.0.0
 * @since 2026/2/7 23:25
 */
public class JwContentCachingRequestWrapper extends ContentCachingRequestWrapper {

    public JwContentCachingRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
         // 先读取一次数据,再使用
        StreamUtils.copyToByteArray(super.getInputStream());
        final ByteArrayInputStream in = new ByteArrayInputStream(this.getContentAsByteArray());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return true;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return in.read();
            }
        };
    }
}

响应包装类:ContentCachingResponseWrapper

这个是HttpServletRequest的包装,在Filter中包装之后,Controller类层可以直接使用,但是Controller处理之后,响应的数据是会缓存在包装类中,而真正的HttpServletResponse中是没有数据的。因此,需要在响应前,在过滤器过拦截器中进行一次数据的复制:

复制代码
    /**
     * 更新响应(不操作这一步,会导致接口响应空白)
     *
     * @param response
     *            响应对象
     * @throws IOException
     *             /
     */
    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (responseWrapper != null) {
            Objects.requireNonNull(responseWrapper).copyBodyToResponse();
        }
    }

完整代码

过滤器中添加包装类

复制代码
package cn.jw.starter.web.core.filters;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import cn.hutool.extra.spring.SpringUtil;
import cn.jw.starter.web.core.interceptors.JwContentCachingRequestWrapper;
import cn.jw.starter.web.core.properties.JwWebProperties;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * 缓存请求消息的filter,
 *
 * @author 王广帅
 * @since 2024/4/3 20:31
 */
public class CacheRequestBodyFilter extends OncePerRequestFilter {

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String uri = request.getRequestURI();
        // 静态资源都不要走这个过滤
        if (uri.endsWith(".html") || uri.endsWith(".js") || uri.endsWith(".css") || uri.endsWith(".ico")) {
            return true;
        }
        JwWebProperties jwWebProperties = SpringUtil.getBean(JwWebProperties.class);
        if (jwWebProperties.getIgnoreCacheBodyUriList().contains(uri)) {
            return true;
        }
        return super.shouldNotFilter(request);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        if (request.getContentLength() > 0 && !(request instanceof JwContentCachingRequestWrapper)) {
            request = new JwContentCachingRequestWrapper(request);
            request.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
            response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
        }
        filterChain.doFilter(request, response);
        updateResponse(response);
    }

    /**
     * 更新响应(不操作这一步,会导致接口响应空白)
     *
     * @param response
     *            响应对象
     * @throws IOException
     *             /
     */
    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (responseWrapper != null) {
            Objects.requireNonNull(responseWrapper).copyBodyToResponse();
        }
    }
}

拦截器统一打印日志

复制代码
package cn.jw.starter.web.core.interceptors;

import java.nio.charset.StandardCharsets;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.util.StreamUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import cn.hutool.extra.spring.SpringUtil;
import cn.jw.starter.common.utils.JwUidUtil;
import cn.jw.starter.web.core.properties.JwWebProperties;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * 请求日志记录
 *
 * @author wgslucky
 * @since 2024/4/30 1:19
 **/
public class RequestLogInterceptor implements HandlerInterceptor, Ordered {
    private final Logger logger = LoggerFactory.getLogger(RequestLogInterceptor.class);
    private static final String TID = "tid";

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 100;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
        String tid = JwUidUtil.getUidOfSystem();
        MDC.put(TID, tid);
        if (this.logger.isDebugEnabled()) {
            String requestUri = request.getRequestURI();
            JwWebProperties jwWebProperties = SpringUtil.getBean(JwWebProperties.class);
            if (jwWebProperties.getIgnoreCacheBodyUriList().contains(requestUri)) {
                return true;
            }
            String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
            logger.debug("({})-> {}", requestUri, body);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
        throws Exception {
        if (this.logger.isDebugEnabled()) {
            String requestUri = request.getRequestURI();
            JwWebProperties jwWebProperties = SpringUtil.getBean(JwWebProperties.class);
            if (jwWebProperties.getIgnoreCacheBodyUriList().contains(requestUri)) {
                MDC.remove(TID);
                return;
            }
            ContentCachingResponseWrapper responseWrapper =
                WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
            if (responseWrapper != null) {
                String responseBody =
                    IOUtils.toString(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8.toString());
                logger.debug("({}) <-- {}", requestUri, responseBody);
            }
        }
        MDC.remove(TID);
    }
}
相关推荐
jzheng86102 小时前
Spring Boot(快速上手)
java·spring boot·后端
Sylvia-girl2 小时前
IO流~~
java·开发语言
冰暮流星2 小时前
javascript之数组
java·前端·javascript
Re.不晚2 小时前
JAVA进阶之路——无奖问答挑战3
java·开发语言
不倒翁玩偶2 小时前
IDEA导入新的SpringBoot项目没有启动按钮
java·spring boot·intellij-idea
小小小米粒3 小时前
Maven Tools
java
kali-Myon3 小时前
2025春秋杯网络安全联赛冬季赛-day1
java·sql·安全·web安全·ai·php·web
我是咸鱼不闲呀3 小时前
力扣Hot100系列20(Java)——[动态规划]总结(下)( 单词拆分,最大递增子序列,乘积最大子数组 ,分割等和子集,最长有效括号)
java·leetcode·动态规划
清水白石0083 小时前
深入解析 LRU 缓存:从 `@lru_cache` 到手动实现的完整指南
java·python·spring·缓存