背景
最近将基于 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
}
}