Spring--視圖內(nèi)容協(xié)商(三)
本文是學(xué)習(xí)了小馬哥在慕課網(wǎng)的課程的《Spring Boot 2.0深度實踐之核心技術(shù)篇》的內(nèi)容結(jié)合自己的需要和理解做的筆記。
上兩篇文章,簡單介紹了一下Spring的視圖內(nèi)容協(xié)商。接下來我們針對 REST 內(nèi)容協(xié)商做一下介紹。
沒想到截圖效果怎么不好,實在是失落。
大綱
- 理解REST請求媒體類型
- REST內(nèi)容協(xié)商流程
- REST內(nèi)容協(xié)商源碼分析
理解REST請求媒體類型
理解解析請求的媒體類型
在我們使用Spring開發(fā)的時候,相信 @RequestMapping 這個注解再也熟悉不過了,相信使用Restful API 接口形式開發(fā)的小伙伴們都避免不了設(shè)置API的媒體類型 比如 Accept和 Content-Type ,在前一篇我們也說過 Spring 通過 ContentNegotiationManager 的 ContentNegotiationStrategy 解析請求中的媒體類型。
以 HeaderContentNegotiationStrategy 為例
- 如果解析成功,則返回合法的
MediaType集合。 - 否則,返回
MediaType.ALL默認(rèn)的媒體類型,也就是*/*
在這里我們可以看一下HeaderContentNegotiationStrategy 源碼,具體解釋已經(jīng)在注釋中給出
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {
//獲取 Accept的 媒體類型的字符串?dāng)?shù)組
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
//如果為空 則返回 全部類型 也就是 */*
if (headerValueArray == null) {
return MEDIA_TYPE_ALL_LIST;
}
List<String> headerValues = Arrays.asList(headerValueArray);
try {
//轉(zhuǎn)換為Spring 內(nèi)置媒體類型集合
List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
//最佳媒體類型排序
MediaType.sortBySpecificityAndQuality(mediaTypes);
return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
"Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
}
}
當(dāng)然 在 ContentNegotiationManager 中也會有判斷
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
//循環(huán)遍歷協(xié)商處理策略
for (ContentNegotiationStrategy strategy : this.strategies) {
//獲取媒體類型 比如 HeaderContentNegotiationStrategy
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
//如果媒體類型是 默認(rèn)的所有 即 */* 則跳過
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
//如果都不滿足 則最后返回 默認(rèn)所有 即*/*
return MEDIA_TYPE_ALL_LIST;
}
對比上面的源碼我們可以看到 Spring 做了很多層校驗 來判斷媒體類型是否為空。
理解可生成的媒體類型
使用@RequestMapping.produces() 屬性 來指定MediaType 類型集合 影響 瀏覽器響應(yīng)頭 Content-Type 媒體類型映射。
- 如果
@RequestMapping.produces()存在,返回指定 MediaType 列表。 - 否則,返回已注冊的
HttpMessageConverter列表中支持的 MediaType 列表。 - 如果該列表與請求的媒體類型兼容,執(zhí)行第一個兼容
HttpMessageConverter的實現(xiàn),默認(rèn)
@RequestMapping#produces內(nèi)容到響應(yīng)頭 Content-Type
否則,拋出HttpMediaTypeNotAcceptableException, HTTP Status Code : 415
這段源碼會在稍后的源碼分析中做詳解。
理解可消費的媒體類型
使用@RequestMapping#consumes 屬性,來設(shè)置兼容的MediaType 類型集合 過濾 請求頭 Content-Type 媒體類型映射。
- 如果請求頭 Content-Type 媒體類型兼容
@RequestMapping.consumes()屬性,執(zhí)行該HandlerMethod - 否則
HandlerMethod不會被調(diào)用。
這段源碼會在稍后的源碼分析中做詳解。
REST內(nèi)容協(xié)商流程
在講述REST協(xié)商流程之前,我們先來了解一下 對于REST內(nèi)容協(xié)商非常關(guān)鍵的兩個解析器
- 處理方法參數(shù)解析器(
HandlerMethodArgumentResolver)- 用于 HTTP 請求中解析
HandlerMethod參數(shù)內(nèi)容
- 用于 HTTP 請求中解析
- 處理方法返回值解析器(
HandlerMethodReturnValueHandler)- 用于
HandlerMethod返回值解析為 HTTP 響應(yīng)內(nèi)容
- 用于
流程圖
對于協(xié)商流程,在這里貼一張圖,流程大體就可以理解了,在結(jié)合下面的源碼分析,相信很快就可以明白Spring的視圖協(xié)商流程。
針對 上述第10步 轉(zhuǎn)化HTTP消息 的詳細(xì)流程圖
調(diào)試前的代碼準(zhǔn)備
在這里需要增加一個簡單的User對象以及對應(yīng)請求的Controller
User.java
/**
* 用戶對象
*/
public class User {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
UserRestController.java
@RestController
public class UserRestController {
//最終相應(yīng)回瀏覽器的Content-Type 是 produces 的內(nèi)容
@PostMapping(value = "/user",
consumes = "application/json;charset=UTF-8",
produces = "application/json;charset=GBK")
public User user(@RequestBody User user) {
return user;
}
}
添加完這兩個類之后,讓我們啟動一下Spring-boot項目。
我們可以對比上面的總體流程圖來對源碼進(jìn)行解讀。我們使用PostMan來發(fā)送一個簡單的請求。
首先我們先打開DispatcherServlet#doDispatch 方法。我們來關(guān)注下面兩個方法

首先我們先來看一下 步驟 2和3的對應(yīng)方法

接下來讓我們進(jìn)入到 步驟 4的對應(yīng)方法中

接下來就是我們的重頭戲就是調(diào)用HandlerMethod,在這個階段就包括了剩下的步驟,也就是步驟5~10。

我們一步一步的往方法里進(jìn)。

我們接著進(jìn)入到 RequestMappingHanlderAdapter 中的 handleInternal 方法。



我們接著進(jìn)入到 invokeAndHandle 方法


我們詳細(xì)看一下如何解析方法參數(shù)的。

這里我們重點看一下 argumentResolvers.supportsParameter(parameter) 這段代碼 主要是判斷 參數(shù)處理器是否支持解析傳入的參數(shù)。

判斷完某個解析器(RequestResponseBodyMethodProcessor)是否支持解析,接下來就是具體解析的操作了。

進(jìn)入到 resolveArgument方法


在這里我們已經(jīng)獲取到了 入?yún)⒌闹?,我們回到最初調(diào)用的方法然后反射調(diào)用方法。



