这不是简单的获取不到参数的问题.
一、问题背景
项目使用springboot+mybatis, 是一个后台系统. 其中有一些功能:
- 有一个自定义的全局过滤器DecryptFilter, 用于接口的解密与验签
- 有一个自定义的全局过滤器LogFilter, 用于打印所有接口的请求参数与响应内容.
因此我们自定义了一个RequestWrapper对象包装了原本的HttpServletRequest, 并将其中的InputStream保存起来, 方便后面的代码去获取请求体或者请求流.
基于此背景的项目, 在新版本的迭代中, 一个表单提交的POST接口的处理便出现了问题.
二、问题现象.
问题的现象不仅在新版本中, 老版本也有. 只不过没引起注意, 在这次的问题暴露出来才排查到的.
- 在新的迭代中, Service层的逻辑代码中, 通过httpServletRequest.getParameter()获取参数为空
- 在新的迭代中, 全局过滤器LogFilter打印了请求体的内容, 与调用方调用时的请求参数一致(不为空).
- 在老版本中, 全局过滤器LogFilter打印的请求体内容为空, 但是httpServletRequest.getParameter()获取参数却是正确的.
三、分析思路
- 首先第一反应就想到:
是不是迭代的时候改到了全局过滤器或者自己封装的RequestWrapper对象, 导致后面再通过getParameter()就获取不到.
那么对比这几个关键的代码发现并没有任何改动, 并且在老版本中getParameter是可以获取到参数的. 因此这个猜想暂时的排除.
- 那接下来根据现象, 之前打印请求体是空, 现在又可以打印出请求体内容(GET请求只会打印QueryString, 其他请求只会打印请求体), 自然想到:
会不会是调用方修改了调用方式, 原来是将请求参数拼接到URL后面, 现在将请求参数放到请求体里面去啦?
这个猜想明显不能说明为什么新版本使用getParameter获取参数, 但还是求证了一下. 查看Nginx的访问日志便可知道, 之前的调用是否是将参数拼接在URL之后. 结果自然是没有.
-
查看getParameter的方法说明如图:
里面有重要的一点说明, 就是如果在POST请求中, 你直接使用getInputStream或getReader来获取请求体的话, 会对getParameter方法的执行造成影响. 但是什么影响呢, 目前并不清楚. -
没有其他思路, 那便模拟接口调用, 在本地进行DEBUG看看问题出现在哪里.
在对新老版本分别DEBUG的时候, getParameter方法的执行路径在RequestWrapper中都一摸一样符合预期, 也进一步佐证了不会是第一个猜想. 然后在专门对新版本进行DEBUG时发现了一些有趣的现象
- 在全局过滤器的第一行进行getParameter操作, 发现是可以获取到参数的.
- 慢慢调试发现, 在生成RequestWrapper对象之后进行getParameter就获取不到参数了.
- 并且如果在new RequestWrapper之前进行了getParameter操作, 那么在后续的Service层中, 也可以获取到参数了. 但是RequestWrapper中获取到的原始InputStream就为空了
- 如果是在new RequestWrapper之后进行的getParameter操作时, 其中获取原始的InputStream不为空.
但是new RequestWrapper()里面啥也没做呀, 就只有关键一行代码如下:
body = StreamUtil.readBytes(request.getReader(), "UTF-8");
而且老版本也是一样的代码呀, 却可以正常获取到参数值呀.
- 带着疑问继续DEBUG到getParameter内部具体实现, 到了org.apache.catalina.connector.Request这个类中, 如下图:
解释就是, 如果参数没有被解析, 那么就去解析, 否则就直接获取. OK那么继续DEBUG发现当时是未解析的, 因此就进入到了详细的参数解析中, 前一部分代码如图:
结果呢, 在执行到红框那部分代码时, 便直接return了, 难怪没有参数呢. 那为啥直接return不继续解析了呢. 那就看看usingInputStream和usingReader到底干啥的.
跟踪这两个变量值发现:
只有在getInputStream()方法中, usingInputStream=true. 而只有在getReader()方法中, usingReader=true.
那这就表明, 只要你事先调用过了getInputStream或者getReader, 再调用getParameter就不会进行解析了,也就解释了为啥获取不到参数.
所以还真的是RequestWrapper的错!? 那为啥老版本没有问题??
- 其实老版本也是有问题的, 像最开始所说的, 只是没有引起注意而已:
在老版本中, 全局过滤器LogFilter打印的请求体内容为空, 但是httpServletRequest.getParameter()获取参数却是正确的.
那就要找到为啥老版本不打印参数了. 现在就开始DEBUG老版本.
首先, 同样在全局过滤器的第一行获取请求体进行打印, 发现......getReader()获取到的是空的. 我第一行打印都是空的是怎么回事?!!之前都执行过啥什么!
然后, 看了一眼DEBUG的栈调用路径, 发现调用路径与新版本不一样啊, 执行的一些Spring的过滤器也不一样. 为啥突然调用路径都变化了?
接着, 由于没有好的解释, 只能使用排除法, 切换回DEBUG新版本, 优先还原配置相关的改动一点点的试.
最后, 在我将新增加的WebMvcConfigurationSupport配置注释掉之后, 发现新版本的表现与老版本一致了.将WebMvcConfigurationSupport作为关键词搜了一下, 发现这货居然关联着WebMvcAutoConfiguration这个自动配置.
Springboot只要加了这货, 就不会加载WebMvcAutoConfiguration, 看来自动配置还是坑多呀. 而且新版本缺少的正是上图下面框起来的过滤器.
- 那这些默认自动配置的过滤器为啥就能让getReader为空呢? 找到HiddenHttpMethodFilter这个过滤器, 发现了下面的代码
原来这个过滤器里面执行了一次getParameter(). 这下所有问题都能解释通了.
四、总结
- Tomcat的ServletRequest中, getParameter()方法与getInputStream()/getReader()不兼容, 只能选择一方.调用了一方, 另一方就会是空的(前提:表单的POST请求).
- WebMvcConfigurationSupport配置会导致WebMvcAutoConfiguration不加载
- WebMvcAutoConfiguration中会加载一些配置, 可能影响你的一些行为.
- 最后, 为了降低影响跟老版本保持一致, 还是采用了WebMvcAutoConfiguration, 去掉了新增的WebMvcConfigurationSupport, 使用了WebMvcRegistrations来作为代替.
最后有两点疑问:
- 为什么使用@RequestBody或者@RequestParam可以获取到请求参数
- Servlet为什么会将请求体保存在流中,只能读一次? 这样多次读取请求体都不方便. 为什么不直接存在一个字节数组中?