上一章簡單的說了下spring boot接收請(qǐng)求數(shù)據(jù),并且用解析器解析成想要的類型。包括一下復(fù)雜的包裝類。然后這章主要是說返回?cái)?shù)據(jù)的封裝和類型轉(zhuǎn)換,已經(jīng)http內(nèi)容協(xié)商的原理。
@ResponseBody
說到數(shù)據(jù)返回,這個(gè)注解幾乎必不可少。無論是之前spring mvc的時(shí)候,這個(gè)注解用來返回json數(shù)據(jù),還是現(xiàn)在的spring boot中@ResponseBody和@Controller合二為一的@RestController,反正都用到了這個(gè)注解。

這個(gè)注解的意思就是可以將返回值以json字符串的形式返回。并且如果在類上標(biāo)注,那么作用域是這個(gè)類中所有的方法。所以不要覺得現(xiàn)在spring boot沒有用這個(gè)注解了,大多數(shù)時(shí)候這個(gè)注解已經(jīng)是必要且必須的了。咱們先探究一下這個(gè)注解的原理。
咱們根據(jù)上文的請(qǐng)求參數(shù)的解析,其實(shí)就能猜到這個(gè)響應(yīng)參數(shù)的解析。大同小異,也是spring boot配置好了一堆解析器,然后根據(jù)不同的類型去轉(zhuǎn)化。而這個(gè)@ResponseBody注解就是其中一個(gè)解析器,其作用就是將數(shù)據(jù)轉(zhuǎn)化成json。其大概的過程如下:
- 返回值處理器判斷是否支持這種類型返回值 supportsReturnType
- 返回值處理器調(diào)用 handleReturnValue 進(jìn)行處理
- RequestResponseBodyMethodProcessor 可以處理返回值標(biāo)了@ResponseBody 注解的。
- 利用 MessageConverters 進(jìn)行處理 將數(shù)據(jù)寫為json
- 內(nèi)容協(xié)商(瀏覽器默認(rèn)會(huì)以請(qǐng)求頭的方式告訴服務(wù)器他能接受什么樣的內(nèi)容類型)
- 服務(wù)器最終根據(jù)自己自身的能力,決定服務(wù)器能生產(chǎn)出什么樣內(nèi)容類型的數(shù)據(jù),
- SpringMVC會(huì)挨個(gè)遍歷所有容器底層的 HttpMessageConverter ,看誰能處理?
- 利用MappingJackson2HttpMessageConverter將對(duì)象轉(zhuǎn)為json再寫出去。
下面我們?nèi)タ纯丛创a:
首先我上面就說了這個(gè)是類似于請(qǐng)求處理器一樣,是有一堆的,下面我們看看這一堆:


可選項(xiàng)默認(rèn)有15個(gè),其實(shí)這里可以看名字就大概知道哪些肯定不可能了,咱們這里我直接劇透用的第12個(gè):RequestResponseBodyMethodProcessor
選擇處理器后繼續(xù)走邏輯(如果沒有合適的處理器會(huì)報(bào)錯(cuò),上面截圖中已經(jīng)說了)。進(jìn)入處理的方法:

走到這步了:

其實(shí)邏輯也不是很復(fù)雜:

單純的轉(zhuǎn)成字符串邏輯其實(shí)不復(fù)雜,但是復(fù)雜的這里涉及到了內(nèi)容協(xié)商(大概意思就是請(qǐng)求的時(shí)候就要告訴服務(wù)器要什么樣的數(shù)據(jù),所以返回的時(shí)候也要與之對(duì)應(yīng)。)正常不設(shè)置的情況下是都要的,比如我這個(gè)demo中的:

這個(gè)最后一個(gè)/的意思就是啥都行,也就是說這個(gè)返回啥都可以(默認(rèn)一般就是這樣)。
所以說咱們這個(gè)返回json是可以的,以下又是一系列操作,幾乎沒啥了,就是在這里利用轉(zhuǎn)換器轉(zhuǎn)換成想要的數(shù)據(jù)返回的。
這里數(shù)據(jù)類型是要看雙方能不能匹配上,但是上面的實(shí)現(xiàn)還使用了一個(gè)重要的類,就是這個(gè):HttpMessageConverter
這個(gè)類的作用簡而言之一句話:將Class類型的對(duì)象轉(zhuǎn)成MediaType類型的http支持?jǐn)?shù)據(jù)。

比如返回值是int和返回值是布爾型還有字符串型,肯定是有不一樣的轉(zhuǎn)化方式的,所以這又是一堆轉(zhuǎn)化的類。這個(gè)就不咋見名知意的,除了第一個(gè)是字節(jié)碼類型,還有字符串類型,剩下看不好,我就不一一點(diǎn)進(jìn)去了,反正每一個(gè)功能肯定不一樣,再直接劇透這里用下標(biāo)為7的那個(gè)處理器:

