在絕對力量面前,一切技巧都是浮雲
上文 介紹了Http內容協商的一些概念,以及Spring MVC
內置的4種協商方式使用介紹。本文主要針對Spring MVC
內容協商方式:從步驟、原理層面理解,最後達到經過本身來擴展協商方式效果。html
首先確定須要介紹的,那必然就是Spring MVC
的默認支持的四大協商策略的原理分析嘍:java
該接口就是Spring MVC
實現內容協商的策略接口:web
// A strategy for resolving the requested media types for a request. // @since 3.2 @FunctionalInterface public interface ContentNegotiationStrategy { // @since 5.0.5 List<MediaType> MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL); // 將給定的請求解析爲媒體類型列表 // 返回的 List 首先按照 specificity 參數排序,其次按照 quality 參數排序 // 若是請求的媒體類型不能被解析則拋出 HttpMediaTypeNotAcceptableException 異常 List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException; }
說白了,這個策略接口就是想知道客戶端的請求須要什麼類型(MediaType
)的數據List
。從 上文 咱們知道Spring MVC
它支持了4種不一樣的協商機制,它都和此策略接口相關的。
它的繼承樹:
從實現類的名字上就能看出它和上文提到的4種方式剛好是一一對應着的(ContentNegotiationManager
除外)。spring
Spring MVC
默認加載兩個該策略接口的實現類:
ServletPathExtensionContentNegotiationStrategy
-->根據文件擴展名(支持RESTful)。
HeaderContentNegotiationStrategy
-->根據HTTP Header
裏的Accept
字段(支持Http)。
Accept Header
解析:它根據請求頭Accept
來協商。json
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy { @Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { // 個人Chrome瀏覽器值是:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3] // postman的值是:[*/*] String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT); if (headerValueArray == null) { return MEDIA_TYPE_ALL_LIST; } List<String> headerValues = Arrays.asList(headerValueArray); try { List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues); // 排序 MediaType.sortBySpecificityAndQuality(mediaTypes); // 最後Chrome瀏覽器的List以下: // 0 = {MediaType@6205} "text/html" // 1 = {MediaType@6206} "application/xhtml+xml" // 2 = {MediaType@6207} "image/webp" // 3 = {MediaType@6208} "image/apng" // 4 = {MediaType@6209} "application/signed-exchange;v=b3" // 5 = {MediaType@6210} "application/xml;q=0.9" // 6 = {MediaType@6211} "*/*;q=0.8" return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST; } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotAcceptableException("Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage()); } } }
能夠看到,若是沒有傳遞Accept,則默認使用MediaType.ALL 也就是*/*
segmentfault
經過file extension
/query param
來協商的抽象實現類。在瞭解它以前,有必要先插隊先了解MediaTypeFileExtensionResolver
它的做用:瀏覽器
MediaTypeFileExtensionResolver
:MediaType
和路徑擴展名解析策略的接口,例如將 .json
解析成 application/json
或者反向解析安全
// @since 3.2 public interface MediaTypeFileExtensionResolver { // 根據指定的mediaType返回一組文件擴展名 List<String> resolveFileExtensions(MediaType mediaType); // 返回該接口註冊進來的全部的擴展名 List<String> getAllFileExtensions(); }
繼承樹以下:
顯然,本處只須要講解它的直接實現子類MappingMediaTypeFileExtensionResolver
便可:架構
public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExtensionResolver { // key是lowerCaseExtension,value是對應的mediaType private final ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<>(64); // 和上面相反,key是mediaType,value是lowerCaseExtension(顯然用的是多值map) private final MultiValueMap<MediaType, String> fileExtensions = new LinkedMultiValueMap<>(); // 全部的擴展名(List非set哦~) private final List<String> allFileExtensions = new ArrayList<>(); ... public Map<String, MediaType> getMediaTypes() { return this.mediaTypes; } // protected 方法 protected List<MediaType> getAllMediaTypes() { return new ArrayList<>(this.mediaTypes.values()); } // 給extension添加一個對應的mediaType // 採用ConcurrentMap是爲了不出現併發狀況下致使的一致性問題 protected void addMapping(String extension, MediaType mediaType) { MediaType previous = this.mediaTypes.putIfAbsent(extension, mediaType); if (previous == null) { this.fileExtensions.add(mediaType, extension); this.allFileExtensions.add(extension); } } // 接口方法:拿到指定的mediaType對應的擴展名們~ @Override public List<String> resolveFileExtensions(MediaType mediaType) { List<String> fileExtensions = this.fileExtensions.get(mediaType); return (fileExtensions != null ? fileExtensions : Collections.emptyList()); } @Override public List<String> getAllFileExtensions() { return Collections.unmodifiableList(this.allFileExtensions); } // protected 方法:根據擴展名找到一個MediaType~(固然多是找不到的) @Nullable protected MediaType lookupMediaType(String extension) { return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH)); } }
此抽象類維護一些Map以及提供操做的方法,它維護了一個文件擴展名和MediaType
的雙向查找表。擴展名和MediaType
的對應關係:併發
MediaType
對應N個擴展名繼續回到AbstractMappingContentNegotiationStrategy
。
// @since 3.2 它是個協商策略抽象實現,同時也有了擴展名+MediaType對應關係的能力 public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver implements ContentNegotiationStrategy { // Whether to only use the registered mappings to look up file extensions, // or also to use dynamic resolution (e.g. via {@link MediaTypeFactory}. // org.springframework.http.MediaTypeFactory是Spring5.0提供的一個工廠類 // 它會讀取/org/springframework/http/mime.types這個文件,裏面有記錄着對應關係 private boolean useRegisteredExtensionsOnly = false; // Whether to ignore requests with unknown file extension. Setting this to // 默認false:若認識不認識的擴展名,拋出異常:HttpMediaTypeNotAcceptableException private boolean ignoreUnknownExtensions = false; // 惟一構造函數 public AbstractMappingContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) { super(mediaTypes); } // 實現策略接口方法 @Override public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { // getMediaTypeKey:抽象方法(讓子類把擴展名這個key提供出來) return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest)); } public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException { if (StringUtils.hasText(key)) { // 調用父類方法:根據key去查找出一個MediaType出來 MediaType mediaType = lookupMediaType(key); // 找到了就return就成(handleMatch是protected的空方法~~~ 子類目前沒有實現的) if (mediaType != null) { handleMatch(key, mediaType); // 回調 return Collections.singletonList(mediaType); } // 若沒有對應的MediaType,交給handleNoMatch處理(默認是拋出異常,見下面) // 注意:handleNoMatch若是經過工廠找到了,那就addMapping()保存起來(至關於註冊上去) mediaType = handleNoMatch(webRequest, key); if (mediaType != null) { addMapping(key, mediaType); return Collections.singletonList(mediaType); } } return MEDIA_TYPE_ALL_LIST; // 默認值:全部 } // 此方法子類ServletPathExtensionContentNegotiationStrategy有複寫 @Nullable protected MediaType handleNoMatch(NativeWebRequest request, String key) throws HttpMediaTypeNotAcceptableException { // 若不是僅僅從註冊裏的拿,那就再去MediaTypeFactory裏看看~~~ 找到了就返回 if (!isUseRegisteredExtensionsOnly()) { Optional<MediaType> mediaType = MediaTypeFactory.getMediaType("file." + key); if (mediaType.isPresent()) { return mediaType.get(); } } // 忽略找不到,返回null吧 不然拋出異常:HttpMediaTypeNotAcceptableException if (isIgnoreUnknownExtensions()) { return null; } throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes()); } }
該抽象類實現了模版處理流程。
由子類去決定:你的擴展名是來自於URL的參數仍是來自於path...
上面抽象類的子類具體實現,從名字中能看出擴展名來自於param參數。
public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { // 請求參數默認的key是format,你是能夠設置和更改的。(set方法) private String parameterName = "format"; // 惟一構造 public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) { super(mediaTypes); } ... // 生路get/set // 小Tips:這裏調用的是getParameterName()而不是直接用屬性名,之後建議你們設計框架也都這麼使用 雖然不少時候效果是同樣的,但更符合使用規範 @Override @Nullable protected String getMediaTypeKey(NativeWebRequest request) { return request.getParameter(getParameterName()); } }
根據一個查詢參數(query parameter)判斷請求的MediaType
,該查詢參數缺省使用format
。
須要注意的是:基於param的此策略
Spring MVC
雖然支持,但默認是木有開啓的,若想使用須要手動顯示開啓
它的擴展名須要從Path裏面分析出來。
public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { private UrlPathHelper urlPathHelper = new UrlPathHelper(); // 它額外提供了一個空構造 public PathExtensionContentNegotiationStrategy() { this(null); } // 有參構造 public PathExtensionContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) { super(mediaTypes); setUseRegisteredExtensionsOnly(false); setIgnoreUnknownExtensions(true); // 注意:這個值設置爲了true this.urlPathHelper.setUrlDecode(false); // 不須要解碼(url請勿有中文) } // @since 4.2.8 可見Spring MVC容許你本身定義解析的邏輯 public void setUrlPathHelper(UrlPathHelper urlPathHelper) { this.urlPathHelper = urlPathHelper; } @Override @Nullable protected String getMediaTypeKey(NativeWebRequest webRequest) { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); if (request == null) { return null; } // 藉助urlPathHelper、UriUtils從URL中把擴展名解析出來 String path = this.urlPathHelper.getLookupPathForRequest(request); String extension = UriUtils.extractFileExtension(path); return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null); } // 子類ServletPathExtensionContentNegotiationStrategy有使用和複寫 // 它的做用是面向Resource找到這個資源對應的MediaType ~ @Nullable public MediaType getMediaTypeForResource(Resource resource) { ... } }
根據請求URL
路徑中所請求的文件資源的擴展名部分判斷請求的MediaType
(藉助UrlPathHelper
和UriUtils
解析URL
)。
它是對PathExtensionContentNegotiationStrategy
的擴展,和Servlet
容器有關了。由於Servlet
額外提供了這個方法:ServletContext#getMimeType(String)
來處理文件的擴展名問題。
public class ServletPathExtensionContentNegotiationStrategy extends PathExtensionContentNegotiationStrategy { private final ServletContext servletContext; ... // 省略構造函數 // 一句話:在去工廠找以前,先去this.servletContext.getMimeType("file." + extension)這裏找一下,找到就直接返回。不然再進工廠 @Override @Nullable protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension) throws HttpMediaTypeNotAcceptableException { ... } // 同樣的:先this.servletContext.getMimeType(resource.getFilename()) 再交給父類處理 @Override public MediaType getMediaTypeForResource(Resource resource) { ... } // 二者調用父類的條件都是:mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType) }
說明:ServletPathExtensionContentNegotiationStrategy
是Spring MVC
默認就開啓支持的策略,無需手動開啓。
固定類型解析:返回固定
的MediaType。
public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy { private final List<MediaType> contentTypes; // 構造函數:必須指定MediaType // 通常經過@RequestMapping.produces這個註解屬性指定(可指定多個) public FixedContentNegotiationStrategy(MediaType contentType) { this(Collections.singletonList(contentType)); } // @since 5.0 public FixedContentNegotiationStrategy(List<MediaType> contentTypes) { this.contentTypes = Collections.unmodifiableList(contentTypes); } }
固定參數類型很是簡單,構造函數傳進來啥返回啥(不能爲null)。
介紹完了上面4中協商策略,開始介紹這個協商"容器"。
這個管理器它的做用特別像以前講述的xxxComposite
這種「容器」管理類,整體思想是管理、委託,有了以前的基礎瞭解起他仍是很是簡單的了。
// 它不只管理一堆strategies(List),還管理一堆resolvers(Set) public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver { private final List<ContentNegotiationStrategy> strategies = new ArrayList<>(); private final Set<MediaTypeFileExtensionResolver> resolvers = new LinkedHashSet<>(); ... // 若沒特殊指定,至少是包含了這一種的策略的:HeaderContentNegotiationStrategy public ContentNegotiationManager() { this(new HeaderContentNegotiationStrategy()); } ... // 由於比較簡單,因此省略其它代碼 }
它是一個ContentNegotiationStrategy
容器,同時也是一個MediaTypeFileExtensionResolver
容器。自身同時實現了這兩個接口。
顧名思義,它是專門用於來建立一個ContentNegotiationManager
的FactoryBean
。
// @since 3.2 還實現了ServletContextAware,能夠獲得當前servlet容器上下文 public class ContentNegotiationManagerFactoryBean implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean { // 默認就是開啓了對後綴的支持的 private boolean favorPathExtension = true; // 默認沒有開啓對param的支持 private boolean favorParameter = false; // 默認也是開啓了對Accept的支持的 private boolean ignoreAcceptHeader = false; private Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>(); private boolean ignoreUnknownPathExtensions = true; // Jaf是一個數據處理框架,可忽略 private Boolean useJaf; private String parameterName = "format"; private ContentNegotiationStrategy defaultNegotiationStrategy; private ContentNegotiationManager contentNegotiationManager; private ServletContext servletContext; ... // 省略普通的get/set // 注意這裏傳入的是:Properties 表示後綴和MediaType的對應關係 public void setMediaTypes(Properties mediaTypes) { if (!CollectionUtils.isEmpty(mediaTypes)) { for (Entry<Object, Object> entry : mediaTypes.entrySet()) { String extension = ((String)entry.getKey()).toLowerCase(Locale.ENGLISH); MediaType mediaType = MediaType.valueOf((String) entry.getValue()); this.mediaTypes.put(extension, mediaType); } } } public void addMediaType(String fileExtension, MediaType mediaType) { this.mediaTypes.put(fileExtension, mediaType); } ... // 這裏面處理了不少默認邏輯 @Override public void afterPropertiesSet() { List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>(); // 默認favorPathExtension=true,因此是支持path後綴模式的 // servlet環境使用的是ServletPathExtensionContentNegotiationStrategy,不然使用的是PathExtensionContentNegotiationStrategy // if (this.favorPathExtension) { PathExtensionContentNegotiationStrategy strategy; if (this.servletContext != null && !isUseJafTurnedOff()) { strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes); } else { strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes); } strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions); if (this.useJaf != null) { strategy.setUseJaf(this.useJaf); } strategies.add(strategy); } // 默認favorParameter=false 木有開啓滴 if (this.favorParameter) { ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes); strategy.setParameterName(this.parameterName); strategies.add(strategy); } // 注意這前面有個!,因此默認Accept也是支持的 if (!this.ignoreAcceptHeader) { strategies.add(new HeaderContentNegotiationStrategy()); } // 若你喜歡,你能夠設置一個defaultNegotiationStrategy 最終也會被add進去 if (this.defaultNegotiationStrategy != null) { strategies.add(this.defaultNegotiationStrategy); } // 這部分我須要提醒注意的是:這裏使用的是ArrayList,因此你add的順序就是u最後的執行順序 // 因此若你指定了defaultNegotiationStrategy,它也是放到最後的 this.contentNegotiationManager = new ContentNegotiationManager(strategies); } // 三個接口方法 @Override public ContentNegotiationManager getObject() { return this.contentNegotiationManager; } @Override public Class<?> getObjectType() { return ContentNegotiationManager.class; } @Override public boolean isSingleton() { return true; } }
這裏解釋了 該文 的順序(後綴 > 請求參數 > HTTP首部Accept
)現象。Spring MVC
是經過它來建立ContentNegotiationManager
進而管理協商策略的。
ContentNegotiationConfigurer
雖說默認狀況下Spring
開啓的協商支持能覆蓋咱們絕大部分應用場景了,但不乏有的時候咱們也仍是須要對它進行個性化的,那麼這部分就講解下對它的個性化配置~
它用於"收集"配置項,根據你提供的配置項來建立出一個ContentNegotiationManager
。
public class ContentNegotiationConfigurer { private final ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean(); private final Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>(); public ContentNegotiationConfigurer(@Nullable ServletContext servletContext) { if (servletContext != null) { this.factory.setServletContext(servletContext); } } // @since 5.0 public void strategies(@Nullable List<ContentNegotiationStrategy> strategies) { this.factory.setStrategies(strategies); } ... public ContentNegotiationConfigurer defaultContentTypeStrategy(ContentNegotiationStrategy defaultStrategy) { this.factory.setDefaultContentTypeStrategy(defaultStrategy); return this; } // 手動建立出一個ContentNegotiationManager 此方法是protected // 惟一調用處是:WebMvcConfigurationSupport protected ContentNegotiationManager buildContentNegotiationManager() { this.factory.addMediaTypes(this.mediaTypes); return this.factory.build(); } }
ContentNegotiationConfigurer
能夠認爲是提供一個設置ContentNegotiationManagerFactoryBean
的入口(本身內容new了一個它的實例),最終交給WebMvcConfigurationSupport
向容器內註冊這個Bean:
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { ... // 請注意是BeanName爲:mvcContentNegotiationManager // 若實在有須要,你是能夠覆蓋的~~~~ @Bean public ContentNegotiationManager mvcContentNegotiationManager() { if (this.contentNegotiationManager == null) { ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext); configurer.mediaTypes(getDefaultMediaTypes()); // 服務端默認支持的後綴名-->MediaType們~~~ // 這個方法就是回調咱們自定義配置的protected方法~~~~ configureContentNegotiation(configurer); // 調用方法生成一個管理器 this.contentNegotiationManager = configurer.buildContentNegotiationManager(); } return this.contentNegotiationManager; } // 默認支持的協商MediaType們~~~~ protected Map<String, MediaType> getDefaultMediaTypes() { Map<String, MediaType> map = new HashMap<>(4); // 幾乎不用 if (romePresent) { map.put("atom", MediaType.APPLICATION_ATOM_XML); map.put("rss", MediaType.APPLICATION_RSS_XML); } // 若導了jackson對xml支持的包,它就會被支持 if (jaxb2Present || jackson2XmlPresent) { map.put("xml", MediaType.APPLICATION_XML); } // jackson.databind就支持json了,因此此處通常都是知足的 // 額外還支持到了gson和jsonb。但願不久未來內置支持fastjson if (jackson2Present || gsonPresent || jsonbPresent) { map.put("json", MediaType.APPLICATION_JSON); } if (jackson2SmilePresent) { map.put("smile", MediaType.valueOf("application/x-jackson-smile")); } if (jackson2CborPresent) { map.put("cbor", MediaType.valueOf("application/cbor")); } return map; } ... }
Tips:WebMvcConfigurationSupport
是@EnableWebMvc
導進去的。
有了上面理論的支撐,那麼使用Spring MVC
協商的最佳實踐配置可參考以下(大多數狀況下都無需配置):
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.favorParameter(true) //.parameterName("mediaType") //.defaultContentTypeStrategy(new ...) // 自定義一個默認的內容協商策略 //.ignoreAcceptHeader(true) // 禁用Accept協商方式 //.defaultContentType(MediaType.APPLICATION_JSON) // 它的效果是new FixedContentNegotiationStrategy(contentTypes) 增長了對固定策略的支 //.strategies(list); //.useRegisteredExtensionsOnly() //PathExtensionContentNegotiationStrategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly); ; } }
本文從原理上分析了Spring MVC
對內容協商策略的管理、使用以及開放的配置,旨在作到心中有數,從而更好、更安全、更方便的進行擴展,對下文內容協商視圖的理解有很是大的幫助做用,有興趣的可持續關注~
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,手動邀請你入羣一塊兒飛==