網絡上對這個問題的分析及解決不是很深入,大部分并不能解決問題,而且內容基本相同,拿來主義,把內容放在自己的博客上!
報錯原因可能有兩種情況:
1.請求頭中沒有設置Content-Type參數,或Content-Type參數值不是application/json;
2.請求頭中正確設置了Content-Type參數及參數值,但是在項目jar依賴中(pom.xml或build.gradle)沒有添加處理json字符串的處理類,如果SpringMVC框架在啟動的時候,檢查com.fasterxml.jackson.databind.ObjectMapper和com.fasterxml.jackson.core.JsonGenerator有一個不存在或不能加載,則不會注冊MappingJackson2HttpMessageConverter,這個類使用Jackson將json請求參數轉成相應的方法參數;同樣檢查com.google.gson.Gson,如果不存在或不能加載,則不會注冊GsonHttpMessageConverter,這個類使用Gson將json請求參數轉成相應的方法參數;如果依賴的Jackson和Gson都沒有被添加或不能加載,則SpringMVC將找不到對應的參數處理類。
源碼分析
在使用SpringMVC的時候,都會添加<mvc:annotation-driven />注解,這個注解下有很多可以配置的擴展參數,有興趣的可以研究一下。有這個注解,就必定有對應的注解解析,查看NamespaceHandler接口的實現類,發現有一個MvcNamespaceHandler。
annotation-driven注解做了什么,直接看AnnotationDrivenBeanDefinitionParser類。這個類中主要的就是parse方法,這個方法中做了很多重要的事,如對一些可擴展的參數進行了解析注冊,這些不是本篇的重點,有興趣的可以研究一下,關注重點代碼。
代碼中的messageConverters是消息轉換器集合,里面包含了對json、xml、atom、rss格式報文的轉換。接著,把messageConverters添加到RequestMappingHandlerAdapter中,RequestMappingHandlerAdapter是處理@RequestMapping注解的HandlerAdapter,簡單說就是標注了@RequestMapping注解的Controller,是經過RequestMappingHandlerAdapter進行調用的。messageConverters是它的一個屬性,代碼如下。
繼續看AnnotationDrivenBeanDefinitionParser類,分析上圖紅框中的ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext),深入這個getMessageConverters方法。
romePresent、jaxb2Present、jackson2Present、jackson2XmlPresent、gsonPresent為true則將對應的轉換器包裝成BeanDefinition,然后將其添加到messageConverters集合中。這幾個布爾變量的值在AnnotationDrivenBeanDefinitionParser類的開頭處就賦值了。
如果相應的實現類存在并且可以被加載,則對應的布爾變量值為true,否則為false。也就是說,如果SpringMVC框架在啟動的時候,檢查com.fasterxml.jackson.databind.ObjectMapper和com.fasterxml.jackson.core.JsonGenerator有一個不存在或不能加載,則不會注冊MappingJackson2HttpMessageConverter,這個類使用Jackson將json請求參數轉成相應的方法參數;同樣檢查com.google.gson.Gson,如果不存在或不能加載,則不會注冊GsonHttpMessageConverter,這個類使用Gson將json請求參數轉成相應的方法參數;如果依賴的Jackson和Gson都沒有被添加或不能加載,則SpringMVC將找不到json參數轉換類,也就沒辦法處理。
如果配置了json參數轉換處理類,SpringMVC框架將根據請求頭中的Content-Type參數遍歷messageConverters,選擇匹配的轉換器類,進行參數轉換。如果Content-Type參數值類型是messageConverters中不支持的,那么就沒辦法做轉換。
總結
首先,SpringMVC框架在啟動的時候會遍歷Spring容器中的所有bean,對標注了@Controller或@RequestMapping注解的類中方法進行遍歷,將類和方法上的@RequestMapping注解值進行合并,使用@RequestMapping注解的相關參數值(如value、method等)封裝一個RequestMappingInfo,將這個Controller實例、方法及方法參數信息(類型、注解等)封裝到HandlerMethod中,然后以RequestMappingInfo為key,HandlerMethod為value存到一個以Map為結構的handlerMethods中。
接著,將@RequestMapping注解中的value(即請求路徑)值取出,即url,然后以url為key,以RequestMappingInfo為value,存到一個以Map為結構的urlMap屬性中。
客戶端發起請求的時候,根據請求的URL到urlMap中查找,找到RequestMappingInfo,然后根據RequestMappingInfo到handlerMethods中查找,找到對應的HandlerMethod,接著將HandlerMethod封裝到HandlerExecutionChain;接著遍歷容器中所有HandlerAdapter實現類,找到支持這次請求的HandlerAdapter,如RequestMappingHandlerAdapter,然后執行SpringMVC攔截器的前置方法(preHandle方法),然后對請求參數解析及轉換,這里主要根據HandlerMethod中封裝的參數信息(方法參數上的注解)來遍歷argumentResolvers(List結構,存儲了HandlerMethodArgumentResolver接口實現類,不同實現類,實現對不同注解參數的解析,如RequestResponseBodyMethodProcessor可以實現對@RequestBody和@ResponseBody參數的解析),找到支持這個注解的HandlerMethodArgumentResolver實現類,然后解析請求參數。
插播一下請求參數的解析及轉換,下圖是HandlerMethodArgumentResolver接口的實現類。
從上圖中可以看到很多常見注解參數的解析類,這里分析RequestResponseBodyMethodProcessor,其它處理類感興趣的可以自己研究一下。RequestResponseBodyMethodProcessor會從請求頭中獲取Content-Type參數值,例如application/json,然后遍歷messageConverters,查找能夠處理這種Content-Type的轉換器類,如果messageConverters中有可以處理application/json請求的處理類,如Jackson或Gson,則使用Jackson或Gson對請求體中的參數進行讀取轉換,轉換成具體方法參數類型,下面是Jackson具體的處理代碼。
如果messageConverters沒有匹配的處理類,那就會報415。
最后,(使用反射)調用具體Controller的對應方法返回一個ModelAndView對象,執行攔截器的后置方法(postHandle方法),然后對返回的結果進行處理,最后執行afterCompletion方法。