SpringMVC源碼剖析5:消息轉換器HttpMessageConverter與@ResponseBody註解

轉自 [SpringMVC關於json、xml自動轉換的原理研究[附帶源碼分析]](https://www.cnblogs.com/fangj...html

本系列文章首發於個人我的博客:https://h2pl.github.io/java

歡迎閱覽個人CSDN專欄:Spring源碼解析 https://blog.csdn.net/column/...ios

部分代碼會放在個人的Github:https://github.com/h2pl/
<!-- more -->git

目錄

前言

SpringMVC是目前主流的Web MVC框架之一。 github

若是有同窗對它不熟悉,那麼請參考它的入門blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.htmlweb

現象

本文使用的demo基於maven,是根據入門blog的例子繼續寫下去的。ajax

咱們先來看一看對應的現象。 咱們這裏的配置文件 *-dispatcher.xml中的關鍵配置以下(其餘常規的配置文件不在講解,可參考本文一開始提到的入門blog):spring

(視圖配置省略)json

<mvc:resources location="/static/" mapping="/static/**"/>
<mvc:annotation-driven/>
<context:component-scan base-package="org.format.demo.controller"/>

pom中須要有如下依賴(Spring依賴及其餘依賴不顯示):segmentfault

<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:

複製代碼

<pre>@RequestMapping("/xmlOrJson")
@ResponseBody public Map<String, Object> xmlOrJson() {

Map<String, Object> map = new HashMap<String, Object>();
map.put("list", employeeService.list()); return map;

}</pre>

複製代碼

直接訪問地址:

咱們看到,短短几行配置。使用@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加上一個方法:

<pre>@RequestMapping("/xmlOrJsonSimple")
@ResponseBody public Employee xmlOrJsonSimple() { return employeeService.getById(1);
}</pre>

實體中加上@XmlRootElement註解

結果以下:

咱們發現,解析成了xml。

這裏爲何解析成xml,而不解析成json呢?

以前分析過,消息轉換器是根據class和mediaType決定的。

咱們使用firebug看到:

咱們發現Accept有xml,沒有json。所以解析成xml了。

咱們再來驗證,同一地址,HTTP頭部不一樣Accept。看是否正確。

複製代碼

<pre>$.ajax({

url: "${request.contextPath}/employee/xmlOrJsonSimple",
success: function(res) {
    console.log(res);
},
headers: { "Accept": "application/xml" }

});</pre>

複製代碼

複製代碼

<pre>$.ajax({

url: "${request.contextPath}/employee/xmlOrJsonSimple",
success: function(res) {
    console.log(res);
},
headers: { "Accept": "application/json" }

});</pre>

複製代碼

驗證成功。

關於配置

若是不想使用<mvc:annotation-driven/>中默認的RequestMappingHandlerAdapter的話,咱們能夠在從新定義這個bean,spring會覆蓋掉默認的RequestMappingHandlerAdapter。

爲什麼會覆蓋,請參考樓主的另一篇博客:http://www.cnblogs.com/fangjian0423/p/spring-Ordered-interface.html

<pre> `<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">

<list>
  <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
  <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
  <bean class="org.springframework.http.converter.ResourceHttpMessageConverter"/>
</list>

</property>
</bean>` </pre>

或者若是隻想換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還有不少內容,之後本身研究其餘內容的時候還會與你們一塊兒共享的。

文章不免會出現一些錯誤,但願讀者們能指明出來。

參考資料

http://my.oschina.net/HeliosFly/blog/205343

http://my.oschina.net/lichhao/blog/172562

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html

詳解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實現類中了。

圖片描述

相關文章
相關標籤/搜索