之前的代码就是相当于一个接口的转发,大概是这样。如下图,initForwardRequest方法里面会判断如果是post请求的话使用request.getInputStream()获取结果。

现在需要对某些特定的请求拦截做前置校验及后置处理,因为懒,想着spring mvc精确匹配大于模糊匹配然后直接在这个controller里改了,新增了两个具体的接口,一个接口需要入参做一些操作,所以加了@RequestBody图省事,另一个不需要入参,只做一些校验。代码结构大概如下。

 

 然后以为到这就结束了。

执行,没想到突然打脸。。。调用path/start时报了inputStream已经closed。

然后才了解到ServletRequest.getInputStream是一次性的,读过了就不能再读了,而造成这个问题的元凶就是@RequestBody,在spring mvc的框架中,RequestResponseBodyMethodProcessor中会解析含有注解@RequestBody的参数,会将ServletRequest.getInputStream读取出来通过HttpMessageConverter(默认是Jackson的实现,ObjectMapper.readValue)转化成@RequestBody标注的实体,这时候这个ServletInputStream就已经关闭了。还有@RequestParam的post请求也会这样。

因为接口中@RequestBody注解是用了一次getInputStream,接口内部也通过getInputStream()去读取body数据,copy到新的Request去,所以报错了。如果想在controller里重复使用getInputStream话需要自己写一个包装类,read时缓存下来从InputStream中读取到的内容,覆盖getInpustStream,这个从缓存里读数据,而且这个包装类要在HandlerMethod执行之前,一般都在tomcat的filter链中替换成包装类,这里也借鉴了一些网上的写法(springboot通过HttpServletRequestWrapper获取所有请求参数_爱上口袋的天空的博客-CSDN博客_requestwrapper)。这里大概贴一下代码。

之后就可以顺利执行了。

但是计划赶不上变化。需求上线那天,上线时还有个公共包升级跟着一起上线了。

开发测试了快10天的需求之前一点问题没有,在这个时候突然请求没有请求参数了,二方那边接受不到请求参数了。

因为服务本地启不起来没法断点,捣鼓了半天,怀疑是公共包可能有哪些改动影响了这个东西,然后就去找到了升级公共包的那些同事,发现还真有改动影响了现在的逻辑。。。

公共包里新增加了一个filter,是将HttpServletRequest包装成ContentCachingRequestWrapper,看了下这个源码发现确实有问题。

ContentCachingRequestWrapper覆盖了几个主要的接口。

1.getInputStream

调用这个接口的时候就相当于调用包装的HttpServletRequest的getInputStream接口

2.getContentAsByteArray将缓存下来的内容返回

3.内部ContentCachingInpustStream覆盖read接口

read时都会将内入缓存下来

其实这个类就是为了调用read读取的时候将内容缓存下来,需要的时候可以通过getContentAsByteArray读取出来。

这里分析一下问题原因。

因为新的公共包中添加ContentCachingRequestWrapper的filter是在自己添加RequestWrapper的filter之后,就导致先将HttpServletRequest封装成了RequestWrapper,然后RequestWrapper又被封装成了ContentCachingRequestWrapper,如下图

最终@RequestBody参数解析的时候调用getInputStream是ContentCachingRequestWrapper的。而ContentCachingRequestWrapper内部会缓存一个ContentCachingInputSream,这个类会使用传入的InputStream作为数据源读取数据,并且读取数据的时候会将数据

缓存到 cachedContent中(ByteArrayOutputStream)

SpringMVC在解析在将流解析成实体类时,第一次调用ContentCachingRequestWrapper.getInputStream(),调用流程ContentCachingRequestWrapper.getInputStream()读取数据=》RequestWrapper.getInputStream()读取数据=>RequestWrapper内部ServletInputStream读取数据=》RequestWrapper内部生成的ByteArrayInputStream。

调用getInputStream后的结构

这里ServletInputStream 在第一次调用InputStream就生成了。里面的ByteArrayInputStream也已经被读取过了,里面的pos指针已经指向数组末尾了,再次读取就读不到数据了,所以在转发请求Copy请求体的时候getInputStream就读不到数据,导致二方接口没有接受到参数。

 这里问题找到了,有两种修改方案:

1.一种方案是调整一下两个filter的顺序,让封装RequestWrapper的filter在封装ContentCachingRequestWrapper的filter之后执行,但是保不齐之后框架又加些什么东西导致又失效了。

2.所以采用另一种方法,自己的RequestWrapper的filter不用了,在forward转发接口中initForwardRequest中获取InputStream的地方加个判断,因为ContentCachingRequestWrapper如果getInputStream过,里面本来就缓存了数据,只不过是必须通过getContentAsByteArray接口获取,可以这样写,判断InputStream是否已经finished,如果已经finished相当于getInputStream过了,就从缓存的内容读取,如果没有finished就直接getInputStream。(ContentCachingRequestWrapper如果不先调用getInputStream,getContentAsByteArray是没有数据的)

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