SpringMVC @RequestBody 為null問題的排查及解決
SpringMVC @RequestBody為null
今天寫一個(gè)springmvc接口,希望入?yún)閖son,然后自動(dòng)轉(zhuǎn)成自己定義的封裝對(duì)象,于是有了下面的代碼
@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;
}
}
程序正常啟動(dòng)后,使用swaggerUI發(fā)起測(cè)試
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'
最后程序報(bào)錯(cuò)
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)
對(duì)了幾遍,接口的編寫和請(qǐng)求內(nèi)容都確定沒有問題,但是請(qǐng)求的json就是沒注入進(jìn)來(lái)轉(zhuǎn)成param對(duì)象。查了一圈資料也沒找到滿意的答案,就只能給springMVC的源碼打斷點(diǎn)跟一遍,看一下具體是哪里出了問題。
由于本篇不是介紹springMVC實(shí)現(xiàn)原理的,就不具體介紹springMVC的源碼。
最后斷點(diǎn)發(fā)現(xiàn)springMVC從request的inputstream沒取出內(nèi)容來(lái)(inputstream.read()出來(lái)的直接是-1)。由于有在一個(gè)攔截器輸出請(qǐng)求的參數(shù)內(nèi)容—>【當(dāng)請(qǐng)求時(shí)get時(shí),通過request.getParameterMap();獲取參數(shù),當(dāng)請(qǐng)求時(shí)post時(shí),則是直接輸出request的inpustream里面的內(nèi)容】。所以請(qǐng)求的body里面是肯定有內(nèi)容的,也就是說(shuō)request.getInputstream()的流是有內(nèi)容的,那為什么到springMVC這read出來(lái)的就是-1呢。
稍微理了下思路,發(fā)現(xiàn)是自己給自己挖了個(gè)坑。答案是:request的inputstream只能讀一次,博主在攔截器中把inputstream的內(nèi)容都輸出來(lái)了,到springMVC這,就沒有內(nèi)容可以讀了。
關(guān)于inputsteam的一些理解
servlet request的inpustream是面向流的,這意味著讀取該inputstream時(shí)是一個(gè)字節(jié)一個(gè)字節(jié)讀的,直到整個(gè)流的字節(jié)全部讀回來(lái),這期間沒有對(duì)這些數(shù)據(jù)做任何緩存。因此,整個(gè)流一旦被讀完,是無(wú)法再繼續(xù)讀的。
這和nio的處理方式就完全不同,如果是nio的話,數(shù)據(jù)是先被讀取到一塊緩存中,然后程序去讀取這塊緩存的內(nèi)容,這時(shí)候就允許程序重復(fù)讀取緩存的內(nèi)容,比如mark()然后reset()或者直接clear()重新讀。
特意去看了下InputStream的源碼,發(fā)現(xiàn)其實(shí)是有mark()和reset()方法的,但是默認(rèn)的實(shí)現(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是一個(gè)空函數(shù),reset函數(shù)直接拋出異常。同時(shí),inputstream還提供了markSupported()方法,默認(rèn)是返回false,表示不支持mark,也就是標(biāo)記(用于重新讀)。
但是并不是所有的Inputstream實(shí)現(xiàn)都不允許重復(fù)讀,比如BufferedInputStream就是允許重復(fù)讀的,從類名來(lái)看,就知道這個(gè)類其實(shí)就是將讀出來(lái)的數(shù)據(jù)進(jìn)行緩存,來(lái)達(dá)到可以重復(fù)讀的效果。下面是BufferedInputStream重寫的3個(gè)方法
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,說(shuō)明它應(yīng)該是支持重復(fù)讀的。我們可以通過mark()和reset()來(lái)實(shí)現(xiàn)重復(fù)讀的效果。
@RequestBody 自動(dòng)映射原理的簡(jiǎn)單介紹
springMVC在處理請(qǐng)求時(shí),先找到對(duì)應(yīng)controller處理該請(qǐng)求的方法,然后遍歷整個(gè)方法的所有參數(shù),進(jìn)行封裝。在處理參數(shù)的過程中,會(huì)調(diào)用AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters()類的方法進(jìn)行進(jìn)行一些轉(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;
}
上面這段代碼主要做的事情大概就是獲取請(qǐng)求的contentType,然后遍歷配置的HttpMessageConverter—>this.messageConverters,如果該HttpMessageConverter可以用于解析這種contentType(genericConverter.canRead方法),就用這種HttpMessageConverter解析請(qǐng)求的請(qǐng)求體內(nèi)容,最后返回具體的對(duì)象。
在spring5.0.7版本中,messageConverters默認(rèn)似乎配置了8種convert。分別是
ByteArrayMessageConverterStringHttpMessageConverterResourceHttpMessageConverterResourceRegionHttpMessageConverterSourceHttpMessageConverterAllEncompassingFormHttpMessageConverterMappingJackson2HttpMessageConverterJaxb2RootElementHttpMessageConverter
具體的convert是哪些contentType并怎么解析的,這里不多做介紹,感興趣的朋友可以自行查看源碼。
比如我們請(qǐng)求的header中的contentType是application/json,那么在遍歷messageConverters的時(shí)候,其他genericConverter.canRead()都會(huì)返回false,說(shuō)明沒有適配上。
然后遍歷到MappingJackson2HttpMessageConverter時(shí)genericConverter.canRead()返回true,接著就去獲取請(qǐng)求的請(qǐng)求體,并通過json解析成我們@RequestBody定義的對(duì)象。
因此,如果我們的請(qǐng)求的contentType和數(shù)據(jù)協(xié)議都是自定義的,我們完全可以自己實(shí)現(xiàn)一個(gè)HttpMessageConverter,然后解析特定的contentType。
最后記得將這個(gè)實(shí)現(xiàn)放入messageConverters中,這樣springMVC就會(huì)自動(dòng)幫我們把請(qǐng)求內(nèi)容解析成對(duì)象了。
關(guān)于@requestBody的一些說(shuō)明
1、@requestBody注解
常用來(lái)處理content-type不是默認(rèn)的application/x-www-form-urlcoded編碼的內(nèi)容,比如說(shuō):application/json或者是application/xml等。一般情況下來(lái)說(shuō)常用其來(lái)處理application/json類型。
2、通過@requestBody
可以將請(qǐng)求體中的JSON字符串綁定到相應(yīng)的bean上,當(dāng)然也可以將其分別綁定到對(duì)應(yīng)的字符串上。
例如說(shuō)以下情況:
$.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字符串中的兩個(gè)變量的值分別賦予了兩個(gè)字符串,但是呢假如我有一個(gè)User類,擁有如下字段:
String userName; String pwd;
那么上述參數(shù)可以改為以下形式:@requestBody User user 這種形式會(huì)將JSON字符串中的值賦予user中對(duì)應(yīng)的屬性上
需要注意的是,JSON字符串中的key必須對(duì)應(yīng)user中的屬性名,否則是請(qǐng)求不過去的。
3、在一些特殊情況
@requestBody也可以用來(lái)處理content-type類型為application/x-www-form-urlcoded的內(nèi)容,只不過這種方式不是很常用,在處理這類請(qǐng)求的時(shí)候,@requestBody會(huì)將處理結(jié)果放到一個(gè)MultiValueMap<String,String>中,這種情況一般在特殊情況下才會(huì)使用,例如jQuery easyUI的datagrid請(qǐng)求數(shù)據(jù)的時(shí)候需要使用到這種方式、小型項(xiàng)目只創(chuàng)建一個(gè)POJO類的話也可以使用這種接受方式。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Boot 中的自動(dòng)配置autoconfigure詳解
這篇文章主要介紹了Spring Boot 中的自動(dòng)配置autoconfigure詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
如何從官網(wǎng)下載Hibernate jar包的方法示例
這篇文章主要介紹了如何從官網(wǎng)下載Hibernate jar包的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2019-04-04
JavaEE中用response向客戶端輸出中文數(shù)據(jù)亂碼問題分析
這篇文章主要介紹了JavaEE中用response向客戶端輸出中文數(shù)據(jù)亂碼問題分析,需要的朋友可以參考下2014-10-10
Java 后端開發(fā)中Tomcat服務(wù)器運(yùn)行不了的五種解決方案
tomcat是在使用Java編程語(yǔ)言開發(fā)服務(wù)端技術(shù)使用最廣泛的服務(wù)器之一,但經(jīng)常在開發(fā)項(xiàng)目的時(shí)候會(huì)出現(xiàn)運(yùn)行不了的情況,這里總結(jié)出幾種能解決的辦法2021-10-10
java 獲取服務(wù)器真實(shí)IP的實(shí)例
這篇文章主要介紹了java 獲取服務(wù)器真實(shí)IP的實(shí)例的相關(guān)資料,這里提供實(shí)現(xiàn)方法幫助大家學(xué)習(xí)理解這部分內(nèi)容,需要的朋友可以參考下2017-08-08
SpringBoot中的統(tǒng)一異常處理詳細(xì)解析
這篇文章主要介紹了SpringBoot中的統(tǒng)一異常處理詳細(xì)解析,該注解可以把異常處理器應(yīng)用到所有控制器,而不是單個(gè)控制器,借助該注解,我們可以實(shí)現(xiàn):在獨(dú)立的某個(gè)地方,比如單獨(dú)一個(gè)類,定義一套對(duì)各種異常的處理機(jī)制,需要的朋友可以參考下2024-01-01
Java超詳細(xì)教你寫一個(gè)學(xué)籍管理系統(tǒng)案例
這篇文章主要介紹了怎么用Java來(lái)寫一個(gè)學(xué)籍管理系統(tǒng),學(xué)籍管理主要涉及到學(xué)生信息的增刪查改,本篇將詳細(xì)的實(shí)現(xiàn),感興趣的朋友跟隨文章往下看看吧2022-03-03
Java Scanner輸入兩個(gè)數(shù)組的方法
今天小編就為大家分享一篇Java Scanner輸入兩個(gè)數(shù)組的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2018-07-07

