内容协商在视图View上的应用【享学Spring MVC】 (2)

这里我们能发现,它默认情况下使用的是我们上文说的默认的ContentNegotiationManager来处理内容协商的。因此下面重点要来到今天的主角ContentNegotiatingViewResolver身上

ContentNegotiatingViewResolver:内容协商视图解析器

ContentNagotiatingViewResolver自己并不解析视图,而是委派给其他的视图处理器。

为了使这个解析器正常工作,order序号需要设置成比其他的视图处理器高的优先级(默认就是最高的)

// @since 3.0 public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean { // 用于内容协商的管理器 @Nullable private ContentNegotiationManager contentNegotiationManager; private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean(); // 如果没有合适的view的时候,是否使用406这个状态码(HttpServletResponse#SC_NOT_ACCEPTABLE) // 默认值是false:表示没有找到就返回null,而不是406 private boolean useNotAcceptableStatusCode = false; // 当无法获取到具体的视图时,会走defaultViews @Nullable private List<View> defaultViews; @Nullable private List<ViewResolver> viewResolvers; private int order = Ordered.HIGHEST_PRECEDENCE; // 默认,优先级就是最高的 // 复写:WebApplicationObjectSupport的方法 // 它在setServletContext和initApplicationContext会调用(也就是容器启动时候会调用) @Override protected void initServletContext(ServletContext servletContext) { Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values(); //容器内找到了 就以容器内所有已经配置好的视图解析器都拿出来(包含父容器) if (this.viewResolvers == null) { this.viewResolvers = new ArrayList<>(matchingBeans.size()); for (ViewResolver viewResolver : matchingBeans) { if (this != viewResolver) { // 排除自己 this.viewResolvers.add(viewResolver); } } } else { // 进入这里证明是调用者自己set进来的 for (int i = 0; i < this.viewResolvers.size(); i++) { ViewResolver vr = this.viewResolvers.get(i); if (matchingBeans.contains(vr)) { continue; } String name = vr.getClass().getName() + i; // 对视图解析器完成初始化工作~~~~~ // 关于AutowireCapableBeanFactory的使用,参见:https://blog.csdn.net/f641385712/article/details/88651128 obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name); } } // 找到所有的ViewResolvers排序后,放进ContentNegotiationManagerFactoryBean里 AnnotationAwareOrderComparator.sort(this.viewResolvers); this.cnmFactoryBean.setServletContext(servletContext); } // 从这一步骤可以知道:contentNegotiationManager 可以自己set // 也可以通过工厂来生成 两种方式均可 @Override public void afterPropertiesSet() { if (this.contentNegotiationManager == null) { this.contentNegotiationManager = this.cnmFactoryBean.build(); } if (this.viewResolvers == null || this.viewResolvers.isEmpty()) { logger.warn("No ViewResolvers configured"); } } // 处理逻辑视图到View 在此处会进行内容协商 @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); // getMediaTypes()这个方法完成了 // 1、通过contentNegotiationManager.resolveMediaTypes(webRequest)得到请求的MediaTypes // 2、拿到服务端能够提供的MediaTypes producibleMediaTypes // (请注意因为没有消息转换器,所以它的值的唯一来源是:request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)) // (若没有指定producers的值,那就是ALL) // 3、按照优先级,协商出`selectedMediaTypes`(是个List) List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); // 进入此处:说明协商出了有可用的MediaTypes(至少有一个嘛) if (requestedMediaTypes != null) { // getCandidateViews()这个很重要的方法,见下文 List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); // 上面一步骤解析出了多个符合条件的views,这里就是通过MediaType、attrs等等一起决定出一个,一个,一个最佳的 // getBestView()方法描述如下: // 第一大步:遍历所有的candidateViews,只要是smartView.isRedirectView(),就直接return // 第二大步:遍历所有的requestedMediaTypes,针对每一种MediaType下再遍历所有的candidateViews // 1、针对每一种MediaType,拿出View.getContentType(),只会看这个值不为null的 // 2、view的contentType!=null,继续看看mediaType.isCompatibleWith(candidateContentType) 若不匹配这个视图就略过 // 3、若匹配:attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST) 然后return掉此视图作为best最佳的 View bestView = getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { // 很显然,找到了最佳的就返回渲染吧 return bestView; } } ... // useNotAcceptableStatusCode=true没找到视图就返回406 // NOT_ACCEPTABLE_VIEW是个private内部静态类View,它的render方法只有一句话: // response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); if (this.useNotAcceptableStatusCode) { return NOT_ACCEPTABLE_VIEW; } else { return null; } } // 根据viewName、requestedMediaTypes等等去得到所有的备选的Views~~ // 这这里会调用所有的viewResolvers.resolveViewName()来分别处理~~~所以可能生成多多个viewo ~ private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception { List<View> candidateViews = new ArrayList<>(); if (this.viewResolvers != null) { Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set"); // 遍历所有的viewResolvers,多逻辑视图一个一个的处理 for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); // 处理好的就装进来 } // 另外还没有完:遍历所有支持的MediaType,拿到它对应的扩展名们(一个MediaType可以对应多个扩展名) // 如果viewName + '.' + extension能被处理成一个视图,也是ok的 // 也就是说index和index.jsp都能被解析成view视图~~~ for (MediaType requestedMediaType : requestedMediaTypes) { // resolveFileExtensions()方法可以说这里是唯一调用的地方 List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType); for (String extension : extensions) { String viewNameWithExtension = viewName + '.' + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); if (view != null) { candidateViews.add(view); // 带上后缀名也能够处理的 这种视图也ok } } } } } // 若指定了默认视图,把视图也得加上(在最后面哦~) if (!CollectionUtils.isEmpty(this.defaultViews)) { candidateViews.addAll(this.defaultViews); } return candidateViews; } }

关于ContentNegotiatingViewResolver我总结出如下细节要点:

ContentNegotiationManager用于内容协商的策略可以手动set指定,也可以通过FactoryBean自动生成

viewResolvers默认是去容器内找到所有的,当然你也可以手动set进来的~

使用request的媒体类型,根据扩展名选择不同的view输出不同的格式

不是自己处理view,而是代理给不同的ViewResolver来处理不同的view;

默认是支持Accept和后缀的协商方式的。并且还支持 逻辑视图名.后缀的视图解析方式~

依据View.getContentType匹配MediaType来完成的最佳匹配

如何使用?

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsspx.html