SpringMVC @RequestBody 為null問題的排查及解決
SpringMVC @RequestBody為null
今天寫一個springmvc接口,希望入?yún)閖son,然后自動轉(zhuǎn)成自己定義的封裝對象,于是有了下面的代碼
@PostMapping("/update") @ApiOperation("更新用戶信息") public CumResponseBody update(@RequestBody UserInfoParam param) { int userId = getUserId(); userService.updateUserInfo(userId, param); return ResponseFactory.createSuccessResponse("ok"); } //UserInfoParam.java public class UserInfoParam { private String tel; private String email; public String getTel() { return tel; } public void setTel(String tel) { this.tel = tel; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
程序正常啟動后,使用swaggerUI發(fā)起測試
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \ "email": "12%40mail.com", \ "tel": "13677682911" \ }' 'http://127.0.0.1:9998/api/user/update'
最后程序報錯
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.pingguiyuan.shop.common.response.CumResponseBody com.pingguiyuan.shop.weixinapi.controller.UserController.update(com.pingguiyuan.shop.common.param.weixin.UserInfoParam)
對了幾遍,接口的編寫和請求內(nèi)容都確定沒有問題,但是請求的json就是沒注入進來轉(zhuǎn)成param對象。查了一圈資料也沒找到滿意的答案,就只能給springMVC的源碼打斷點跟一遍,看一下具體是哪里出了問題。
由于本篇不是介紹springMVC實現(xiàn)原理的,就不具體介紹springMVC的源碼。
最后斷點發(fā)現(xiàn)springMVC從request的inputstream沒取出內(nèi)容來(inputstream.read()出來的直接是-1)。由于有在一個攔截器輸出請求的參數(shù)內(nèi)容—>【當請求時get時,通過request.getParameterMap();獲取參數(shù),當請求時post時,則是直接輸出request的inpustream里面的內(nèi)容】。所以請求的body里面是肯定有內(nèi)容的,也就是說request.getInputstream()的流是有內(nèi)容的,那為什么到springMVC這read出來的就是-1呢。
稍微理了下思路,發(fā)現(xiàn)是自己給自己挖了個坑。答案是:request的inputstream只能讀一次,博主在攔截器中把inputstream的內(nèi)容都輸出來了,到springMVC這,就沒有內(nèi)容可以讀了。
關(guān)于inputsteam的一些理解
servlet request的inpustream是面向流的,這意味著讀取該inputstream時是一個字節(jié)一個字節(jié)讀的,直到整個流的字節(jié)全部讀回來,這期間沒有對這些數(shù)據(jù)做任何緩存。因此,整個流一旦被讀完,是無法再繼續(xù)讀的。
這和nio的處理方式就完全不同,如果是nio的話,數(shù)據(jù)是先被讀取到一塊緩存中,然后程序去讀取這塊緩存的內(nèi)容,這時候就允許程序重復讀取緩存的內(nèi)容,比如mark()然后reset()或者直接clear()重新讀。
特意去看了下InputStream的源碼,發(fā)現(xiàn)其實是有mark()和reset()方法的,但是默認的實現(xiàn)表示這是不能用的,源碼如下
public boolean markSupported() { return false; } public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } public synchronized void mark(int readlimit) {}
其中mark是一個空函數(shù),reset函數(shù)直接拋出異常。同時,inputstream還提供了markSupported()方法,默認是返回false,表示不支持mark,也就是標記(用于重新讀)。
但是并不是所有的Inputstream實現(xiàn)都不允許重復讀,比如BufferedInputStream就是允許重復讀的,從類名來看,就知道這個類其實就是將讀出來的數(shù)據(jù)進行緩存,來達到可以重復讀的效果。下面是BufferedInputStream重寫的3個方法
public synchronized void mark(int readlimit) { marklimit = readlimit; markpos = pos; } public synchronized void reset() throws IOException { getBufIfOpen(); // Cause exception if closed if (markpos < 0) throw new IOException("Resetting to invalid mark"); pos = markpos; } public boolean markSupported() { return true; }
可以看到BufferedInputStream的markSupported()方法返回的是true,說明它應該是支持重復讀的。我們可以通過mark()和reset()來實現(xiàn)重復讀的效果。
@RequestBody 自動映射原理的簡單介紹
springMVC在處理請求時,先找到對應controller處理該請求的方法,然后遍歷整個方法的所有參數(shù),進行封裝。在處理參數(shù)的過程中,會調(diào)用AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters()類的方法進行進行一些轉(zhuǎn)換操作,源碼如下
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { MediaType contentType; boolean noContentType = false; try { contentType = inputMessage.getHeaders().getContentType(); } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } if (contentType == null) { noContentType = true; contentType = MediaType.APPLICATION_OCTET_STREAM; } Class<?> contextClass = parameter.getContainingClass(); Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null); if (targetClass == null) { ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); targetClass = (Class<T>) resolvableType.resolve(); } HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null); Object body = NO_VALUE; EmptyBodyCheckingHttpInputMessage message; try { message = new EmptyBodyCheckingHttpInputMessage(inputMessage); for (HttpMessageConverter<?> converter : this.messageConverters) { Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : (targetClass != null && converter.canRead(targetClass, contentType))) { if (logger.isDebugEnabled()) { logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } if (message.hasBody()) { HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType); body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse)); body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); } else { body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType); } break; } } } catch (IOException ex) { throw new HttpMessageNotReadableException("I/O error while reading input message", ex); } if (body == NO_VALUE) { if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && !message.hasBody())) { return null; } throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); } return body; }
上面這段代碼主要做的事情大概就是獲取請求的contentType,然后遍歷配置的HttpMessageConverter—>this.messageConverters,如果該HttpMessageConverter可以用于解析這種contentType(genericConverter.canRead方法),就用這種HttpMessageConverter解析請求的請求體內(nèi)容,最后返回具體的對象。
在spring5.0.7版本中,messageConverters默認似乎配置了8種convert。分別是
ByteArrayMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
ResourceRegionHttpMessageConverter
SourceHttpMessageConverter
AllEncompassingFormHttpMessageConverter
MappingJackson2HttpMessageConverter
Jaxb2RootElementHttpMessageConverter
具體的convert是哪些contentType并怎么解析的,這里不多做介紹,感興趣的朋友可以自行查看源碼。
比如我們請求的header中的contentType是application/json,那么在遍歷messageConverters的時候,其他genericConverter.canRead()都會返回false,說明沒有適配上。
然后遍歷到MappingJackson2HttpMessageConverter時genericConverter.canRead()返回true,接著就去獲取請求的請求體,并通過json解析成我們@RequestBody定義的對象。
因此,如果我們的請求的contentType和數(shù)據(jù)協(xié)議都是自定義的,我們完全可以自己實現(xiàn)一個HttpMessageConverter,然后解析特定的contentType。
最后記得將這個實現(xiàn)放入messageConverters中,這樣springMVC就會自動幫我們把請求內(nèi)容解析成對象了。
關(guān)于@requestBody的一些說明
1、@requestBody注解
常用來處理content-type不是默認的application/x-www-form-urlcoded編碼的內(nèi)容,比如說:application/json或者是application/xml等。一般情況下來說常用其來處理application/json類型。
2、通過@requestBody
可以將請求體中的JSON字符串綁定到相應的bean上,當然也可以將其分別綁定到對應的字符串上。
例如說以下情況:
$.ajax({ url:"/login", type:"POST", data:'{"userName":"admin","pwd","admin123"}', content-type:"application/json charset=utf-8", success:function(data) { alert("request success ! "); } });
@requestMapping("/login") public void login(@requestBody String userName,@requestBody String pwd){ System.out.println(userName+" :"+pwd); }
這種情況是將JSON字符串中的兩個變量的值分別賦予了兩個字符串,但是呢假如我有一個User類,擁有如下字段:
String userName; String pwd;
那么上述參數(shù)可以改為以下形式:@requestBody User user 這種形式會將JSON字符串中的值賦予user中對應的屬性上
需要注意的是,JSON字符串中的key必須對應user中的屬性名,否則是請求不過去的。
3、在一些特殊情況
@requestBody也可以用來處理content-type類型為application/x-www-form-urlcoded的內(nèi)容,只不過這種方式不是很常用,在處理這類請求的時候,@requestBody會將處理結(jié)果放到一個MultiValueMap<String,String>中,這種情況一般在特殊情況下才會使用,例如jQuery easyUI的datagrid請求數(shù)據(jù)的時候需要使用到這種方式、小型項目只創(chuàng)建一個POJO類的話也可以使用這種接受方式。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
DOM解析XML報錯Content is not allowed in prolog解決方案詳解
這篇文章主要介紹了DOM解析XML報錯解決方案詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-10-10Mybatis-Plus中Mapper的接口文件與xml文件相關(guān)的坑記錄
這篇文章主要介紹了Mybatis-Plus中Mapper的接口文件與xml文件相關(guān)的坑記錄,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01MyBatis批量插入幾千條數(shù)據(jù)為何慎用foreach
這篇文章主要介紹了MyBatis批量插入幾千條數(shù)據(jù)為何慎用foreach問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10Spring注解驅(qū)動之ApplicationListener異步處理事件說明
這篇文章主要介紹了Spring注解驅(qū)動之ApplicationListener異步處理事件說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09Mybatis詳解動態(tài)SQL以及單表多表查詢的應用
MyBatis的動態(tài)SQL是基于OGNL表達式的,它可以幫助我們方便的在SQL語句中實現(xiàn)某些邏輯,下面這篇文章主要給大家介紹了關(guān)于Mybatis超級強大的動態(tài)SQL語句的相關(guān)資料,需要的朋友可以參考下2022-06-06springboot動態(tài)定時任務的實現(xiàn)方法示例
這篇文章主要給大家介紹了關(guān)于springboot動態(tài)定時任務的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-02-02