实现HttpServletRequest下多次获取流数据

HttpServletRequest下多次获取流数据

背景

​众所周知request的输入流只能读取一次,不能重复读取。而在HttpServletRequest中,获取请求体数据的流(通过getInputStream()方法)默认只能被读取一次。一旦读取后,流将处于末尾状态,再次尝试读取会返回EOF(文件结束符),无法重新获取原始数据。

如果在过滤器或者拦截器中有业务需求对输入流进行一些其他操作,那么此处读取过后再到controller层就会报错,提示IO异常,本次的需求就是在拦截器中获取请求体中的数据。

如果多次调用会出现如下错误【如果拦截器中将请求体中的流消费完毕,那么到了Controller方法中如果有一个参数需要读取请求体内容(例如@RequestBody注解的参数)那么会出现异常)】

java 复制代码
java.lang.IllegalStateException: getInputStream() has already been called for this request

这里采用实现HttpServletRequestWrapper自定义一个包装器的方式解决输入流不能重复读取的问题,并实现修改流的功能。

示例

主要思想:将流转换成字节数组作为对象的属性持久化保存起来,当需要获取的时候再将字节数组转换回数据流。

java 复制代码
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.util.WebUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class BufferedRequestWrapper extends HttpServletRequestWrapper {

    private byte[] requestBodyBytes;
    //在类的序列化过程中忽略这些字段
    private transient ServletInputStream inputStream;
    private transient BufferedReader reader;

    public BufferedRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 一次性将请求体内容读取并缓存到requestBodyBytes中
        requestBodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (inputStream == null) {
            inputStream = new BufferedServletInputStream();
        }
        return inputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (reader == null) {
            reader = new BufferedReader(new InputStreamReader(getInputStream()));
        }
        return reader;
    }

    // 自定义ServletInputStream以实现多次读取
    private class BufferedServletInputStream extends ServletInputStream {

        private ByteArrayInputStream buffer;

        public BufferedServletInputStream() {
            buffer = new ByteArrayInputStream(requestBodyBytes);
        }

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

        @Override
        public boolean isFinished() {
            return buffer.available() == 0;
        }

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

        @Override
        public void setReadListener(ReadListener listener) {
            throw new UnsupportedOperationException("Not supported");
        }
    }

    // 如果需要以String形式获取请求体内容
    public String getRequestBody() throws IOException {
        return new String(requestBodyBytes, getCharacterEncoding());
    }

    // 可选:将请求体反序列化为JSON对象
    public <T> T getRequestBodyAs(Class<T> clazz) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(requestBodyBytes, clazz);
    }
}

然后,在我们需要的地方使用这个BufferedRequestWrapper。但是,需要注意的是这个新的 request 对象是我们消耗掉原来 request 中的流数据创建的,也就是说,原来的流已经被关闭了无法再次使用。

既然如此,我们就需要让新建的请求对象与之前的进行替换,达到可以多次获取数据流的效果。

注意:

Servlet 3.1 开始,ServletInputStream有新的方法isFinished()isReady()setReadListener(ReadListener readListener),在自定义CachedServletInputStream时可能需要实现这些方法。因为这些方法用于支持非阻塞IO操作,如果你不使用非阻塞读取,可以简单地实现这些方法并返回默认值(例如,isFinished()返回true,而isReady()返回true)。

错误的尝试

报错:HttpMessageNotReadableException: Required request body is missing

错误解释Controller方法中有一个参数需要读取请求体内容(例如@RequestBody注解的参数),但实际请求中并没有包含请求体或者请求体为空。

错误的代码

java 复制代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) th
    // 正确使用 RepeatableRequestWrapper 包装请求
    if (!(request instanceof BufferedRequestWrapper)) {
        request = new BufferedRequestWrapper(request);
    }
    //判断当前拦截到的是Controller的方法还是其他资源
    if (!(handler instanceof HandlerMethod)) {
        //当前拦截到的不是动态方法(控制器中的方法),直接放行
        return true;
    }
    //获取访问的方法
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    //如果没有被日志注解注解,则放行
    if (!method.isAnnotationPresent(Logger.class)) {
        return true;
    }
   	//其他无关校验逻辑和其他信息(略)
   	.....
   	
    String requestBody = ((BufferedRequestWrapper) request).getRequestBody();
    //3.记录方法的参数 
    request.setAttribute("rqParam", requestBody);
    return true;
}

@Override
public void postHandle(HttpServletRequest request,
 			HttpServletResponse response, Object handler, ModelAndView modelAndView
 			) throws Exception {
 	//这里的需求是获取请求参数之后和其他信息一起插入到数据库中,记录下操作
 	//2.获取请求参数
    String rqParam = (String) request.getAttribute("rqParam");
    //其他略
    ......
}

