HttpRequest中InputStream只能一次性读取问题采坑

场景:前端在request body中传了多个参数,为了方便使用@RequestBody映射成相应的参数对象。

@PostMapping(value = "/game/sync")
public WebMessage gameMsgReport(UserInfo userInfo,@RequestBody GameMsg msgJson){
     gameService.gameMsgReport(userInfo.getUserId(), msgJson);
     return WebMessage.DEFAULT;
}   

问题:部署到测试后对象参数始终是空的,传的参数无法映射。

原因分析:@RequestBody 原理上就是spring在进行请求转发时,通过InputStream读取Request body中的数据,然后分装成指定的对象类型参数。所以怀疑问题出在数据读取上,经过资料查询发现HttpServletRequest的数据流只能被读取一次,读取后就不能再重置。所以排查项目拦截器中使用到Request的InputStream的地方,果然发现:在日志输出时使用InputStream读取了Request中的数据

accessLogger.debug("user access,requestURI = {},headers = {},params = {}",request.getRequestURI(),ServletUtils.getHeaderInfo(request),ServletUtils.getParameters(request));
 1 public static String getParameters(HttpServletRequest request){
 2         String params = request.getQueryString();
 3         if (StringUtils.isNotBlank(params)) {
 4             return params;
 5         }
 6         try {
 7             StringBuilder stringBuilder = new StringBuilder();
 8             BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
 9             String inputStr;
10             while ((inputStr = streamReader.readLine()) != null) {
11                 stringBuilder.append(inputStr);
12             }
13             return stringBuilder.toString();
14         } catch (Exception e) {
15             return null;
16         }
17     }

解决方法:将Request中的数据读取后缓存下来。

1、继承HttpServletRequestWrapper,将Request请求中的数据读取完缓存到封装类中。

 1 @Slf4j
 2 public class RepeatedlyReadRequestWrapper extends HttpServletRequestWrapper {
 3 
 4     private final byte[] body;
 5 
 6     private static final int BUFFER_START_POSITION = 0;
 7 
 8     private static final int CHAR_BUFFER_LENGTH = 1024;
 9 
10     public RepeatedlyReadRequestWrapper(HttpServletRequest request) throws IOException
11     {
12         super(request);
13         StringBuilder stringBuilder = new StringBuilder();
14 
15         InputStream inputStream = null;
16         try {
17             inputStream = request.getInputStream();
18         } catch (IOException e) {
19             log.error("Error reading the request body…", e);
20         }
21         if (inputStream != null) {
22             try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
23                 char[] charBuffer = new char[CHAR_BUFFER_LENGTH];
24                 int bytesRead;
25                 while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
26                     stringBuilder.append(charBuffer, BUFFER_START_POSITION, bytesRead);
27                 }
28             } catch (IOException e) {
29                 log.error("Fail to read input stream",e);
30             }
31         } else {
32             stringBuilder.append("");
33         }
34         body = stringBuilder.toString().getBytes();
35     }
36 
37     @Override
38     public BufferedReader getReader() throws IOException
39     {
40         return new BufferedReader(new InputStreamReader(getInputStream()));
41     }
42 
43     @Override
44     public ServletInputStream getInputStream() throws IOException
45     {
46         final ByteArrayInputStream bais = new ByteArrayInputStream(body);
47 
48         return new ServletInputStream(){
49 
50             @Override
51             public boolean isFinished()
52             {
53                 return false;
54             }
55 
56             @Override
57             public boolean isReady()
58             {
59                 return false;
60             }
61 
62             @Override
63             public void setReadListener(ReadListener listener)
64             {
65 
66             }
67 
68             @Override
69             public int read() throws IOException
70             {
71                 return bais.read();
72             }
73 
74         };
75 
76     }
77 
78     @Override
79     public String getHeader(String name)
80     {
81         return super.getHeader(name);
82     }
83 
84     @Override
85     public Enumeration<String> getHeaderNames()
86     {
87         return super.getHeaderNames();
88     }
89 
90     @Override
91     public Enumeration<String> getHeaders(String name)
92     {
93         return super.getHeaders(name);
94     }
95 }

2、将包装后的HttpServletRequest传递到后面的请求链路中。

 1 @Component
 2 @WebFilter
 3 public class RepeatlyReadFilter implements Filter {
 4 
 5     @Override
 6     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
 7         if (request instanceof HttpServletRequest) {
 8             request = new RepeatedlyReadRequestWrapper((HttpServletRequest) request);
 9         }
10         chain.doFilter(request, response);
11     }
12 }

还有一个知识点:因为第一次读取Request中的数据是在拦截器Interceptor中,而缓存Request中的数据是在过滤器Filter中做的,所以就涉及到两者的执行顺讯问题。Filter依赖于Servlet容器,基于函数回调;Interceptor依赖于web框架(如SpringMVC),基于java反射,两者并没有什么关系,而Filter的执行在Interceptor之前,盗用网上一张执行流程图,如下:

                                                         

 

 

 

 

posted @ 2021-02-03 15:49  jingyi_up  阅读(489)  评论(0编辑  收藏  举报