在绝对力量面前,一切技巧都是浮云
上文 介绍了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,手动邀请你入群一块儿飞==