思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在前几章中我们对SpringMVC
中注解@ReqeustBody
注解的解析原理进行了深入的剖析,在之前的分析中,并就SpringMVC
中重复使用@ReqeustBody
所导致的现象即原因进行了深入的剖析。
而本文我们则主要介绍如何通过手动Coding
的方式,使SpringMVC
内部支持@RequestBody
的多次读取。
不了解
@RequestBody
解析原理的读者可参考:剖析SpringMVC内部对于@ReqeustBody注解的解析、深入剖析@RequestBody无法被重复解析的原因进行了解~
前言
众所周知 Spring MVC
不支持多个@RequestBody
注解用于同一个方法参数上 。但在剖析SpringMVC内部对于@ReqeustBody注解的解析我们曾留下如下这样一段代码,并放出豪言我们有手段让SpringMVC
支持如下代码的解析!
java
@PostMapping("/duplicate")
public ResponseEntity getBookAndUserInfo(@RequestBody BookInfo bookInfo ,
@RequestBody UserInfo userInfo) {
BookInfoDto bookInfoDto = BookInfoDto.builder()
.bookInfo(bookInfo)
.userInfo(userInfo)
.build();
return new ResponseEntity(bookInfoDto, HttpStatus.OK);
}
你可能会想SpringMVC
内部不支持重复使用@RequestBody
一定有其道理,按着规矩来就可以了,何必写成这样呢?并且上述代码完全可以将BookInfo
和UserInfo
封装为同一个实体,然后在进行转换即可。这样做事没错,但这次笔者
想做点不一样的,希望笔者
的思路能给你带来启发!
@RequestBody
无法重复解析的原因
在之前的分析中,我们只是简要的分析了@ReqeustBody
无法重复解析的原理。这次我们通过手动Debug
的方式来一行一行的分析。理论结合实践往往的更加透彻的了理解。
本次请求示例代码如下所示:
java
@PostMapping("/duplicate")
public ResponseEntity getBookAndUserInfo(@RequestBody BookInfo bookInfo ,
@RequestBody UserInfo userInfo) {
BookInfoDto bookInfoDto = BookInfoDto.builder()
.bookInfo(bookInfo)
.userInfo(userInfo)
.build();
return new ResponseEntity(bookInfoDto, HttpStatus.OK);
}
不难发现,在方法getBookAndUserInfo
中的入参中通过两个@RequestBody
来进行修饰。进一步,对于InvocableHandlerMethod # getMethodArgumentValues
而言,其在解析入参信息时,其中的parameters
数组便记录了当前被请求方法入参信息,具体如下所示:
由于在getBookAndUserInfo
中方法入参包含两个参数,因此parameters
的容量为2
。因此AbstractMessageConverterMethodArgumentResolver
中的readWithMessageConverters
中进行参数解析的逻辑也就会被调用两次。
注:此处不熟悉相关调用逻辑可参参考:深入剖析@RequestBody无法被重复解析的原因
更进一步,两次调用readWithMessageConverters
方法时,其内部在构建EmptyBodyCheckingHttpInputMessage
时的逻辑如下
解析第一个参数BookInfo参数时,构建的EmptyBodyCheckingHttpInputMessage对象时的情况如下
解析第二个参数UserInfo参数时,构建的EmptyBodyCheckingHttpInputMessage对象时的情况如下
对比上图不难发现,当在第二次调用readWithMessageConverters
进行构建EmptyBodyCheckingHttpInputMessage
对象时,我们注意到其会将一个body
成员变量置为null
。
其置为null
的原因,我们曾在剖析SpringMVC内部对于@ReqeustBody注解的解析中提及过,核心原因无非就是:当前I/O流
已经关系,所以无法从body
请求体中读取内容!
进一步,当EmptyBodyCheckingHttpInputMessage
中成员变量body
被置为null
后,其所诱发的连锁反应就是导致readWithMessageConverters
中message.hasBody()
的执行结果返回false
,进而导致参数无法被解析。
具体到当前例子来看,这就导致第二个被@RequestBody
修饰的UserInfo
参数无法解析,进而导致本该被解析参数返回null
,从而导致出现请求出现400
。
重复读取@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
的构造方法。
java
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
InputStream inputStream = inputMessage.getBody();
if (inputStream.markSupported()) {
inputStream.mark(1);
this.body = (inputStream.read() != -1 ? inputStream : null);
inputStream.reset();
}
else {
PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
int b = 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
)。
相关改造代码如下所示:
- 缓存请求体内容,构造信息
HttpServletRequest
java
public class CacheRequestBodyContent extends HttpServletRequestWrapper {
private byte[] body = null;
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public CacheRequestBodyContent(HttpServletRequest request, ServletResponse response) {
super(request);
try {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
body = IoUtil.readBytes(request.getInputStream(), false);
} catch (Exception e) {
log.error("请求数据读取失败,请重试");
}
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream cache = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return cache.read();
}
@Override
public int available() throws IOException {
return body.length;
}
// .. 省略其他无关方法
};
}
}
此处之所以还要重写其中的getReader
与 getInputStream
方法主要为了保证 pushbackInputStream.read()
读取内容时,可以读取到我们所缓存的请求体内容。
- 定义过滤器
java
public class CacheRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new CacheRequestBodyContent((HttpServletRequest) request, response);
}
if (null == requestWrapper) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
}
其中,通过 requestWrapper = new CacheRequestBodyContent((HttpServletRequest) request, response);
这行代码对默认的Request
对象进行替换,以换成我我们自定义的Reqeust
对象。
- 配置过滤器
java
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CacheRequestFilter());
registration.addUrlPatterns("/*");
registration.setName("requestFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
}
}
配置完毕启动程序后,我们调用http://localhost:8080/users/duplicate
内容,我们会可以看到起会得到如下结果:
显然,通过我们这样的改造使得SpringMVC
内部支持了对@ReqeuestBody
注解的解析。SpringMVC
内部无法重复解析@ReqeustBody
的问题被我们完美解决!
总结
至此我们也就利用三篇文章完成的对SpringMVC
中@ReqeustBody
的解析原理进行剖析,并利用相关的源码知识对SpringMVC
中无法重复解析@ReqeustBody
注解的问题进行解决。
如果觉得文章对你有所帮助不妨点赞+收藏+关注作者,我们下次再见!