至此 我們步驟8之前的已經(jīng)講解完了。下面我們講解一下返回值解析。我們重新回到org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle 方法中。

進(jìn)入handleReturnValue 方法。

我們來看一下 selectHandler() 這個方法

選擇好了返回值解析器 (RequestResponseBodyMethodProcesser),下面我們就看看如何解析返回值的。
細(xì)心的小伙伴可以發(fā)現(xiàn) 這個解析器即是 方法參數(shù)解析器 又是 返回值解析器。這里就不多做解釋了 這是因為他即繼承AbstractMessageConverterMethodArgumentResolver 抽象類又實現(xiàn)了 HandlerMethodReturnValueHandler 接口。我們再進(jìn)入到里面一層的handleReturnValue


接下來的內(nèi)容就是詳細(xì)流程圖中的內(nèi)容了我們從第7步開始看,我們通過媒體類型來匹配HttpMessageConverter。


選擇好轉(zhuǎn)換器之后我們就可以進(jìn)行轉(zhuǎn)換了,我們看一下MappingJackson2HttpMessageConverter 是如何轉(zhuǎn)換然后返回相應(yīng)的。




最后,在PostMan中顯示返回結(jié)果


通過響應(yīng)頭 我們也可以看到 @RequestMapping.produces() 的作用。
總結(jié)
雖然協(xié)商邏輯以及流程比較繁瑣,但是在我們使用Spring的時候,這些功能給了我們很大的便利,至于 最后Jackson是如何序列化的,這里就不詳細(xì)說明了。不屬于本次內(nèi)容的范疇。 別的不多說,繼續(xù)努力吧。