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);
    }
}
相关推荐
老了,不知天命15 小时前
鳶尾花項目JAVA
java·开发语言·机器学习
二哈赛车手15 小时前
新人笔记---实现简易版的rag的bm25检索(利用ES),以及RAG上传时的ES与向量数据库双写
java·数据库·笔记·spring·elasticsearch·ai
winner888115 小时前
从零吃透C++命名空间、std、#include、string、vector
java·开发语言·c++
AI人工智能+电脑小能手16 小时前
【大白话说Java面试题】【Java基础篇】第26题:Java的抽象类和接口有哪些区别
java·开发语言·面试
bzmK1DTbd16 小时前
SOLID原则在Java中的实践:单一职责与开闭原则
java·开发语言·开闭原则
winner888116 小时前
C++ 命名空间、虚函数、抽象类、protected 权限全套通俗易懂精讲(附与 Java 对比)
java·开发语言·c++
直奔標竿16 小时前
Java开发者AI转型第二十五课!Spring AI 个人知识库实战(四)——RAG来源追溯落地,拒绝AI幻觉
java·开发语言·人工智能·spring boot·后端·spring
qq_5895681016 小时前
java基础学习,案例练习,即时通讯
java·开发语言·学习
逸Y 仙X17 小时前
文章十九: ElasticSearch Full Text 全文本查询
java·大数据·数据库·elasticsearch·搜索引擎·全文检索
AI科技星17 小时前
全域数学·第卷:场计算机卷(场空间计算机)【乖乖数学】
java·开发语言·人工智能·算法·机器学习·数学建模·数据挖掘