精盡Spring MVC原始碼分析 - HandlerAdapter 元件(五)之 HttpMessageConverter
阿新 • • 發佈:2020-12-21
> 該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 [Spring MVC 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀
>
> Spring 版本:5.2.4.RELEASE
>
> 該系列其他文件請檢視:[**《精盡 Spring MVC 原始碼分析 - 文章導讀》**](https://www.cnblogs.com/lifullmoon/p/14123963.html)
## HandlerAdapter 元件
HandlerAdapter 元件,處理器的介面卡。因為處理器 `handler` 的型別是 Object 型別,需要有一個呼叫者來實現 `handler` 是怎麼被執行。Spring 中的處理器的實現多變,比如使用者的處理器可以實現 Controller 介面或者 HttpRequestHandler 介面,也可以用 `@RequestMapping` 註解將方法作為一個處理器等,這就導致 Spring MVC 無法直接執行這個處理器。所以這裡需要一個處理器介面卡,由它去執行處理器
由於 HandlerMapping 元件涉及到的內容較多,考慮到內容的排版,所以將這部分內容拆分成了五個模組,依次進行分析:
- [**《HandlerAdapter 元件(一)之 HandlerAdapter》**](https://www.cnblogs.com/lifullmoon/p/14137467.html)
- [**《HandlerAdapter 元件(二)之 ServletInvocableHandlerMethod》**](https://www.cnblogs.com/lifullmoon/p/14137483.html)
- [**《HandlerAdapter 元件(三)之 HandlerMethodArgumentResolver》**](https://www.cnblogs.com/lifullmoon/p/14137494.html)
- [**《HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler》**](https://www.cnblogs.com/lifullmoon/p/14137508.html)
- [**《HandlerAdapter 元件(五)之 HttpMessageConverter》**](https://www.cnblogs.com/lifullmoon/p/14137520.html)
## HandlerAdapter 元件(五)之 HttpMessageConverter
本文是接著[**《HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler》**](https://www.cnblogs.com/lifullmoon/p/14137508.html)一文來分享 **HttpMessageConverter** 元件。在 `HandlerAdapter` 執行處理器的過程中,具體的執行過程交由 `ServletInvocableHandlerMethod` 物件來完成,其中需要先通過 `HandlerMethodArgumentResolver` 引數解析器從請求中解析出方法的入參,然後再通過反射機制呼叫對應的方法,獲取到執行結果後需要通過 `HandlerMethodReturnValueHandler` 結果處理器來進行處理。在處理返回結果的過程中,可能需要通過 **HttpMessageConverter** 訊息轉換器將返回結果設定到響應體中,當然也可能需要通過它從請求體獲取入參。
在使用 Spring MVC 時,`@RequestBody` 和 `@ResponseBody` 兩個註解,分別完成**請求報文到 Java 物件**、**Java 物件到響應報文**的轉換,底層的實現就是通過 Spring 3.x 中引入的 **HttpMessageConverter** 訊息轉換機制來實現的。
再開始閱讀本文之前,先來理解一些概念。在處理 HTTP 請求的過程中,需要解析請求體,返回結果設定到響應體。在 Servlet 標準中,`javax.servlet.ServletRequest` 和 `javax.servlet.ServletResponse` 分別有有以下方法:
```java
// javax.servlet.ServletRequest
public ServletInputStream getInputStream() throws IOException;
// javax.servlet.ServletResponse
public ServletOutputStream getOutputStream() throws IOException;
```
通過上面兩個方法可以獲取到請求體和響應體,ServletInputStream 和 ServletOutputStream 分別繼承 java 中的 InputStream 和 OutputStream 流物件,可以通過它們獲取請求報文和設定響應報文。我們只能從流中讀取原始的字串報文,或者往流中寫入原始的字串,而 Java 是面向物件程式設計的,字串與 Java 物件之間的轉換不可能交由開發者去實現。在 Sping MVC 中,會將 Servlet 提供的請求和響應進行一層**抽象**封裝,便於操作讀取和寫入,再通過 **HttpMessageConverter** 訊息轉換機制來解析請求報文或者設定響應報文。
### 回顧
先來回顧一下 `HandlerMethodReturnValueHandler` 如何處理放回結果的,可以回到 [**《HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler》**](https://www.cnblogs.com/lifullmoon/p/14137508.html) 中 **RequestResponseBodyMethodProcessor** 小節下面的 `handleReturnValue` 方法和 `writeWithMessageConverters` 方法
#### handleReturnValue
```java
// RequestResponseBodyMethodProcessor.java
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// <1> 設定已處理
mavContainer.setRequestHandled(true);
// <2> 建立請求和響應
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// <3> 使用 HttpMessageConverter 對物件進行轉換,並寫入到響應
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
// AbstractMessageConverterMethodProcessor.java
protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
return new ServletServerHttpRequest(servletRequest);
}
// AbstractMessageConverterMethodProcessor.java
protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
Assert.state(response != null, "No HttpServletResponse");
return new ServletServerHttpResponse(response);
}
```
上面會將請求封裝成 ServletServerHttpRequest 和 ServletServerHttpResponse 物件
- ServletServerHttpRequest:實現了 ServerHttpRequest、HttpRequest、HttpInputMessage、HttpMessage介面
- ServletServerHttpResponse:實現 ServerHttpResponse、HttpOutputMessage 介面
上面這些介面定義了一些獲取請求和設定響應相關資訊的方法,便於獲取請求和設定響應
#### writeWithMessageConverters
```java
// AbstractMessageConverterMethodProcessor.java
protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// <1> 獲得 body、valueType、targetType
Object body; Class> valueType; Type targetType;
// <3> 選擇使用的 MediaType
MediaType selectedMediaType = null;
// <4> 如果匹配到,則進行寫入邏輯
if (selectedMediaType != null) {
// <4.1> 移除 quality 。例如,application/json;q=0.8 移除後為 application/json
selectedMediaType = selectedMediaType.removeQualityValue();
// <4.2> 遍歷 messageConverters 陣列
for (HttpMessageConverter> converter : this.messageConverters) {
// <4.3> 判斷 HttpMessageConverter 是否支援轉換目標型別
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter
? (GenericHttpMessageConverter>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType)
: converter.canWrite(valueType, selectedMediaType)) {
// <5.2> body 非空,則進行寫入
if (body != null) {
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
} else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
// <5.4> return 返回,結束整個邏輯
return;
}
}
}
// ... 上面省略了大量程式碼
}
```
`<4.2>` 處,遍歷所有的 HttpMessageConverter 實現類
`<4.3>` 處,呼叫當前 HttpMessageConverter 實現類的 `canWrite` 方法,判斷是否支援寫入
`<5.2>` 處,呼叫該 HttpMessageConverter 實現類的 `write` 方法,進行寫入
### HttpInputMessage 介面
`org.springframework.http.HttpInputMessage`:對一次 Http 請求報文的抽象
```java
public interface HttpInputMessage extends HttpMessage {
/**
* Return the body of the message as an input stream.
* @return the input stream body (never {@code null})
* @throws IOException in case of I/O errors
*/
InputStream getBody() throws IOException;
}
```
在 HttpMessageConverter 的 `read` 方法中,有一個 HttpInputMessage 的形參,它正是 Spring MVC 的訊息轉換器所作用的受體**請求訊息**的內部抽象,訊息轉換器從**請求訊息**中按照規則提取訊息,轉換為方法形參中宣告的物件。
### HttpOutputMessage 介面
`org.springframework.http.HttpOutputMessage`:對一次 Http 響應報文的抽象
```java
public interface HttpOutputMessage extends HttpMessage {
/**
* Return the body of the message as an output stream.
* @return the output stream body (never {@code null})
* @throws IOException in case of I/O errors
*/
OutputStream getBody() throws IOException;
}
```
在 HttpMessageConverter 的 `write` 方法中,有一個 HttpOutputMessage 的形參,它正是 Spring MVC 的訊息轉換器所作用的受體**響應訊息**的內部抽象,訊息轉換器將**響應訊息**按照一定的規則寫到響應報文中
### HttpMessageConverter 介面
`org.springframework.http.converter.HttpMessageConverter`:對訊息轉換器最高層次的介面抽象,描述了一個訊息轉換器的一般特徵
```java
public interface HttpMessageConverter {
/** 能否讀取 */
boolean canRead(Class> clazz, @Nullable MediaType mediaType);
/** 能夠寫入 */
boolean canWrite(Class> clazz, @Nullable MediaType mediaType);
/** 獲取支援的 MediaType */
List getSupportedMediaTypes();
/** 讀取請求體 */
T read(Class extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
/** 設定響應體 */
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
```
### 類圖
HttpMessageConverter 介面體系的結構如下:
上圖只列出了**部分**實現類,因為在 Spring MVC 和 Sping Boot 中預設的 HttpMessageConverter 實現類差不多就上面幾個,我們來看看有哪些實現類:
- Spring MVC
- Spring Boot
### 示例
```java
@RestController
public class UserController {
@Autowired
UserService userService;
/** 這裡的 @RequestBody 標註入參僅示例,是為了後續的分析 */
@GetMapping(value = "/query")
public Result
- > queryUser(@RequestBody String name) {
try {
return Result.success().data(userService.queryUserByName(name));
} catch (Exception e) {
return Result.fail(e);
}
}
}
```
當你發起一個 HTTP 請求 `GET /query`,因為你添加了@RequestBody 註解,所以是從請求體讀請求報文的,可以設定請求體中的資料為 `ming`,也就是我想要拿到名字為 `ming` 的所有使用者的資訊
Spring MVC 處理該請求時,會先進入到 RequestResponseBodyMethodProcessor 這個類,獲取方法入參,其中會通過 `StringHttpMessageConverter` 從請求體中讀取 `ming` 資料,作為呼叫方法的入參。
在 Spring MVC 獲取到方法的返回結果後,又會進入到 RequestResponseBodyMethodProcessor 這個類,往響應體中寫資料,其中會通過 `MappingJackson2HttpMessageConverter` 將 `List