代碼邏輯上,只要這個(gè)類能被序列化,就可以使用這個(gè)處理器。所以繼續(xù)往下走。
最終 MappingJackson2HttpMessageConverter 把對(duì)象轉(zhuǎn)為JSON(利用底層的jackson的objectMapper轉(zhuǎn)換的)
至此這個(gè)@ResponseBody標(biāo)注的方法的返回值就返回結(jié)束了。
(ps:如果這里返回的類型不同會(huì)使用不同的處理器,我上面只說了返回json數(shù)據(jù)的。比如返回資源的話會(huì)用資源處理器。)
內(nèi)容協(xié)商
其實(shí)這個(gè)上文簡單的介紹過了,在發(fā)送請(qǐng)求的時(shí)候,我們可以設(shè)置想要返回?cái)?shù)據(jù)類型。比如我們之前說過的報(bào)錯(cuò),在接口請(qǐng)求中是json,在頁面請(qǐng)求種是xml。這個(gè)就是內(nèi)容協(xié)商的作用。
這個(gè)想要接收的數(shù)據(jù)類型是在消息頭中設(shè)置的(默認(rèn)使用基于請(qǐng)求頭的策略)。

前面是可以接收的類型,后面的小數(shù)是權(quán)重。比如圖中前面的幾個(gè)json/xml都是0.9.而后面imgs和/等都是0.8.
當(dāng)然了這塊我們也可以手動(dòng)去設(shè)置,只需要改變請(qǐng)求頭中Accept字段。Http協(xié)議中規(guī)定的,告訴服務(wù)器本客戶端可以接收的數(shù)據(jù)類型。
這里在我們上面走代碼的時(shí)候跑過了,我們直接總結(jié)以下:
- 獲取可以處理當(dāng)前返回值的所有返回值類型(結(jié)果集是list)。
- 獲取服務(wù)器所要的所有返回值類型(結(jié)果集是list)。
-
雙層for循環(huán)找到兩者匹配的。如下圖
雙層for循環(huán)找到最終使用類型
這里有一點(diǎn)比較好的是客戶端類型優(yōu)先。而且可能我們會(huì)發(fā)現(xiàn)有多種處理辦法都可以實(shí)現(xiàn)這個(gè)功能,但是這里是當(dāng)遇到一個(gè)可以處理的就break。
使用第一個(gè)結(jié)果,后面不管了
其實(shí)這個(gè)設(shè)計(jì)的挺巧妙,也挺很簡單的。就是要的和能給的,選擇最佳匹配的那個(gè)。只要知道原理就很容易理解啦。而且一般瀏覽器按照權(quán)重,都會(huì)優(yōu)先html等,所以error頁面會(huì)出現(xiàn)網(wǎng)頁和接口測試工具出現(xiàn)的東西不一樣。
上面說了好多都是基于請(qǐng)求頭設(shè)置的接收方法,但是有一個(gè)問題!瀏覽器的請(qǐng)求頭輕易不好改,所以這里還有一種策略:基于format參數(shù)的內(nèi)容協(xié)商。
想要實(shí)現(xiàn)這個(gè)只要一個(gè)配置:
spring:
contentnegotiation:
favor-parameter: true #開啟請(qǐng)求參數(shù)內(nèi)容協(xié)商模式
這個(gè)配置就是開啟請(qǐng)求參數(shù)內(nèi)容協(xié)商模式。并且參數(shù)的內(nèi)容協(xié)商策略是優(yōu)先于消息頭的內(nèi)容協(xié)商策略。
大致流程開啟請(qǐng)求參數(shù)內(nèi)容協(xié)商模式以后,先獲取請(qǐng)求參數(shù)中的值進(jìn)行內(nèi)容協(xié)商返回給客戶端json即可。當(dāng)然了默認(rèn)這個(gè)參數(shù)格式只支持xml和json。就是這兩個(gè)字面量。別的都不好使。

如果參數(shù)中是/,那么還會(huì)繼續(xù)去匹配消息頭中的。否則就不管消息頭中的了。
自定義返回值轉(zhuǎn)換類型
其實(shí)這個(gè)和解析器差不多,一般我們都可以自定義。而我們只要找到之前spring boot自帶的一堆解析器是從哪里來的,就比較容易把自己寫好的加進(jìn)去了。這里我直接說spring boot加載默認(rèn)配置的地方:

其實(shí)名字也比較容易理解,然后我們進(jìn)去看看這個(gè)類,這個(gè)類幾乎是webmvc中好多默認(rèn)提供的解析器的匯集了。包括請(qǐng)求解析器,參數(shù)解析器,和現(xiàn)在我們要找的返回值解析器等。我們直接說返回值解析器這塊:

