改造你的Web应用,让其支持@RequestBody内容的重复读取
前言 众所周知 Spring MVC不支持多个@RequestBody注解用于同一个方法参数上。但在剖析SpringMVC内部对于@ReqeustBody注解的解析我们曾留下如下这样一段代码,并放出豪言我们有手段让SpringMVC支持如下代码的解析!
@PostMapping("/duplicate")
publicResponseEntitygetBookAndUserInfo(@RequestBodyBookInfobookInfo,
@RequestBodyUserInfouserInfo){
BookInfoDtobookInfoDto=BookInfoDto.builder()
.bookInfo(bookInfo)
.userInfo(userInfo)
.build();
returnnewResponseEntity(bookInfoDto,HttpStatus.OK);
}
你可能会想SpringMVC内部不支持重复使用@RequestBody一定有其道理,按着规矩来就可以了,何必写成这样呢?并且上述代码完全可以将BookInfo和UserInfo封装为同一个实体,然后在进行转换即可。这样做事没错,但这次笔者想做点不一样的,希望笔者的思路能给你带来启发!
@RequestBody无法重复解析的原因 在之前的分析中,我们只是简要的分析了@ReqeustBody无法重复解析的原理。这次我们通过手动Debug的方式来一行一行的分析。理论结合实践往往的更加透彻的了理解。
本次请求示例代码如下所示:
@PostMapping("/duplicate")
publicResponseEntitygetBookAndUserInfo(@RequestBodyBookInfobookInfo,
@RequestBodyUserInfouserInfo){
BookInfoDtobookInfoDto=BookInfoDto.builder()
.bookInfo(bookInfo)
.userInfo(userInfo)
.build();
returnnewResponseEntity(bookInfoDto,HttpStatus.OK);
}
不难发现,在方法getBookAndUserInfo中的入参中通过两个@RequestBody来进行修饰。进一步,对于InvocableHandlerMethod # getMethodArgumentValues而言,其在解析入参信息时,其中的parameters数组便记录了当前被请求方法入参信息,具体如下所示:
image.png由于在getBookAndUserInfo中方法入参包含两个参数,因此parameters的容量为2。因此AbstractMessageConverterMethodArgumentResolver中的readWithMessageConverters中进行参数解析的逻辑也就会被调用两次。
注:此处不熟悉相关调用逻辑可参参考:深入剖析@RequestBody无法被重复解析的原因
更进一步,两次调用readWithMessageConverters方法时,其内部在构建EmptyBodyCheckingHttpInputMessage时的逻辑如下
解析第一个参数BookInfo参数时,构建的EmptyBodyCheckingHttpInputMessage对象时的情况如下image.png解析第二个参数UserInfo参数时,构建的EmptyBodyCheckingHttpInputMessage对象时的情况如下image.png对比上图不难发现,当在第二次调用readWithMessageConverters进行构建EmptyBodyCheckingHttpInputMessage对象时,我们注意到其会将一个body成员变量置为null。
其置为null的原因,我们曾在剖析SpringMVC内部对于@ReqeustBody注解的解析中提及过,核心原因无非就是:当前I/O流已经关闭,所以无法从body请求体中读取内容!
进一步,当EmptyBodyCheckingHttpInputMessage中成员变量body被置为null后,其所诱发的连锁反应就是导致readWithMessageConverters中message.hasBody()的执行结果返回false,进而导致参数无法被解析。
具体到当前例子来看,这就导致第二个被@RequestBody修饰的UserInfo参数无法解析,进而导致本该被解析参数返回null,从而导致出现请求出现400。
image.png重复读取@RequestBody内容 那有没有一种办法让我们重复读取@ReqeustBody内容呢?答案是肯定。在提供解决方案之前,我们不妨先来看看导致@RequestBody无法重复读取原因是什么。
通过之前分析不难发现,如果一个方法入参中同时包含两个@ReqeustBody所修饰的Java对象,那么第二个被@ReqeustBody所修饰的对象在进行解析时,其在读取请求体中相关内容时存在无法解析的问题。
那导致该问题的原因是什么呢?具体来看,在EmptyBodyCheckingHttpInputMessage构造器中执行pushbackInputStream.read()时会返回一个-1。而这个-1则表示无法从当前请求体中读取相关内容。那为什么会导致这样的问题呢?
答案也很简单,就是因为SpringMVC中对于请求体的内容是通过I/O流进行处理的,当处理完毕后会将相应的I/O流进行关闭。 而@ReqeustBody内容主要封装于请求体中,如果一个请求方法中使用多个@ReqeustBody注解进行修饰,那么SpringMVC在解析时会遍历请求方法所有的入参信息,并且会重复获取请求体中的内容,以完成相应Java对象的封装。
但是请求体的中I/O在第一次解析处理后会关闭,这就导致后续再处理时,无法从请求体中获取相应内容,进而也就导致后续被@ReqeustBody修饰的对象无法完成封装。
明白了问题所在后,不知道你能否想到了相对应解决策略?此处只提供一种缓存的思路,如果你有其他好的思路也可在评论区留言~~~
所谓缓存的思路其实也很简单,既然你对于请求体中的内容只读取一次,那么如果我们把请求体中内容进行缓存,这样你下次再调用pushbackInputStream.read()时是不是就可以读取到了,进而也就不会使得EmptyBodyCheckingHttpInputMessage中body置为null。
顺着这个思路,我们再来看EmptyBodyCheckingHttpInputMessage的构造方法。
publicEmptyBodyCheckingHttpInputMessage(HttpInputMessageinputMessage)throwsIOException{
this.headers=inputMessage.getHeaders();
InputStreaminputStream=inputMessage.getBody();
if(inputStream.markSupported()){
inputStream.mark(1);
this.body=(inputStream.read()!=-1?inputStream:null);
inputStream.reset();
}
else{
PushbackInputStreampushbackInputStream=newPushbackInputStream(inputStream);
intb=pushbackInputStream.read();
if(b==-1){
this.body=null;
}
else{
this.body=pushbackInputStream;
pushbackInputStream.unread(b);
}
}
}
我们注意到pushbackInputStream的读取依赖于inputMessage.getBody()。而inputMessage.getBody()则主要会获取当前请求Reqeust对象,并读取其中的I/O流信息。
进一步,既然会获取当前请求Reqeust对象,那么是不是就可以通过对Reqeust进行处理呢?只要我们读取请求中的Reqeust中请求体内容,并进行缓存我们对于请求体缓存的目标也就实现了。
更进一步,为了SpringMVC中获取的请求可以知道知晓我们所缓存的内容,我们还需要对原先Reqeuest进行替换,那什么组件能支持我们完成这样操作呢?最简单的手段无非是使用过滤器(Filter)。
相关改造代码如下所示:
缓存请求体内容,构造信息HttpServletRequestpublicclassCacheRequestBodyContentextendsHttpServletRequestWrapper{
privatebyte[]body=null;
/**
*Constructsarequestobjectwrappingthegivenrequest.
*
*@paramrequestTherequesttowrap
*@throwsIllegalArgumentExceptioniftherequestisnull
*/
publicCacheRequestBodyContent(HttpServletRequestrequest,ServletResponseresponse){
super(request);
try{
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
body=IoUtil.readBytes(request.getInputStream(),false);
}catch(Exceptione){
log.error("请求数据读取失败,请重试");
}
}
@Override
publicBufferedReadergetReader()throwsIOException{
returnnewBufferedReader(newInputStreamReader(getInputStream()));
}
@Override
publicServletInputStreamgetInputStream()throwsIOException{
finalByteArrayInputStreamcache=newByteArrayInputStream(body);
returnnewServletInputStream(){
@Override
publicintread()throwsIOException{
returncache.read();
}
@Override
publicintavailable()throwsIOException{
returnbody.length;
}
//..省略其他无关方法
}
}
此处之所以还要重写其中的getReader与getInputStream方法主要为了保证pushbackInputStream.read()读取内容时,可以读取到我们所缓存的请求体内容。
定义过滤器publicclassCacheRequestFilterimplementsFilter{
@Override
publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{
ServletRequestrequestWrapper=null;
if(requestinstanceofHttpServletRequest){
requestWrapper=newCacheRequestBodyContent((HttpServletRequest)request,response);
}
if(null==requestWrapper){
chain.doFilter(request,response);
}else{
chain.doFilter(requestWrapper,response);
}
}
}
其中,通过requestWrapper = new CacheRequestBodyContent((HttpServletRequest) request, response);这行代码对默认的Request对象进行替换,以换成我我们自定义的Reqeust对象。
配置过滤器@Configuration
publicclassFilterConfig{
@Bean
publicFilterRegistrationBeansomeFilterRegistration(){
FilterRegistrationBeanregistration=newFilterRegistrationBean();
registration.setFilter(newCacheRequestFilter());
registration.addUrlPatterns("/*");
registration.setName("requestFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
returnregistration;
}
}
配置完毕启动程序后,我们调用http://localhost:8080/users/duplicate内容,我们会可以看到起会得到如下结果:
image.png显然,通过我们这样的改造使得SpringMVC内部支持了对@ReqeuestBody注解的解析。SpringMVC内部无法重复解析@ReqeustBody的问题被我们完美解决!
总结 至此我们也就利用三篇文章完成的对SpringMVC中@ReqeustBody的解析原理进行剖析,并利用相关的源码知识对SpringMVC中无法重复解析@ReqeustBody注解的问题进行解决。
如果觉得文章对你有所帮助不妨点赞+收藏+关注作者,我们下次再见!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线