背景
最近将基于 Spring Boot 1.4 的项目迁移到 Spring Boot 2.2,迁移后 application/x-www-form-urlencoded
类型的 POST 请求获取 body 失败,表现为抛出异常
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public void com.example.controller.FooController.bar(java.lang.String)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:161)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:131)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
...
控制器方法
@Controller
@RequestMapping("/foo")
public class FooController {
@RequestMapping("/bar")
public void bar(@RequestBody String content) {
// do something
}
}
curl请求
curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "abc=123&def=456" http://localhost:8080/foo/bar
环境
JDK 8
Spring Boot 2.2.13
Jetty Servlet 9.4.35
原因
当前版本的 Spring 在处理 POST 的 application/x-www-form-urlencoded
类型的请求时,获取 body 的操作会通过 javax.servlet.ServletRequest#getParameterMap
方法获取表单项后重新拼回 String,而不是调用 getInputStream()
。
找到关键的处理方法 org.springframework.http.server.ServletServerHttpRequest#getBodyFromServletRequestParameters
,该方法的注释为
Use javax.servlet.ServletRequest.getParameterMap() to reconstruct the body of a form 'POST' providing a predictable outcome as opposed to reading from the body, which can fail if any other code has used the ServletRequest to access a parameter, thus causing the input stream to be "consumed".
翻译: 使用 javax.servlet.ServletRequest.getParameterMap() 重建表单 'POST' 的 body,提供可预测的结果,而不是从 body 中读取,如果任何其他代码使用 ServletRequest 访问参数,则可能失败,从而导致输入流被“消耗”。
在 jetty 的 HttpServletRequest 实现中,有一个 _inputState
内部标志,该标志在调用 getReader()
或 getInputStream()
时被更新,之后在调用 getParameterMap()
方法时,会判断 _inputState
是否被更新过,如过被更新过,则视为被“消耗”了,这时 getParameterMap()
将返回空结果。
而我们的项目中设置了一个用于检查登录 token 的 servlet 过滤器,该过滤器包装了原来的 HttpServletRequest
,将 getInputStream()
方法返回的内容缓存起来,以使 body 其可被重复读取。因为在该过滤器中已经调用过一次 getInputStream()
方法,用于获取 body 内容来检查是否存在 token,而这个操作会使 HttpServletRequest
内部的 _inputState
标志被更新,所以导致后续 Spring 无法获得表单内容。
解决
目前我的解决办法是修改控制器方法,直接从 HttpServletRequest
中获取 body 内容
@Controller
@RequestMapping("/foo")
public class FooController {
@RequestMapping("/bar")
public void bar(HttpServletRequest request) {
ServletInputStream inputStream = request.getInputStream();
// read content from inputStream
// do something
}
}