HttpMessageConverter

CodeWalker發表於2018-11-19

1.@RestController和@Controller,@RestController = @Controller + @ResponseBody

2.@RequestMapping(mehtod="",value="")用於指定類和方法路徑對映

RequestMapping註解有六個屬性,下面我們把她分成三類進行說明。

1、 value, method;

value: 指定請求的實際地址,指定的地址可以是URI Template 模式(後面將會說明);

method: 指定請求的method型別, GET、POST、PUT、DELETE等;

2、 consumes,produces;

consumes: 指定處理請求的提交內容型別(Content-Type),例如application/json, text/html;

produces: 指定返回的內容型別,僅當request請求頭中的(Accept)型別中包含該指定型別才返回;

3、 params,headers;

params: 指定request中必須包含某些引數值是,才讓該方法處理。

headers: 指定request中必須包含某些指定的header值,才能讓該方法處理請求


3.@ResponseBody

作用:
該註解用於將Controller的方法返回的物件,通過適當的HttpMessageConverter轉換為指定格式後,寫入到Response物件的body資料區,即寫入HttpEntity的body中。

在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實現類中了

1、HttpMessageConverter資訊

HttpMessageConverter<T> 是 Spring3.0 新新增的一個介面,負責將請求資訊轉換為一個物件(型別為 T),將物件(型別為 T)輸出為響應資訊

    

2、HttpMessageConverter 執行原理

(1)HttpInputMessage 將請求的資訊先轉為 InputStream 物件,InputStream 再由 HttpMessageConverter 轉換為 SpringMVC 需要的java物件;

(2)SpringMVC 返回一個 java 物件, 並通過 HttpMessageConverter 轉為響應資訊,接著 HttpOutputMessage 將響應的資訊轉換為 OutputStream,接著給出響應。

3、 HttpMessageConveter 的實現類

4、 SpringMVC 預設載入的 HttpMessageConverter 實現類

(1)DispatcherServlet 預設載入 HttpMessageConveter 的6個實現類,如下所示:

(2)加入 jackson jar包後,啟動的時候載入 7個 HttpMessageConverter 的實現類,如下所示:

5、使用 HttpMessageConverter

(1) @ResponseBody 和 @RequestBody 示例:

(2) HttpEntity 和 ResponseEntity 示例

實現自定義的訊息轉換器要實現AbstractHttpMessageConverter