背景
最近將基于 Spring Boot 1.4 的項(xiàng)目遷移到 Spring Boot 2.2更啄,遷移后 application/x-www-form-urlencoded
類(lèi)型的 POST 請(qǐng)求獲取 body 失敗沙热,表現(xiàn)為拋出異常
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請(qǐng)求
curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "abc=123&def=456" http://localhost:8080/foo/bar
環(huán)境
JDK 8
Spring Boot 2.2.13
Jetty Servlet 9.4.35
原因
當(dāng)前版本的 Spring 在處理 POST 的 application/x-www-form-urlencoded
類(lèi)型的請(qǐng)求時(shí)骂倘,獲取 body 的操作會(huì)通過(guò) javax.servlet.ServletRequest#getParameterMap
方法獲取表單項(xiàng)后重新拼回 String,而不是調(diào)用 getInputStream()
贮泞。
找到關(guān)鍵的處理方法 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,提供可預(yù)測(cè)的結(jié)果嚎京,而不是從 body 中讀取,如果任何其他代碼使用 ServletRequest 訪問(wèn)參數(shù)隐解,則可能失敗挖藏,從而導(dǎo)致輸入流被“消耗”。
在 jetty 的 HttpServletRequest 實(shí)現(xiàn)中厢漩,有一個(gè) _inputState
內(nèi)部標(biāo)志膜眠,該標(biāo)志在調(diào)用 getReader()
或 getInputStream()
時(shí)被更新,之后在調(diào)用 getParameterMap()
方法時(shí),會(huì)判斷 _inputState
是否被更新過(guò)宵膨,如過(guò)被更新過(guò)架谎,則視為被“消耗”了,這時(shí) getParameterMap()
將返回空結(jié)果辟躏。
而我們的項(xiàng)目中設(shè)置了一個(gè)用于檢查登錄 token 的 servlet 過(guò)濾器谷扣,該過(guò)濾器包裝了原來(lái)的 HttpServletRequest
,將 getInputStream()
方法返回的內(nèi)容緩存起來(lái)捎琐,以使 body 其可被重復(fù)讀取会涎。因?yàn)樵谠撨^(guò)濾器中已經(jīng)調(diào)用過(guò)一次 getInputStream()
方法,用于獲取 body 內(nèi)容來(lái)檢查是否存在 token瑞凑,而這個(gè)操作會(huì)使 HttpServletRequest
內(nèi)部的 _inputState
標(biāo)志被更新末秃,所以導(dǎo)致后續(xù) Spring 無(wú)法獲得表單內(nèi)容。
解決
目前我的解決辦法是修改控制器方法籽御,直接從 HttpServletRequest
中獲取 body 內(nèi)容
@Controller
@RequestMapping("/foo")
public class FooController {
@RequestMapping("/bar")
public void bar(HttpServletRequest request) {
ServletInputStream inputStream = request.getInputStream();
// read content from inputStream
// do something
}
}