问题背景
项目是 SpringBoot 单体式,在项目中,为了实现调用 controller 请求的日志记录功能。因此做了以下配置:
- 创建自定义拦截器 LogInterceptor;
- 因为需要使用到流获取请求参数,解决流只能读一次问题,所以需要自定义 HttpServletRequestWrapper;
- 需要使得自定义 HttpServletRequestWrapper 生效,因此还需要自定义 Filter (HttpServletRequestFilter);
目前在项目中可以正常使用。现在需要把项目的这些配置复制到另一个项目,其中Springboot版本一致,其他三方依赖什么都一样。但是新项目导入文件时,报参数缺失。
排查思路
-
查询新老项目配置的 filter是否一样?
发现是一样的。并且没有配置多余的filter。
-
查询新老项目导入文件接口的 controller 请求入参方式是否一样?
发现导入文件时,都是form 表单,content-type 都是 multipart/form-data。(导入文件仅支持form表单,因此第一次排查属于多余!)
-
怀疑拦截器 LogInterceptor 使用了 request,导致 request数据丢失。
通过debug发现,其实走到 logInterceptor 时,request 数据已经丢失。
-
既然还没走到拦截器,那可能是自定义 HttpServletRequestWrapper 或者 自定义 HttpServletRequestFilter 问题,接着 debug,发现配置都没问题,可以正常执行。
-
依次排查了 filter 的加载顺序,发现也没啥用。
-
排除了 自定义 requestWrapper 和 filter,又发现可以正常获取数据了。然后试着将 requestWrapper 中构造方式的流转换为字节的操作放在每次获取 getInputStream() 方法中,发现也可以。因此怀疑是底层加载出现问题了。
-
试着对比新老项目所使用的 filter,我通过debug打断点的方式,可以在ApplicationFilterChain的internalDoFilter()方法上打断点。
发现两边不太一样。查询 hiddenHttpMethodFilter过滤器,发现是在 WebMvcAutoConfiguration 自动装配的,但是必须设置 spring.mvc.hiddenmethod.filter.enabled=ture。然后查询项目中 application.yml 配置,发现新老项目这块缺失不一样。然后需要新项目配置,发现问题解决。
根因分析
思考:
-
为啥调整 requestWrapper 中流转换为字节操作的位置,从构造方式移到 getInputStream() 方法。就可以。
-
为啥非要使用 hiddenHttpMethodFilter 过滤器,也就能解决问题
分析:
通过百度和查看 hiddenHttpMethodFilter 源码发现,org.apache.catalina.connector.Request.parseParameters() 方法中,判断事先调用过了getInputStream或者getReader,再调用getParameter就不会进行解析了。**也就是在一个请求链中,请求对象被前面对象方法中调用request.getInputStream()或request.getReader()获取过内容后,后面的对象方法里再调用这两个方法也无法获取到客户端请求的内容,但是调用request.getParameter()方法获取过内容后,后面的对象方法里依然可以调用它获取到参数的内容。**这也就验证了以上两个问题
- 问题1,移动位置后,在 自定义 filter中, 将 httpServletRequest 对象转换成对象时,不执行 getInputStream() 方法,也就解决了问题。
- 问题2,使用 hiddenHttpMethodFilter 过滤器,里边执行了一行代码 String paramValue = request.getParameter(this.methodParam);,这行代码保证了parseParameters()方法优先执行,也能解决问题。
根因:
Tomcat的ServletRequest中, getParameter()方法与getInputStream()/getReader()不兼容, 只能选择一方.调用了一方, 另一方就会是空的(前提:表单的POST请求)。
解决方案
原理已经清楚了,按时会产生出各种不同的问题,因此解决方案需要根据问题场景来说。常见的有:
-
调整自定义 RequestWrapper 中流转换为字节操作的位置,使其放在 getInputStream() 方法中,这样就不影响底层框架使用。
-
在 自定义 filter 中,继承 OncePerRequestFilter, 并重写 shouldNotFilter(HttpServletRequest request) 方法,然后只对 Json 请求内容做流只能读取一次的处理(按需引入 Wrapper)。
扩展知识
application/x- www-form-urlencoded 是 Post 请求默认的请求体内容类型,该请求方式是通过调用 request.getParameter() 方法来获取请求参数值。
当请求体内容(注意:get请求没有请求体)类型是 application/x- www-form-urlencoded 时也可以直接调用request.getInputStream()或request.getReader()方法获取到请求内容再解析出具体都参数,但前提是还没调用request.getParameter()方法。此时当request.getInputStream()或request.getReader()获取到请求内容后,无法再调request.getParameter()获取请求内容。即对该类型的请求,三个方法互斥,只能调其中一个。
application/json 是 Post 请求 body 体传参方式,该请求方式是通过调用 request.getInputStream()或request.getReader() 方法来获取请求内容值。
multipart/form-data 是Post 请求文件上传的传参方式,这种比较特殊,经测试,可以使用 request.getParameter() 获取到除文件外的其他参数,获取文件参数,需要使用 request.getInputStream() 。
参考链接:
自定义httpservletrequestwrapper导致form表单提交数据丢失_添加requestwapper后无法获取提交参数