拦截器操作数据流异常问题分析

【背景】

当时的需要是在针对特定的请求做签名认证,而这个我选择了使用拦截器来处理,刚开始我是将签名的信息都放在了Body里面,因此拦截器我需要通过HttpServletRequest来获取,而Body数据是存储在流中的,当时就通过request.getReader()读取流信息,除此之外,我在controller用到了@RequestBody来接收JSON体,结果程序运行就发生了下面的问题:

错误一:getInputStream() has already been called for this request
错误二:nested exception is java.lang.IllegalStateException: getWriter() has already been called for this respons
备注:我是在程序中遇到错误一,但是上面俩个问题原理是一样,这里总结在一起

【错误代码案例】

问题一:request
在这里插入图片描述
问题二:HttpServletResponse
在这里插入图片描述

【分析】

一个流不能读两次异常,这种异常一般出现在框架或者拦截器中读取了request中的流的数据,我们在业务代码中再次读取(如@requestBody),由于流中的数据已经没了,所以第二次读取的时候就会抛出异常。

【解决方案】

解决方案一:这个方案也是我在网上查询到最普遍的方案,
先将 Request Body 保存,然后通过 Servlet 自带的 HttpServletRequestWrapper 类覆盖 getReader() 和
getInputStream() 方法,使流从保存的body读取。然后再Filter中将ServletRequest替换为AuthenticationRequestWrapper。

public class MyRequestWrapper extends HttpServletRequestWrapper {
    
    
    private byte[] body;
 
    public MyRequestWrapper(HttpServletRequest request) throws IOException {
    
    
        super(request);
 
        StringBuilder sb = new StringBuilder();
        String line;
        BufferedReader reader = request.getReader();
        while ((line = reader.readLine()) != null) {
    
    
            sb.append(line);
        }
        String body = sb.toString();
        this.body = body.getBytes(StandardCharsets.UTF_8);
    }
 
 
    public String getBody() {
    
    
        return new String(body, StandardCharsets.UTF_8);
    }
}
//使用
MyRequestWrapper myRequestWrapper = new MyRequestWrapper(request);
      myRequestWrapper.getBody();

解决方案二:

 public static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal();

使用ThreadLocal,利用其线程私有的特性,针对每个请求的处理线程分配一个ThreadLocal,然后在拦截器使用流的时候将流数据存储到ThreadLocal容器中,后面直接从这个读取即可。
比如在拦截器读取Body内容,然后将其转换为Map,然后存储到静ThreadLocal后面的controller拿到这个容器数据即可。

解决方案三:
上面的俩种方式都算是侵入式的,其实还有很多类似的解决方案,不同的你选择什么样的容器进行存储,除了上述方式外,你还可以将数据存储到request.setAttribute中等等。
但是如果你这样写了,那么对于其他人或者自己以后再添加新代码的话都需要再加一些特别的流程,比如通过MyRequestWrapper 拿到body,或者通过ThreadLocal.get()获取数据。一方面需要额外的内存存储,一方面增加了代码。但是如果不可避免的话,那么只能选择这种方式。在我的实践中,由于我只需要对请求进行签名认证,为了避免上述的麻烦,我将签名信息和业务数据分开了,签名信息放在header里面,业务数据还是放在body,那么在拦截器阶段我只需要request.getHeader即可。就不需要再去操作流数据了。当然了,这种方式就需要调用方修改请求携带参数,具体的践行方案还是需要依据实际需求和自己的思考。

猜你喜欢

转载自blog.csdn.net/Octopus21/article/details/110732774