十個光頭九個富,最後一個會砍樹
不知你在使用Spring Boot
時是否對這樣一個現象"詫異"過:同一個
接口(同一個URL)在接口報錯狀況下,若你用rest訪問,它返回給你的是一個json
串;但若你用瀏覽器訪問,它返回給你的是一段html
。恰以下面例子(Spring Boot
環境~):html
@RestController @RequestMapping public class HelloController { @GetMapping("/test/error") public Object testError() { System.out.println(1 / 0); // 強制拋出異常 return "hello world"; } }
使用瀏覽器訪問:http://localhost:8080/test/error
使用Postman訪問:
同根不一樣命有木有。RESTful
服務中很重要的一個特性是:同一資源能夠有多種表述,這就是咱們今天文章的主題:內容協商(ContentNegotiation
)。前端
雖然本文主要是想說Spring MVC
中的內容協商機制,可是在此以前是頗有必要先了解HTTP
的內容協商是怎麼回事(Spring MVC
實現了它而且擴展了它更爲強大~)。java
一個URL資源
服務端能夠以多種形式進行響應:即MIME(MediaType
)媒體類型。但對於某一個客戶端(瀏覽器、APP、Excel導出...)來講它只須要一種。so這樣客戶端和服務端就得有一種機制來保證這個事情,這種機制就是內容協商機制。程序員
http
的內容協商方式大體有兩種:web
(經常使用)客戶端發請求時就指明須要的MIME
們(好比Http
頭部的:Accept
),服務端根據客戶端指定的要求返回合適的形式,而且在響應頭中作出說明(如:Content-Type
)json
1. 若客戶端要求的MIME類型服務端提供不了,那就406錯誤吧~
==請求頭==Accept
:告訴服務端須要的MIME(通常是多個,好比text/plain
,application/json
等。/表示能夠是任何MIME資源)Accept-Language
:告訴服務端須要的語言(在中國默認是中文嘛,但瀏覽器通常均可以選擇N多種語言,可是是否支持要看服務器是否能夠協商)Accept-Charset
:告訴服務端須要的字符集Accept-Encoding
:告訴服務端須要的壓縮方式(gzip,deflate,br)
==響應頭==Content-Type
:告訴客戶端響應的媒體類型(如application/json
、text/html
等)Content-Language
:告訴客戶端響應的語言Content-Charset
:告訴客戶端響應的字符集Content-Encoding
:告訴客戶端響應的壓縮方式(gzip)segmentfault
Accept
與Content-Type
的區別有不少文章粗暴的解釋:Accept
屬於請求頭,Content-Type
屬於響應頭,其實這是不許確的。
在先後端分離開發成爲主流的今天,你應該不乏見到前端的request請求上大都有Content-Type:application/json;charset=utf-8
這個請求頭,所以可見Content-Type
並不只僅是響應頭。後端
HTTP協議規範的格式以下四部分:瀏覽器
Content-Type
指請求消息體的數據格式,由於請求和響應中均可以有消息體,因此它便可用在請求頭,亦可用在響應頭。
關於更多Http中的Content-Type
的內容,我推薦參見此文章:Http請求中的Content-Type服務器
Spring MVC
實現了HTTP
內容協商的同時,又進行了擴展。它支持4種協商方式:
HTTP
頭Accept
說明:如下示例基於Spring進行演示,而非
Spring Boot
Accept
@RestController @RequestMapping public class HelloController { @ResponseBody @GetMapping("/test/{id}") public Person test(@PathVariable(required = false) String id) { System.out.println("id的值爲:" + id); Person person = new Person(); person.setName("fsx"); person.setAge(18); return person; } }
若是默認就這樣,無論瀏覽器訪問仍是Postman訪問,獲得的都是json串。
但若你僅僅只需在pom
加入以下兩個包:
<!-- 此處須要導入databind包便可, jackson-annotations、jackson-core都不須要顯示本身的導入了--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency> <!-- jackson默認只會支持的json。若要xml的支持,須要額外導入以下包 --> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.9.8</version> </dependency>
再用瀏覽器/Postman訪問,獲得結果就是xml了,形如這樣:
有的文章說:瀏覽器是xml,postman是json。本人親試:都是xml。
但若咱們postman
手動指定這個頭:Accept:application/json
,返回就和瀏覽器有差別了(若不手動指定,Accept
默認值是*/*
):
而且咱們能夠看到response
的頭信息對好比下:
手動指定了Accept:application/json
:
木有指定Accept(默認*/*
):
Chrome
瀏覽器請求默認發出的Accept
是:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
。
因爲我例子使用的是@ResponseBody
,所以它不會返回一個view:交給消息轉換器處理,所以這就和MediaType
以及權重有關了。
消息最終都會交給AbstractMessageConverterMethodProcessor.writeWithMessageConverters()
方法:
// @since 3.1 AbstractMessageConverterMethodProcessor: protected <T> void writeWithMessageConverters( ... ) { Object body; Class<?> valueType; Type targetType; ... HttpServletRequest request = inputMessage.getServletRequest(); // 這裏交給contentNegotiationManager.resolveMediaTypes() 找出客戶端能夠接受的MediaType們~~~ // 此處是已經排序好的(根據Q值等等) List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); // 這是服務端它所能提供出的MediaType們 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); // 協商。 通過必定的排序、匹配 最終匹配出一個合適的MediaType ... // 把待使用的們再次排序, MediaType.sortBySpecificityAndQuality(mediaTypesToUse); // 最終找出一個最合適的、最終使用的:selectedMediaType for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } }
acceptableTypes
是客戶端經過Accept
告知的。producibleTypes
表明着服務端所能提供的類型們。參考這個getProducibleMediaTypes()
方法:
AbstractMessageConverterMethodProcessor: protected List<MediaType> getProducibleMediaTypes( ... ) { // 它設值的地方惟一在於:@RequestMapping.producers屬性 // 大多數狀況下:咱們通常都不會給此屬性賦值吧~~~ Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } // 大多數狀況下:都會走進這個邏輯 --> 從消息轉換器中匹配一個合適的出來 else if (!this.allSupportedMediaTypes.isEmpty()) { List<MediaType> result = new ArrayList<>(); // 從全部的消息轉換器中 匹配出一個/多個List<MediaType> result出來 // 這就表明着:我服務端所能支持的全部的List<MediaType>們了 for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter && targetType != null) { if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } else if (converter.canWrite(valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } else { return Collections.singletonList(MediaType.ALL); } }
能夠看到服務端最終可以提供哪些MediaType
,來源於消息轉換器HttpMessageConverter
對類型的支持。
本例的現象:起初返回的是json串,僅僅只須要導入jackson-dataformat-xml
後就返回xml
了。緣由是由於加入MappingJackson2XmlHttpMessageConverter
都有這個判斷:
private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); if (jackson2XmlPresent) { addPartConverter(new MappingJackson2XmlHttpMessageConverter()); }
因此默認狀況下Spring MVC
並不支持application/xml
這種媒體格式,因此若不導包協商出來的結果是:application/json
。
默認狀況下優先級是xml高於json。固然通常都木有xml包,因此才輪到json的。
另外還須要注意一點:有的小夥伴說經過在請求頭裏指定Content-Type:application/json
來達到效果。如今你應該知道,這樣作顯然是沒用的(至於爲什麼沒用,但願讀者作到了心知肚明),只能使用Accept
這個頭來指定~~~
第一種協商方式是Spring MVC
徹底基於HTTP Accept
首部的方式了。該種方式Spring MVC
默認支持且默認已開啓。
優缺點:
基於上面例子:若我訪問/test/1.xml
返回的是xml,若訪問/test/1.json
返回的是json;完美~
這種方式使用起來很是的便捷,而且還不依賴於瀏覽器。但我總結了以下幾點使時的注意事項:
test.json / test.xml
就404~@PathVariable
的參數類型只能使用通用類型(String/Object
),由於接收過來的value值就是1.json/1.xml
,因此若用Integer
接收將報錯類型轉換錯誤~
1. 小技巧:我我的建議是這部分不接收(這部分不使用`@PathVariable`接收),拿出來**只爲內容協商使用**
優缺點:
這種協商方式Spring MVC
支持,但默認是關閉的,須要顯示的打開:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // 支持請求參數協商 configurer.favorParameter(true); } }
請求URL:/test/1?format=xml
返回xml;/test/1?format=json
返回json。一樣的我總結以下幾點注意事項:
低於
擴展名(所以你測試時若想它生效,請去掉url的後綴)優缺點:
它就是利用@RequestMapping
註解屬性produces
(可能你平時也在用,但並不知道緣由):
@ResponseBody @GetMapping(value = {"/test/{id}", "/test"}, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Person test() { ... }
訪問:/test/1
返回的就是json;即便你已經導入了jackson的xml包,返回的依舊仍是json。
它也有它很很很重要的一個注意事項:produces
指定的MediaType
類型不能和後綴、請求參數、Accept衝突。例如本利這裏指定了json
格式,若是你這麼訪問/test/1.xml
,或者format=xml
,或者Accept
不是application/json或者*/*
將沒法完成內容協商:http狀態碼爲406,報錯以下:produces
使用當然也比較簡單,針對上面報錯406的緣由,我簡單解釋以下。
一、先解析請求的媒體類型:1.xml
解析出來的MediaType
是application/xml
二、拿着這個MediaType
(固然還有URL、請求Method等全部)去匹配HandlerMethod
的時候會發現producers
匹配不上
三、匹配不上就交給RequestMappingInfoHandlerMapping.handleNoMatch()
處理:
RequestMappingInfoHandlerMapping: @Override protected HandlerMethod handleNoMatch(...) { if (helper.hasConsumesMismatch()) { ... throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes)); } // 拋出異常:HttpMediaTypeNotAcceptableException if (helper.hasProducesMismatch()) { Set<MediaType> mediaTypes = helper.getProducibleMediaTypes(); throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes)); } }
四、拋出異常後最終交給DispatcherServlet.processHandlerException()
去處理這個異常,轉換到Http
狀態碼
會調用全部的handlerExceptionResolvers
來處理這個異常,本處會被DefaultHandlerExceptionResolver
最終處理。最終處理代碼以下(406狀態碼):
protected ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE); return new ModelAndView(); }
Spring MVC
默認註冊的異常處理器是以下3個:
![]()
有了關於Accept
的原理描述,理解它就很是簡單了。由於指定了produces
屬性,因此getProducibleMediaTypes()
方法在拿服務端支持的媒體類型時:
protected List<MediaType> getProducibleMediaTypes( ... ){ Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } ... }
由於設置了producers
,因此代碼第一句就能拿到值了(後面的協商機制徹底同上)。
備註:若produces屬性你要指定的很是多,建議可使用
!xxx
語法,它是支持這種語法(排除語法)的~
優缺點:
HandlerMethod
處理器缺失靈活性再回到開頭的Spring Boot
爲什麼對異常消息,瀏覽器和postman的展現不同。這就是Spring Boot
默認的對異常處理方式:它使用的就是基於 固定類型(produces)實現的內容協商。
Spirng Boot
出現異常信息時候,會默認訪問/error
,它的處理類是:BasicErrorController
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { ... // 處理類瀏覽器 @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { ... return (modelAndView != null ? modelAndView : new ModelAndView("error", model)); } // 處理restful/json方式 @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); } ... }
有了上面的解釋,對這塊代碼的理解應該就沒有盲點了~
內容協商在RESTful
流行的今天仍是很是重要的一塊內容,它對於提高用戶體驗,提高效率和下降維護成本都有不可忽視的做用,注意它三的優先級爲:後綴 > 請求參數 > HTTP首部Accept
通常狀況下,咱們爲了通用都會使用基於Http的內容協商(Accept),但在實際應用中其實不多用它,由於不一樣的瀏覽器可能致使不一樣的行爲(好比Chrome
和Firefox
就很不同),因此爲了保證「穩定性」通常都選擇使用方案二或方案三(好比Spring的官方doc)。
【小家Spring】Spring MVC容器的web九大組件之---HandlerMapping源碼詳解(二)---RequestMappingHandlerMapping系列
ContentNegotiation內容協商機制(一)---Spring MVC內置支持的4種內容協商方式【享學Spring MVC】
ContentNegotiation內容協商機制(二)---Spring MVC內容協商實現原理及自定義配置【享學Spring MVC】
ContentNegotiation內容協商機制(三)---在視圖View上的應用:ContentNegotiatingViewResolver深度解析【享學Spring MVC】
==The last:若是以爲本文對你有幫助,不妨點個讚唄。固然分享到你的朋友圈讓更多小夥伴看到也是被做者本人許可的~
==
**若對技術內容感興趣能夠加入wx羣交流:Java高工、架構師3羣
。
若羣二維碼失效,請加wx號:fsx641385712
(或者掃描下方wx二維碼)。而且備註:"java入羣"
字樣,會手動邀請入羣**==若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入羣一塊兒飛==