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);
    }
}
相关推荐
深蓝轨迹6 分钟前
@Autowired与@Resource:Spring依赖注入注解核心差异剖析
java·python·spring·注解
不想看见4048 分钟前
C++八股文【详细总结】
java·开发语言·c++
huaweichenai26 分钟前
java的数据类型介绍
java·开发语言
weisian15137 分钟前
Java并发编程--17-阻塞队列BlockingQueue:生产者-消费者模式的最佳实践
java·阻塞队列·blockqueue
奔跑的呱呱牛37 分钟前
GeoJSON 在大数据场景下为什么不够用?替代方案分析
java·大数据·servlet·gis·geojson
爱丽_44 分钟前
Pinia 状态管理:模块化、持久化与“权限联动”落地
java·前端·spring
luom01021 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端
毕设源码-朱学姐1 小时前
【开题答辩全过程】以 骨科术后营养餐推荐系统为例,包含答辩的问题和答案
java
丶小鱼丶1 小时前
数据结构和算法之【栈】
java·数据结构
希望永不加班2 小时前
SpringBoot 核心配置文件:application.yml 与 application.properties
java·spring boot·后端·spring