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);
    }
}
相关推荐
孟陬19 分钟前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
想用offer打牌21 分钟前
一站式了解四种限流算法
java·后端·go
华仔啊1 小时前
Java 开发千万别给布尔变量加 is 前缀!很容易背锅
java
也些宝2 小时前
Java单例模式:饿汉、懒汉、DCL三种实现及最佳实践
java
Nyarlathotep01132 小时前
SpringBoot Starter的用法以及原理
java·spring boot
wuwen52 小时前
WebFlux + Lettuce Reactive 中 SkyWalking 链路上下文丢失的修复实践
java
SimonKing3 小时前
GitHub 10万星的OpenCode,正在悄悄改变我们的工作流
java·后端·程序员
Seven974 小时前
虚拟线程深度解析:轻量并发编程的未来趋势
java
雨中飘荡的记忆13 小时前
ElasticJob分布式调度从入门到实战
java·后端