當(dāng)然了這個(gè)類也有一些小細(xì)節(jié):比如是存在jacksonXml依賴,才會(huì)添加xml的解析器。這里就順便提一下,繼續(xù)說自己定制東西在spring boot中統(tǒng)一的方式:給容器中添加WebMvcConfigurer里面放入自己的配置。如下操作:
- 寫一個(gè)自己的解析器(這里其實(shí)代碼比較簡單,但是因?yàn)橛泻芏喾椒?,所以我直接貼代碼以便復(fù)制粘貼)
/**
* 自定義的converter
* @author 11511
*
*/
public class MyMessageConverter implements HttpMessageConverter<SysUser>{
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// TODO Auto-generated method stub
return false;
}
//可以讀什么類型
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// TODO Auto-generated method stub
return clazz.isAssignableFrom(SysUser.class);
}
//服務(wù)器統(tǒng)計(jì)可以處理的內(nèi)容協(xié)商的類型
@Override
public List<MediaType> getSupportedMediaTypes() {
// TODO Auto-generated method stub
return MediaType.parseMediaTypes("application/x");
}
@Override
public SysUser read(Class<? extends SysUser> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// TODO Auto-generated method stub
return null;
}
@Override
public void write(SysUser t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
//接收參數(shù)后,返回如下數(shù)據(jù),這樣能明確測出是不是走了這個(gè)解析器
String data = t.getName()+",自定義格式啟動(dòng)成功!";
outputMessage.getBody().write(data.getBytes());
}
}
這個(gè)類的方法都挺明顯的,畢竟這個(gè)converter可讀可寫,咱們這是為了測試寫,所以read方法都不懂,注意我這里能解析的消息頭我設(shè)置的application/x。這個(gè)是我自己編的,正常不存在這個(gè)格式。剩下就沒啥了。
-
自己寫webMvcConfigurer并重寫這個(gè)方法:
WebMvcConfigurer可重寫方法
把我們自己的解析器添加進(jìn)去
然后這一塊的東西最好的一點(diǎn)就是見名知意。我們是要往httpMessageConverter中添加自定義的東西,所以重寫這個(gè)方法,其實(shí)這里可以舉一反三,如果添加別的也是差不多的方式。
這兩步做好以后,我們可以去測試下這個(gè)轉(zhuǎn)換器是不是生效了(因?yàn)檫@個(gè)格式瀏覽器不好設(shè)置,所以我用postman測試)


再放個(gè)對(duì)比圖:一樣的接口一樣的參數(shù),就是內(nèi)容協(xié)商中的類型不同就返回了不同的結(jié)果。所以說這塊的邏輯沒問題,和我們學(xué)到了和理解的都一樣。
而且事實(shí)證明往spring 中添加一些自定義的東西只要知道流程還是很簡單的。畢竟它的擴(kuò)展性是很好的,咱們再說一個(gè)小問題:
上文已經(jīng)說了正常是參數(shù)內(nèi)容協(xié)商雖然優(yōu)先級(jí)高,但是只支持xml和json。那么想要實(shí)現(xiàn)自定義的怎么辦?
簡單來說,去用webMvcConfigurer添加自定義配置。這個(gè)思路絕對(duì)沒問題。然后我們要去webMvcConfigurer中找去重寫哪個(gè)方法:


咳咳,反正英語不好的我是用的百度翻譯。。然后名字這么明顯了所以重寫這個(gè)方法就得了:
下面是自定的方法(ps:這里為了復(fù)制粘貼方法所以貼代碼):
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyMessageConverter());
WebMvcConfigurer.super.configureMessageConverters(converters);
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String,MediaType> map = new HashMap<String,MediaType>();
map.put("lsj", MediaType.parseMediaType("application/x"));
map.put("json", MediaType.APPLICATION_JSON);
List<ContentNegotiationStrategy> list = new ArrayList<ContentNegotiationStrategy>();
list.add(new ParameterContentNegotiationStrategy(map));
configurer.strategies(list);
WebMvcConfigurer.super.configureContentNegotiation(configurer);
}
};
}
這里因?yàn)楦鞣N數(shù)據(jù)類型的不互通,而且比如參數(shù)的list,參數(shù)是map之類的,所以要一層一層找代碼。但是過程還是很簡單也很有意思,建議自己寫出來,也算是熟悉的過程。
配置完成以后我們訪問一下:

雖然亂碼了但是是因?yàn)闉g覽器的編碼問題,反正能看出來是名字+一段話,所以說明測試成功!
至此關(guān)于內(nèi)容協(xié)商的東西簡單的就說完了,其實(shí)我感覺這章最好的內(nèi)容是如何自定義配置spring的一些配置,當(dāng)然了要注意當(dāng)自定義配置以后有可能會(huì)覆蓋spring原本的配置,這里要慎重使用。
本篇文章就記錄到這里,如果稍微幫到你記得點(diǎn)個(gè)喜歡點(diǎn)個(gè)關(guān)注,也祝大家工作順順利利!



