本文源码基于SpringMVC 5.2.7
ContentNegotiationManager也是SpringMVC的重要功能性组件。其提供解析requst可接受的MediaType以及根据MediaType解析对应文件扩展名。
ContentNegotiationManager包含2个重要的成员变量
ContentNegotiationManager本身也实现这两个接口ContentNegotiationStrategy、MediaTypeFileExtensionResolver。ContentNegotiationManager在解析MediaType时就是遍历strategies依次解析。同样,在解析扩展名的时候也是遍历resolvers
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
... ...
//笔者注:遍历strategies解析request可接受的MediaTypes
public List resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
//笔者注:遍历resolvers解析MediaType对应的文件扩展名
private List doResolveExtensions(Function> extractor) {
List result = null;
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
List extensions = extractor.apply(resolver);
if (CollectionUtils.isEmpty(extensions)) {
continue;
}
result = (result != null ? result : new ArrayList<>(4));
for (String extension : extensions) {
if (!result.contains(extension)) {
result.add(extension);
}
}
}
return (result != null ? result : Collections.emptyList());
}
.... ....
}
在SpringMVC框架中,ContentNegotiationManager主要用在以下3种场合:
其一,在RequestMappingInfoHandlerMapping寻找匹配的RequestMappingInfo时有一种匹配条件是ProducesRequestCondition。这个条件需要获取request可接受的MediaType。这时就是使用ContentNegotiationManager来解析:
public final class ProducesRequestCondition extends AbstractRequestCondition {
... ...
private List getAcceptedMediaTypes(HttpServletRequest request)
throws HttpMediaTypeNotAcceptableException {
List result = (List) request.getAttribute(MEDIA_TYPES_ATTRIBUTE);
if (result == null) {
//笔者注:contentNegotiationManager解析request可接受的MediaTypes
result = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
request.setAttribute(MEDIA_TYPES_ATTRIBUTE, result);
}
return result;
}
... ...
}
其二,返回处理器HttpEntityMethodProcessor、RequestResponseBodyMethodProcessor往response写数据时需要判断request可接受MediaType与@RequestMapping属性produces指定的MediaTypes是否兼容
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
... ...
protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
... ...
//笔者注:获取acceptableTypes
List acceptableTypes = getAcceptableMediaTypes(request);
List producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
//笔者注:遍历acceptableTypes中与producibleTypes兼容的MediaType
List mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
... ...
}
//笔者注:获取acceptableTypes
private List getAcceptableMediaTypes(HttpServletRequest request)
throws HttpMediaTypeNotAcceptableException {
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
}
其三,在ContentNegotiatingViewResolver视图解析器根据viewName解析view时也需要根据request的可接受MediaType解出最佳视图
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
implements ViewResolver, Ordered, InitializingBean {
... ...
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
//笔者注:获取request可接受的MediaTypes
List requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
//笔者注:获取候选视图集合
List candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
//笔者注:根据requestedMediaTypes获取最佳视图
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
... ...
}
//笔者注:获取acceptableMediaTypes与producibleMediaTypes兼容的MediaTypes
protected List getMediaTypes(HttpServletRequest request) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
try {
ServletWebRequest webRequest = new ServletWebRequest(request);
//笔者注:解析出request可接受的acceptableMediaTypes
List acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
List producibleMediaTypes = getProducibleMediaTypes(request);
Set compatibleMediaTypes = new LinkedHashSet<>();
for (MediaType acceptable : acceptableMediaTypes) {
for (MediaType producible : producibleMediaTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
}
}
}
List selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
return selectedMediaTypes;
}
catch (HttpMediaTypeNotAcceptableException ex) {
if (logger.isDebugEnabled()) {
logger.debug(ex.getMessage());
}
return null;
}
}
//笔者注:获取候选视图
private List getCandidateViews(String viewName, Locale locale, List requestedMediaTypes)
throws Exception {
List candidateViews = new ArrayList<>();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
//笔者注:根据MediaType获取对应的文件扩展名
List extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
}
根据请求参数指定的文件扩展名获取MediaType。ParameterContentNegotiationStrategy类结构如下:
public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver
implements ContentNegotiationStrategy {
... ...
public List resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException {
//笔者注:getMediaTypeKey在子类ParameterContentNegotiationStrategy中的实现是获取request中参数"fomat"的值
return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
}
public List resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key)
throws HttpMediaTypeNotAcceptableException {
if (StringUtils.hasText(key)) {
//笔者注:lookupMediaType在父类MappingMediaTypeFileExtensionResolver中实现,根据文件扩展名获取MediaType。MappingMediaTypeFileExtensionResolver存储类这样的Map
MediaType mediaType = lookupMediaType(key);
if (mediaType != null) {
handleMatch(key, mediaType);
return Collections.singletonList(mediaType);
}
//笔者注:如果没命中就会走默认的文件扩展名与MediaType匹配,并且添加到MappingMediaTypeFileExtensionResolver的映射中
mediaType = handleNoMatch(webRequest, key);
if (mediaType != null) {
//笔者注:添加到映射中,供下次请求使用
addMapping(key, mediaType);
return Collections.singletonList(mediaType);
}
}
return MEDIA_TYPE_ALL_LIST;
}
... ...
}
固定返回提前配置好的MediaTypes
public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy {
private final List contentTypes;
@Override
public List resolveMediaTypes(NativeWebRequest request) {
return this.contentTypes;
}
}
这种解析MediaType策略就是从request的请求头"Accept"中解析
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
public List resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {
//笔者注: 根据请求头"Accept"解析MediaType
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
if (headerValueArray == null) {
return MEDIA_TYPE_ALL_LIST;
}
List headerValues = Arrays.asList(headerValueArray);
try {
List 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());
}
}
}
MappingMediaTypeFileExtensionResolver存储了一个Map映射关系,解析时就是从Map里面查找
public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExtensionResolver {
//笔者注:MediaType与文件扩展名的映射关系
private final ConcurrentMap> fileExtensions = new ConcurrentHashMap<>(64);
... ...
public List resolveFileExtensions(MediaType mediaType) {
List fileExtensions = this.fileExtensions.get(mediaType);
return (fileExtensions != null ? fileExtensions : Collections.emptyList());
}
... ...
}