SpringMVC原始碼剖析5:訊息轉換器HttpMessageConverter與@ResponseBody註解
本系列文章首發於我的個人部落格: https://h2pl.github.io/
歡迎閱覽我的CSDN專欄:Spring原始碼解析 https://blog.csdn.net/column/details/21851.html
部分程式碼會放在我的的Github: https://github.com/h2pl/
目錄
前言
SpringMVC是目前主流的Web MVC框架之一。
如果有同學對它不熟悉,那麼請參考它的入門blog: http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html
現象
本文使用的demo基於maven,是根據入門blog的例子繼續寫下去的。
我們先來看一看對應的現象。 我們這裡的配置檔案 *-dispatcher.xml中的關鍵配置如下(其他常規的配置檔案不在講解,可參考本文一開始提到的入門blog):
(檢視配置省略)
<mvc:resources location="/static/" mapping="/static/**"/> <mvc:annotation-driven/> <context:component-scan base-package="org.format.demo.controller"/>
pom中需要有以下依賴(Spring依賴及其他依賴不顯示):
<dependency> <groupId>org.codehaus.jackson</groupId> jackson-core-asl <version>1.9.13</version> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> jackson-mapper-asl <version>1.9.13</version> </dependency>
這個依賴是json序列化的依賴。
ok。我們在Controller中新增一個method:
@RequestMapping("/xmlOrJson") @ResponseBody public Map
直接訪問地址:
我們看到,短短几行配置。使用@ResponseBody註解之後,Controller返回的物件 自動被轉換成對應的json資料,在這裡不得不感嘆SpringMVC的強大。
我們好像也沒看到具體的配置,唯一看到的就是*-dispatcher.xml中的一句配置: mvc:annotation-driven/。其實就是這個配置,導致了java物件自動轉換成json物件的現象。
那麼spring到底是如何實現java物件到json物件的自動轉換的呢? 為什麼轉換成了json資料,如果想轉換成xml資料,那該怎麼辦?
原始碼分析
**本文使用的spring版本是4.0.2。 **
在講解 mvc:annotation-driven/這個配置之前,我們先了解下Spring的訊息轉換機制。@ResponseBody這個註解就是使用訊息轉換機制,最終通過json的轉換器轉換成json資料的。
HttpMessageConverter介面就是Spring提供的http訊息轉換介面。有關這方面的知識大家可以參考"參考資料"中的 第二條連結,裡面講的很清楚。
下面開始分析 mvc:annotation-driven/這句配置:
這句程式碼在spring中的解析類是:
在AnnotationDrivenBeanDefinitionParser原始碼的152行parse方法中:
分別例項化了RequestMappingHandlerMapping,ConfigurableWebBindingInitializer,RequestMappingHandlerAdapter等諸多類。
其中 RequestMappingHandlerMapping和RequestMappingHandlerAdapter這兩個類比較重要。
RequestMappingHandlerMapping處理請求對映的,處理@RequestMapping跟請求地址之間的關係。
RequestMappingHandlerAdapter是請求處理的介面卡,也就是請求之後處理具體邏輯的執行,關係到哪個類的哪個方法以及轉換器等工作,這個類是我們講的重點,其中它的屬性messageConverters是本文要講的重點。
私有方法:getMessageConverters
從程式碼中我們可以,RequestMappingHandlerAdapter設定messageConverters的邏輯:
1.如果 mvc:annotation-driven節點有子節點message-converters,那麼它的轉換器屬性messageConverters也由這些子節點組成。
message-converters的子節點配置如下:
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.example.MyHttpMessageConverter"/> <bean class="org.example.MyOtherHttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven>
2.message-converters子節點不存在或它的屬性register-defaults為true的話,加入其他的轉換器:ByteArrayHttpMessageConverter、StringHttpMessageConverter、ResourceHttpMessageConverter等。
我們看到這麼一段:
這些boolean屬性是哪裡來的呢,它們是AnnotationDrivenBeanDefinitionParser的靜態變數。
其中ClassUtils中的isPresent方法如下:
看到這裡,讀者應該明白了為什麼本文一開始在pom檔案中需要加入對應的jackson依賴,為了讓json轉換器jackson成為預設轉換器之一。
mvc:annotation-driven的作用讀者也明白了。
下面我們看如何通過訊息轉換器將java物件進行轉換的。
RequestMappingHandlerAdapter在進行handle的時候,會委託給HandlerMethod(具體由子類ServletInvocableHandlerMethod處理)的invokeAndHandle方法進行處理,這個方法又轉接給HandlerMethodReturnValueHandlerComposite處理。
HandlerMethodReturnValueHandlerComposite維護了一個HandlerMethodReturnValueHandler列表。 HandlerMethodReturnValueHandler是一個對返回值進行處理的策略介面,這個介面非常重要。關於這個介面的細節,請參考樓主的另外一篇部落格: http://www.cnblogs.com/fangjian0423/p/springMVC-request-param-analysis.html。然後找到對應的HandlerMethodReturnValueHandler對結果值進行處理。
最終找到RequestResponseBodyMethodProcessor這個Handler(由於使用了@ResponseBody註解)。
RequestResponseBodyMethodProcessor的supportsReturnType方法:
然後使用handleReturnValue方法進行處理:
我們看到,這裡使用了轉換器。
具體的轉換方法:
至於為何是請求頭部的 Accept資料,讀者可以進去debug這個 getAcceptableMediaTypes方法看看。 我就不羅嗦了~~~
ok。至此,我們走遍了所有的流程。
現在,回過頭來看。為什麼一開始的demo輸出了json資料?
我們來分析吧。
由於我們只配置了 mvc:annotation-driven,因此使用spring預設的那些轉換器。
很明顯,我們看到了2個xml和1個json轉換器。 要看能不能轉換,得看HttpMessageConverter介面的public boolean canWrite(Class<?> clazz, MediaType mediaType)方法是否返回true來決定的。
我們先分析SourceHttpMessageConverter:
它的canWrite方法被父類AbstractHttpMessageConverter重寫了。
發現SUPPORTED_CLASSES中沒有Map類(本文demo返回的是Map類),因此不支援。
下面看Jaxb2RootElementHttpMessageConverter:
這個類直接重寫了canWrite方法。
需要有XmlRootElement註解。 很明顯,Map類當然沒有。
最終MappingJackson2HttpMessageConverter匹配,進行json轉換。(為何匹配,請讀者自行檢視原始碼)
例項講解
我們分析了轉換器的轉換過程之後,下面就通過例項來驗證我們的結論吧。
首先,我們先把xml轉換器實現。
之前已經分析,預設的轉換器中是支援xml的。下面我們加上註解試試吧。
由於Map是jdk原始碼中的部分,因此我們用Employee來做demo。
因此,Controller加上一個方法:
@RequestMapping("/xmlOrJsonSimple") @ResponseBody public Employee xmlOrJsonSimple() { return employeeService.getById(1); }
實體中加上@XmlRootElement註解
結果如下:
我們發現,解析成了xml。
這裡為什麼解析成xml,而不解析成json呢?
之前分析過,訊息轉換器是根據class和mediaType決定的。
我們使用firebug看到:
我們發現Accept有xml,沒有json。因此解析成xml了。
我們再來驗證,同一地址,HTTP頭部不同Accept。看是否正確。
$.ajax({ url: "${request.contextPath}/employee/xmlOrJsonSimple", success: function(res) { console.log(res); }, headers: { "Accept": "application/xml" } });
$.ajax({ url: "${request.contextPath}/employee/xmlOrJsonSimple", success: function(res) { console.log(res); }, headers: { "Accept": "application/json" } });
驗證成功。
關於配置
如果不想使用 mvc:annotation-driven/中預設的RequestMappingHandlerAdapter的話,我們可以在重新定義這個bean,spring會覆蓋掉預設的RequestMappingHandlerAdapter。
為何會覆蓋,請參考樓主的另外一篇部落格: http://www.cnblogs.com/fangjian0423/p/spring-Ordered-interface.html
` `
或者如果只想換messageConverters的話。
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.example.MyHttpMessageConverter"/> <bean class="org.example.MyOtherHttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven>
如果還想用其他converters的話。
以上是spring-mvc jar包中的converters。
這裡我們使用轉換xml的MarshallingHttpMessageConverter。
這個converter裡面使用了marshaller進行轉換
我們這裡使用XStreamMarshaller。
json沒有轉換器,返回406.
至於xml格式的問題,大家自行解決吧。 這裡用的是XStream~。
使用這種方式,pom別忘記了加入xstream的依賴:
<dependency> <groupId>com.thoughtworks.xstream</groupId> xstream <version>1.4.7</version> </dependency>
總結
寫了這麼多,可能讀者覺得有點羅嗦。 畢竟這也是自己的一些心得,希望都能說出來與讀者共享。
剛接觸SpringMVC的時候,發現這種自動轉換機制很牛逼,但是一直沒有研究它的原理,目前,算是了了一個小小心願吧,SpringMVC還有很多內容,以後自己研究其他內容的時候還會與大家一起共享的。
文章難免會出現一些錯誤,希望讀者們能指明出來。
參考資料
詳解RequestBody和@ResponseBody註解
概述 在SpringMVC中,可以使用@ RequestBody和@ResponseBody兩個註解,分別完成請求報文到物件和物件到響應報文的轉換,底層這種靈活的訊息轉換機制,就是Spring3.x中新引入的HttpMessageConverter即訊息轉換器機制。
#Http請求的抽象 還是回到請求-響應,也就是解析請求體,然後返回響應報文這個最基本的Http請求過程中來。我們知道,在servlet標準中,可以用javax.servlet.ServletRequest介面中的以下方法:
public ServletInputStream getInputStream() throws IOException;
來得到一個ServletInputStream。這個ServletInputStream中,可以讀取到一個原始請求報文的所有內容。同樣的,在javax.servlet.ServletResponse介面中,可以用以下方法:
public ServletOutputStream getOutputStream() throws IOException;
來得到一個ServletOutputStream,這個ServletOutputSteam,繼承自java中的OutputStream,可以讓你輸出Http的響應報文內容。
讓我們嘗試著像SpringMVC的設計者一樣來思考一下。我們知道,Http請求和響應報文字質上都是一串字串,當請求報文來到java世界,它會被封裝成為一個ServletInputStream的輸入流,供我們讀取報文。響應報文則是通過一個ServletOutputStream的輸出流,來輸出響應報文。
我們從流中,只能讀取到原始的字串報文,同樣,我們往輸出流中,也只能寫原始的字元。而在java世界中,處理業務邏輯,都是以一個個有業務意義的 物件為處理維度的,那麼在報文到達SpringMVC和從SpringMVC出去,都存在一個字串到java物件的阻抗問題。這一過程,不可能由開發者手工轉換。我們知道,在Struts2中,採用了OGNL來應對這個問題,而在SpringMVC中,它是HttpMessageConverter機制。我們先來看兩個介面。
#HttpInputMessage 這個類是SpringMVC內部對一次Http請求報文的抽象,在HttpMessageConverter的read()方法中,有一個HttpInputMessage的形參,它正是SpringMVC的訊息轉換器所作用的受體“請求訊息”的內部抽象,訊息轉換器從“請求訊息”中按照規則提取訊息,轉換為方法形參中宣告的物件。
package org.springframework.http; import java.io.IOException; import java.io.InputStream; public interface HttpInputMessage extends HttpMessage { InputStream getBody() throws IOException; }
#HttpOutputMessage 這個類是SpringMVC內部對一次Http響應報文的抽象,在HttpMessageConverter的write()方法中,有一個HttpOutputMessage的形參,它正是SpringMVC的訊息轉換器所作用的受體“響應訊息”的內部抽象,訊息轉換器將“響應訊息”按照一定的規則寫到響應報文中。
package org.springframework.http; import java.io.IOException; import java.io.OutputStream; public interface HttpOutputMessage extends HttpMessage { OutputStream getBody() throws IOException; }
#HttpMessageConverter 對訊息轉換器最高層次的介面抽象,描述了一個訊息轉換器的一般特徵,我們可以從這個介面中定義的方法,來領悟Spring3.x的設計者對這一機制的思考過程。
package org.springframework.http.converter; import java.io.IOException; import java.util.List; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; public interface HttpMessageConverter<T> { boolean canRead(Class<?> clazz, MediaType mediaType); boolean canWrite(Class<?> clazz, MediaType mediaType); List<MediaType> getSupportedMediaTypes(); T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }
HttpMessageConverter介面的定義出現了成對的canRead(),read()和canWrite(),write()方法,MediaType是對請求的Media Type屬性的封裝。舉個例子,當我們宣告瞭下面這個處理方法。
@RequestMapping(value="/string", method=RequestMethod.POST) public @ResponseBody String readString(@RequestBody String string) { return "Read string '" + string + "'"; }
在SpringMVC進入readString方法前,會根據@RequestBody註解選擇適當的HttpMessageConverter實現類來將請求引數解析到string變數中,具體來說是使用了StringHttpMessageConverter類,它的canRead()方法返回true,然後它的read()方法會從請求中讀出請求引數,繫結到readString()方法的string變數中。
當SpringMVC執行readString方法後,由於返回值標識了@ResponseBody,SpringMVC將使用StringHttpMessageConverter的write()方法,將結果作為String值寫入響應報文,當然,此時canWrite()方法返回true。
我們可以用下面的圖,簡單描述一下這個過程。
#RequestResponseBodyMethodProcessor 將上述過程集中描述的一個類是org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor,這個類同時實現了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler兩個介面。前者是將請求報文繫結到處理方法形參的策略介面,後者則是對處理方法返回值進行處理的策略介面。兩個介面的原始碼如下:
package org.springframework.web.method.support; import org.springframework.core.MethodParameter; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception; } package org.springframework.web.method.support; import org.springframework.core.MethodParameter; import org.springframework.web.context.request.NativeWebRequest; public interface HandlerMethodReturnValueHandler { boolean supportsReturnType(MethodParameter returnType); void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception; }
RequestResponseBodyMethodProcessor這個類,同時充當了方法引數解析和返回值處理兩種角色。我們從它的原始碼中,可以找到上面兩個介面的方法實現。
對HandlerMethodArgumentResolver介面的實現:
public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); } public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Object argument = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); WebDataBinder binder = binderFactory.createBinder(webRequest, argument, name); if (argument != null) { validate(binder, parameter); } mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); return argument; }
對HandlerMethodReturnValueHandler介面的實現
public boolean supportsReturnType(MethodParameter returnType) { return returnType.getMethodAnnotation(ResponseBody.class) != null; } public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException { mavContainer.setRequestHandled(true); if (returnValue != null) { writeWithMessageConverters(returnValue, returnType, webRequest); } }
看完上面的程式碼,整個HttpMessageConverter訊息轉換的脈絡已經非常清晰。因為兩個介面的實現,分別是以是否有@RequestBody和@ResponseBody為條件,然後分別呼叫HttpMessageConverter來進行訊息的讀寫。
如果你想問,怎麼樣跟蹤到RequestResponseBodyMethodProcessor中,請你按照前面幾篇博文的思路,然後到這裡 spring-mvc-showcase下載原始碼回來,對其中HttpMessageConverter相關的例子進行debug,只要你肯下功夫,相信你一定會有屬於自己的收穫的。
#思考 張小龍在談微信的本質時候說:“微信只是個平臺,訊息在其中流轉”。在我們對SpringMVC原始碼分析的過程中,我們可以從HttpMessageConverter機制中領悟到類似的道理。在SpringMVC的設計者眼中,一次請求報文和一次響應報文,分別被抽象為一個請求訊息HttpInputMessage和一個響應訊息HttpOutputMessage。
處理請求時,由合適的訊息轉換器將請求報文繫結為方法中的形參物件,在這裡,同一個物件就有可能出現多種不同的訊息形式,比如json和xml。同樣,當響應請求時,方法的返回值也同樣可能被返回為不同的訊息形式,比如json和xml。
在SpringMVC中,針對不同的訊息形式,我們有不同的HttpMessageConverter實現類來處理各種訊息形式。但是,只要這些訊息所蘊含的“有效資訊”是一致的,那麼各種不同的訊息轉換器,都會生成同樣的轉換結果。至於各種訊息間解析細節的不同,就被遮蔽在不同的HttpMessageConverter實現類中了。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69906029/viewspace-2654875/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 【SpringMVC】HttpMessageConverter報文資訊轉換器SpringMVCHTTP
- SpringMVC:@ResponseBody註解與HttpServletResponse物件SpringMVCHTTPServlet物件
- SpringMVC的@ResponseBody註解說明SpringMVC
- springmvc 自定義訊息轉換器完整例子SpringMVC
- Spring MVC 中 HttpMessageConverter 轉換器SpringMVCHTTP
- 好程式設計師Java分享SpringMVC之@ResponseBody註解程式設計師JavaSpringMVC
- 全面剖析Android訊息機制原始碼Android原始碼
- swoft2.0 原始碼剖析系列 01-註解器原始碼
- 用於日期轉換的訊息轉換器
- springMVC @ResponseBody 原理SpringMVC
- @ResponseBody註解和@RequestBody註解使用
- SpringMVC的亂碼與時間轉換SpringMVC
- @ResponseBody註解的作用
- @responseBody註解的使用
- 深入剖析 RocketMQ 原始碼 - 訊息儲存模組MQ原始碼
- RabbitMQ 入門(三)SpringAMQP訊息轉換器MQSpringGAM
- 原始碼剖析 啟動Eureka Server(一)@EnableEurekaServer註解原始碼Server
- 精盡Spring Boot原始碼分析 - 剖析 @SpringBootApplication 註解Spring Boot原始碼APP
- SpringMVC(二)處理器方法繫結形參(簡單型別和註解@RequestParam,pojo型別)、自定義型別轉換器、springMVC亂碼解決SpringMVC型別POJO
- springMvc原始碼解讀–AbstractUrlHandlerMappingSpringMVC原始碼APP
- myBatis原始碼解析-型別轉換篇(5)MyBatis原始碼型別
- iOS 訊息傳送與轉發詳解iOS
- Android Handler訊息機制原始碼解讀Android原始碼
- Runtime 從NullSafe原始碼看訊息轉發 機制Null原始碼
- 【SpringMVC】@RequestMapping註解SpringMVCAPP
- 深入瞭解SpringMVC原始碼解析SpringMVC原始碼
- SpringMVC原始碼分析2:SpringMVC設計理念與DispatcherServletSpringMVC原始碼Servlet
- 【原始碼解讀】js原生訊息提示外掛原始碼JS
- YYImage原始碼剖析與學習原始碼
- Spring5原始碼 - Spring IOC 註解複習Spring原始碼
- Java集合原始碼剖析——ArrayList原始碼剖析Java原始碼
- SpringBoot @ConditionalOnBean、@ConditionalOnMissingBean註解原始碼分析與示例Spring BootBean原始碼
- SpringMvc的常用註解SpringMVC
- ArrayList原始碼剖析與程式碼實測原始碼
- SpringMVC原始碼分析3:DispatcherServlet的初始化與請求轉發SpringMVC原始碼Servlet
- 主動寫入流對@ResponseBody註解的影響
- Spring原始碼剖析5:JDK和cglib動態代理原理詳解Spring原始碼JDKCGLib
- Giraph原始碼分析(三)—— 訊息通訊原始碼