可重复读取的ServletRequest,以及Required request body is missing ...根源
前言
对于开发中我们在过滤器或者拦截器中往往会对请求体做验证,往往会导致发生Required request body is missing的问题
例如
那么,在controller就无法取到这个的值而是直接抛出
有的博客会提出使用ConContentCachingRequestWrapper包装一下ServletRequest来解决,但是实际使用,对于controller参数中使用@RequestBody,仍旧会导致相同的问题。
类似于
而他的缓存(caching)性其实表现在这里,举个例子
这里getContentAsByteArray就是可以重复读取的一个方法,其底层就是ByteArrayOutputStream
既然它可以被重复读取那么为什么又会因为无法重复读取而抛出异常呢?
为什么ConContentCachingRequestWrapper无法解决的重复读取问题
我们先故意触发这个异常,看看异常堆栈信息
再转到对应方法
对于我们这个需要反序列化的参数,含有RequestBody注解,且使用required的默认值true,且不为optional,所以这个判断函数为true,所以抛出这个异常的原因在于arg为null,进而问题出在readWithMessageConverters方法上。
那么我们再去寻找什么情况下这个方法会返回null(其实不是这里返回的null,这里是启发我向上找的原因)
我们再向上寻找body的赋值,同时发现上面也有个一个对messag.hasBody()的判定
那么我们就把断点放到这里(有注释的AbstractMessageConverterMethodArgumentResolver类202行),再此执行
发现其实它比较的是内部的body是否为空,我们再来看这个EmptyBodyCheckingHttpInputMessage类到底在哪里初始化的body这个变量
原来是在构造函数里面,我们再在蓝色高亮处打个断点再重新试试
结合idea提示的类型信息和源码,也就说如果body不为空,那么其中含有的inputstream类就要支持(mark,reset)或者还未读取完毕。
我们再回看ContentCachingRequestWrapper这个类中的ContentCachingInputStream类,首先这个时候因为我们故意在拦截器消费了这个流,所以我们要看看它支不支持(mark,reset)功能
所以说不支持
那么我们再看else分支的这个PushbackInputStream和他的read方法到底何方神圣
因为单参数初始化的后的pos =1 buf数组长度 =1,即返回值为super.read()的返回值
即传入的那个inputStream调用read()方法
结论
你看问题就出在这里。还是调用的ServletInputStream的read,因为直接原请求流被我们消费了,所以返回值为-1
再走到了else中进行处理空body
在这个方法中返回了null(因为第一个参数为null),这就是为什么这个方法返回为null的真正原因
进而我们上面提到的这个if为true的,也就因此抛出了这个我们熟悉的
Required request body is missing异常提示
归根结底是因为ContentCachingRequestWrapper的内部类 ContentCachingInputStream的read方法还是由ServletInputStream去执行read方法的
解决方案
我来提供一个简单的解决方法
我们先来复习一下我们需要什么样的InputStream?支持reset,mark
那么jdk有没有这样一个呢?有!ByteArrayInputStream,这个是个实现InputStream的假装成流的字符数组缓存。
设计思路如下,由这个包装类先行消费输入流做成比特数组储存起来,通过getInputStream提供一个
ServletInputStream的实现类用于代理ByteArrayInputStream进行操作
ByteArrayInputStream只是保留了一个引用,同时这个body的字符数组是只读的,也不用担心线程安全问题,更不用担心ByteArrayInputStream的关闭问题(毕竟不是真正的流)
public class RepeatableRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
private Charset charset;
@SneakyThrows
public RepeatableRequestWrapper(HttpServletRequest request, Charset charset) {
super(request);
body = request.getInputStream().readAllBytes();
this.charset = charset;
}
public RepeatableRequestWrapper(HttpServletRequest request) {
this(request,StandardCharsets.UTF_8);
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new RepeatableInputStream();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(),charset));
}
private class RepeatableInputStream extends ServletInputStream{
private ByteArrayInputStream byteArrayInputStream;
@Override
public synchronized void reset() throws IOException {
byteArrayInputStream.reset();
}
@Override
public synchronized void mark(int readlimit) {
byteArrayInputStream.mark(readlimit);
}
public RepeatableInputStream() {
byteArrayInputStream = new ByteArrayInputStream(body);
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException("不支持监听");
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean markSupported() {
return byteArrayInputStream.markSupported();
}
}
}
我们再来请求一次
舒服了,这次可以了