做网站的成本在哪,网络优化培训要多少钱,网站单页模板怎么安装,网站技术的解决方案HttpServletRequest下多次获取流数据 背景示例错误的尝试全局替换执行顺序 背景
众所周知request的输入流只能读取一次#xff0c;不能重复读取。而在HttpServletRequest中#xff0c;获取请求体数据的流#xff08;通过getInputStream()方法#xff09;默认只能被读取一… HttpServletRequest下多次获取流数据 背景示例错误的尝试全局替换执行顺序 背景
众所周知request的输入流只能读取一次不能重复读取。而在HttpServletRequest中获取请求体数据的流通过getInputStream()方法默认只能被读取一次。一旦读取后流将处于末尾状态再次尝试读取会返回EOF文件结束符无法重新获取原始数据。
如果在过滤器或者拦截器中有业务需求对输入流进行一些其他操作那么此处读取过后再到controller层就会报错提示IO异常本次的需求就是在拦截器中获取请求体中的数据。
如果多次调用会出现如下错误【如果拦截器中将请求体中的流消费完毕那么到了Controller方法中如果有一个参数需要读取请求体内容例如RequestBody注解的参数那么会出现异常】
java.lang.IllegalStateException: getInputStream() has already been called for this request这里采用实现HttpServletRequestWrapper自定义一个包装器的方式解决输入流不能重复读取的问题并实现修改流的功能。
示例
主要思想将流转换成字节数组作为对象的属性持久化保存起来当需要获取的时候再将字节数组转换回数据流。
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());}Overridepublic ServletInputStream getInputStream() throws IOException {if (inputStream null) {inputStream new BufferedServletInputStream();}return inputStream;}Overridepublic 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);}Overridepublic int read() throws IOException {return buffer.read();}Overridepublic boolean isFinished() {return buffer.available() 0;}Overridepublic boolean isReady() {return true;}Overridepublic 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(ClassT 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注解的参数但实际请求中并没有包含请求体或者请求体为空。
错误的代码
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);//其他略......
}这里可以发现
request new BufferedRequestWrapper(request);这段代码已经将 request 请求替换为了 BufferedRequestWrapper 但是会出现如上报错可知这里仅仅只是替换了此处的请求对象其他的地方使用的还是之前的请求。
因此为了确保 BufferedRequestWrapper 正确工作应该在拦截器链中尽早应用此拦截器以便所有后续的处理都能使用到包装后的请求对象。
全局替换
创建一个 Filter 类使它包装 HttpServletRequest 为我们自己定义的 BufferedRequestWrapper
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 {Overridepublic 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标准而Interceptor是Spring的概念。
Filter: 是在请求进入Servlet之前进行预处理和在响应客户端之前进行后处理的对象。Interceptor : 在DispatcherServletSpring的前端控制器之后执行它可以访问执行链中的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对象从而能够多次读取请求体内容。