这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战
前提背景
项目中需要记录用户的请求参数便于后面查找问题,对于这种需求一般可以通过Spring中的拦截器或者是使Servlet中的过滤器来实现。这里我选择使用过滤器来实现,就是添加一个过滤器,然后在过滤器中获取到Request对象,将Reques中的信息记录到日志中。
问题介绍
在调用request.getReader之后重置HttpRequest:
有时候我们的请求是post,但我们又要对参数签名,这个时候我们需要获取到body的信息,但是当我们使用HttpServletRequest的getReader()和getInputStream()获取参数后,后面不管是框架还是自己想再次获取body已经没办法获取。当然也有一些其他的场景,可能需要多次获取的情况。
可能抛出类似以下的异常
1 | kotlin复制代码java.lang.IllegalStateException: getReader() has already been called for this request |
因此,针对这问题,给出一下解决方案:
定义过滤器解决
使用过滤器很快我实现了统一记录请求参数的的功能,整个代码实现如下:
1 | java复制代码@Slf4j |
上面的实现方式对于GET请求没有问题,可以很好的记录前端提交过来的参数。对于POST请求就没那么简单了。根据POST请求中Content-Type类型我们常用的有下面几种:
- application/x-www-form-urlencoded:这种方式是最常见的方式,浏览器原生的form表单就是这种方式提交。
- application/json:这种方式也算是一种常见的方式,当我们在提交一个复杂的对象时往往采用这种方式。
- multipart/form-data:这种方式通常在使用表单上传文件时会用。
注意:上面三种常见的POST方式我实现的过滤器有一种是无法记录到的,当Content-Type为application/json时,通过调用Request对象中getParameter相关方法是无法获取到请求参数的。
application/json解决方案及问题
想要该形式的请求参数能被打印,我们可以通过读取Request中流的方式来获取请求JSON请求参数,现在修改代码如下:
1 | java复制代码@Slf4j |
上面的代码中我通过获取Request中的流来获取到请求提交到服务器中的JSON数据,最后在日志中能打印出客户端提交过来的JSON数据。但是最后接口的返回并没有成功,而且在Controller中也无法获取到请求参数,最后程序给出的错误提示关键信息为:Required request body is missing。
之所以会出现异常是因为Request中的流只能读取一次,我们在过滤器中读取之后如果后面有再次读取流的操作就会导致服务异常,简单的说就是Request中获取的流不支持重复读取。
所以这种方案Pass
扩展HttpServletRequest
HttpServletRequestWrapper
通过上面的分析我们知道了问题所在,对于Request中流无法重复读取的问题,我们要想办法让其支持重复读取。
难道我们要自己去实现一个Request,且我们的Request中的流还支持重复读取,想想就知道这样做很麻烦了。
幸运的是Servlet中提供了一个HttpServletRequestWrapper类,这个类从名字就能看出它是一个Wrapper类,就是我们可以通过它将原先获取流的方法包装一下,让它支持重复读取即可。
创建一个自定义类
继承HttpServletRequestWrapper实现一个CustomHttpServletRequest并且写一个构造函数来缓存body数据,先将RequestBody保存为一个byte数组,然后通过Servlet自带的HttpServletRequestWrapper类覆盖getReader()和getInputStream()方法,使流从保存的byte数组读取。
1 | java复制代码public class CustomHttpServletRequest extends HttpServletRequestWrapper { |
重写getReader()
1 | java复制代码@Override |
重写getInputStream()
1 | java复制代码@Override |
然后再Filter中将ServletRequest替换为ServletRequestWrapper。代码如下:
实现ServletInputStream
创建一个继承了ServletInputStream的类
1 | java复制代码public class CachedBodyServletInputStream extends ServletInputStream { |
创建一个Filter加入到容器中
既然要加入到容器中,可以创建一个Filter,然后加入配置
我们可以简单的继承OncePerRequestFilter然后实现下面方法即可。
1 | java复制代码@Override |
然后,添加该Filter加入即可,在上面的过滤器中先调用了getParameterMap方法获取参数,然后再获取流,如果我先getInputStream然后再调用getParameterMap会导致参数解析失败。
例如,将过滤器中代码调整顺序为如下:
1 | java复制代码@Slf4j |
调整了getInputStream和getParameterMap这两个方法的调用时机,最后却会产生两种结果,这让我一度以为这个是个BUG。最后我从源码中知道了为啥会有这种结果,如果我们先调用getInputStream,这将会getParameterMap时不会去解析参数,以下代码是SpringBoot中嵌入的tomcat实现。
org.apache.catalina.connector.Request:
1 | java复制代码protected void parseParameters() { |
上面代码从方法名字可以看出就是用来解析参数的,其中有一处关键的信息如下:
1 | java复制代码 if (usingInputStream || usingReader) { |
这个判断的意思是如果usingInputStream或者usingReader为true,将导致解析中断直接认为已经解析成功了。这个是两个属性默认都为false,而将它们设置为true的地方只有两处,分别为getInputStream和getReader,源码如下:
getInputStream()
1 | java复制代码public ServletInputStream getInputStream() throws IOException { |
getReader()
1 | java复制代码public BufferedReader getReader() throws IOException { |
为何在tomcat要如此实现呢?tomcat如此实现可能是有它的道理,作为Servlet容器那必须按照Servlet规范来实现,通过查询相关文档还真就找到了Servlet规范中的内容,下面是Servlet3.1规范中关于参数解析的部分内容:
总结
为了获取请求中的参数我们要解决的核心问题就是让流可以重复读取即可,同时注意先读取流会导致getParameterMap时参数无法解析这两点关键点即可。
参考资料
本文转载自: 掘金