这里可以发现

java 复制代码
request = new BufferedRequestWrapper(request);

这段代码已经将 request 请求替换为了 BufferedRequestWrapper ,但是会出现如上报错,可知这里仅仅只是替换了此处的请求对象,其他的地方使用的还是之前的请求。

因此,为了确保 BufferedRequestWrapper 正确工作,应该在拦截器链中尽早应用此拦截器,以便所有后续的处理都能使用到包装后的请求对象。

全局替换

创建一个 Filter 类,使它包装 HttpServletRequest 为我们自己定义的 BufferedRequestWrapper

java 复制代码
import com.shen.stock.config.BufferedRequestWrapper;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
//设置高优先级
@Order(1)
public class CachedBodyFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        BufferedRequestWrapper cachedBodyHttpServletRequest = new BufferedRequestWrapper(httpServletRequest);
        filterChain.doFilter(cachedBodyHttpServletRequest, servletResponse);
    }

    // Add init() and destroy() methods if needed
}

在这里,使用了 @Component@Order 注解来标记这是一个 Spring 组件,以及定义了它在所有过滤器中的执行顺序,使其优先级高于其他 Filter ,这样就能确保其他的Filter使用的是包装后的请求对象。

确保 CachedBodyFilter 被Spring Boot自动检测并添加到过滤器链中。由于我们使用了 @Component 注解,Spring Boot会自动发现这个过滤器并将其注册为一个Spring Bean。如果你的Spring Boot应用中有自定义的Filter注册逻辑,则需要在那里添加对 CachedBodyFilter 的支持。

现在,任何在过滤器链之后执行的代码(如控制器方法)都将能够多次读取 HttpServletRequest 中的流,因为它将被 CachedBodyHttpServletRequest 包装,它缓存了请求体的内容。

要注意的一点是,如果请求体数据很大或者请求频率很高,这种缓存方法可能会产生性能问题或大量内存占用。确保你的应用场景可以接受这种实现方式。

执行顺序

另外,补充一下过滤器和拦截器的执行顺序问题。

如果你按照上述步骤正确创建并注册了 CachedBodyFilter 类,并将其优先级设置得高于你的自定义拦截器,那么在 Spring Boot 的过滤器链中,自定义的拦截器将会接收到 BufferedRequestWrapper 对象作为请求对象。

Spring Boot 中过滤器(Filter)和拦截器(Interceptor)有不同的执行顺序。Filter是基于Servlet标准,而InterceptorSpring的概念

  • Filter: 是在请求进入Servlet之前进行预处理和在响应客户端之前进行后处理的对象。
  • Interceptor : 在DispatcherServlet(Spring的前端控制器)之后执行,它可以访问执行链中的Controller,并且可以在Controller方法执行之前、之后以及完成渲染视图返回给客户端之后执行操作。

由于Filter在Servlet容器级别工作,它在Interceptor之前执行,所以任何请求都会首先经过Filter然后才到达Interceptor。因此,如果在Filter中将普通的 HttpServletRequest 包装成 BufferedRequestWrapper,那么随后在Spring的处理流程中------包括Interceptor和Controller中------接收到的都将是已经包装的 BufferedRequestWrapper

为了确保CachedBodyFilter的执行顺序正确,请在@Order注解或者Filter的注册中明确指定足够低的顺序值(或优先级高)。在Spring中,@Order注解中值越低,优先级越高。

示例中的@Order(1)表明CachedBodyFilter会在大多数其他Filter之前执行,但你可能需要根据你的应用配置进行必要的调整。如果你使用WebSecurityConfigurerAdapter进行额外的过滤器配置,确保CachedBodyFilter优先于Spring Security的过滤器链执行。

请记住,如果你使用了第三方库或已有的Filter实现,也需要确保它们的执行顺序是正确的。任何在CachedBodyFilter之后执行并打算处理请求体的组件都会收到BufferedRequestWrapper对象,从而能够多次读取请求体内容。

相关推荐
程序员大金6 分钟前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
qq_25183645713 分钟前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端
武昌库里写JAVA34 分钟前
Vue3常用API总结
数据结构·spring boot·算法·bootstrap·课程设计
qq_2518364571 小时前
基于springboot vue3 在线考试系统设计与实现 源码数据库 文档
数据库·spring boot·后端
2401_858120531 小时前
古典舞在线交流平台:SpringBoot设计与实现详解
java·spring boot·后端
潘多编程3 小时前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_857589363 小时前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
原机小子3 小时前
Spring Boot框架下的新闻推荐技术
服务器·spring boot·php
2401_857622663 小时前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
2402_857589365 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端