汕尾住房和城乡建设局网站,cms网站后台上传图片提示图片类型错误但是类型是正确的,建设电子商务网站总体设计阶段,网站系统模版ViewResolver 组件ViewResolver 组件#xff0c;视图解析器#xff0c;根据视图名和国际化#xff0c;获得最终的视图 View 对象回顾先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 ViewResolver 组件#xff0c;可以回到《一个请求响应的旅行过程》中的 …ViewResolver 组件ViewResolver 组件视图解析器根据视图名和国际化获得最终的视图 View 对象回顾先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 ViewResolver 组件可以回到《一个请求响应的旅行过程》中的 DispatcherServlet 的 render 方法中看看如下protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {// Determine locale for request and apply it to the response.// 1 解析 request 中获得 Locale 对象并设置到 response 中Locale locale (this.localeResolver ! null ? this.localeResolver.resolveLocale(request) : request.getLocale());response.setLocale(locale);// 获得 View 对象View view;String viewName mv.getViewName();// 情况一使用 viewName 获得 View 对象if (viewName ! null) {// We need to resolve the view name.// 2.1 使用 viewName 获得 View 对象view resolveViewName(viewName, mv.getModelInternal(), locale, request);if (view null) { // 获取不到抛出 ServletException 异常throw new ServletException(Could not resolve view with name mv.getViewName() in servlet with name getServletName() );}}// 情况二直接使用 ModelAndView 对象的 View 对象else {// No need to lookup: the ModelAndView object contains the actual View object.// 直接使用 ModelAndView 对象的 View 对象view mv.getView();if (view null) { // 获取不到抛出 ServletException 异常throw new ServletException(ModelAndView [ mv ] neither contains a view name nor a View object in servlet with name getServletName() );}}// Delegate to the View object for rendering.// 打印日志if (logger.isTraceEnabled()) {logger.trace(Rendering view [ view ] );}try {// 3 设置响应的状态码if (mv.getStatus() ! null) {response.setStatus(mv.getStatus().value());}// 4 渲染页面view.render(mv.getModelInternal(), request, response);}catch (Exception ex) {if (logger.isDebugEnabled()) {logger.debug(Error rendering view [ view ], ex);}throw ex;}
}Nullable
protected View resolveViewName(String viewName, Nullable MapString, Object model,Locale locale, HttpServletRequest request) throws Exception {if (this.viewResolvers ! null) {// 遍历 ViewResolver 数组for (ViewResolver viewResolver : this.viewResolvers) {// 根据 viewName locale 参数解析出 View 对象View view viewResolver.resolveViewName(viewName, locale);// 解析成功直接返回 View 对象if (view ! null) {return view;}}}return null;
}
如果 ModelAndView 对象不为null且需要进行页面渲染则调用 render 方法如果设置的 View 对象是 String 类型也就是 viewName则需要调用 resolveViewName 方法通过 ViewResolver 根据 viewName 和 locale 解析出对应的 View 对象这是前后端未分离的情况下重要的一个组件ViewResolver 接口org.springframework.web.servlet.ViewResolver视图解析器根据视图名和国际化获得最终的视图 View 对象代码如下public interface ViewResolver {/*** 根据视图名和国际化获得最终的 View 对象*/NullableView resolveViewName(String viewName, Locale locale) throws Exception;
}ViewResolver 接口体系的结构如下ViewResolver 的实现类比较多其中 Spring MVC 默认使用 org.springframework.web.servlet.view.InternalResourceViewResolver 这个实现类Spring Boot 中的默认实现类如下可以看到有三个实现类org.springframework.web.servlet.view.ContentNegotiatingViewResolverorg.springframework.web.servlet.view.ViewResolverComposite默认没有实现类org.springframework.web.servlet.view.BeanNameViewResolverorg.springframework.web.servlet.view.InternalResourceViewResolver初始化过程在 DispatcherServlet 的 initViewResolvers(ApplicationContext context) 方法初始化 ViewResolver 组件方法如下private void initViewResolvers(ApplicationContext context) {// 置空 viewResolvers 处理this.viewResolvers null;// 情况一自动扫描 ViewResolver 类型的 Bean 们if (this.detectAllViewResolvers) {// Find all ViewResolvers in the ApplicationContext, including ancestor contexts.MapString, ViewResolver matchingBeans BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);if (!matchingBeans.isEmpty()) {this.viewResolvers new ArrayList(matchingBeans.values());// We keep ViewResolvers in sorted order.AnnotationAwareOrderComparator.sort(this.viewResolvers);}}// 情况二获得名字为 VIEW_RESOLVER_BEAN_NAME 的 Bean 们else {try {ViewResolver vr context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);this.viewResolvers Collections.singletonList(vr);}catch (NoSuchBeanDefinitionException ex) {// Ignore, well add a default ViewResolver later.}}// Ensure we have at least one ViewResolver, by registering// a default ViewResolver if no other resolvers are found./*** 情况三如果未获得到则获得默认配置的 ViewResolver 类* {link org.springframework.web.servlet.view.InternalResourceViewResolver}*/if (this.viewResolvers null) {this.viewResolvers getDefaultStrategies(context, ViewResolver.class);if (logger.isTraceEnabled()) {logger.trace(No ViewResolvers declared for servlet getServletName() : using default strategies from DispatcherServlet.properties);}}
}
如果“开启”探测功能则扫描已注册的 ViewResolver 的 Bean 们添加到 viewResolvers 中默认开启如果“关闭”探测功能则获得 Bean 名称为 viewResolver 对应的 Bean 将其添加至 viewResolvers如果未获得到则获得默认配置的 ViewResolver 类调用 getDefaultStrategies(ApplicationContext context, ClassT strategyInterface) 方法就是从 DispatcherServlet.properties 文件中读取 ViewResolver 的默认实现类如下org.springframework.web.servlet.ViewResolverorg.springframework.web.servlet.view.InternalResourceViewResolver在 Spring Boot 不是通过这样初始化的感兴趣的可以去看看ContentNegotiatingViewResolverorg.springframework.web.servlet.view.ContentNegotiatingViewResolver实现 ViewResolver、Ordered、InitializingBean 接口继承 WebApplicationObjectSupport 抽象类基于内容类型来获取对应 View 的 ViewResolver 实现类。其中内容类型指的是 Content-Type 和拓展后缀构造方法public class ContentNegotiatingViewResolver extends WebApplicationObjectSupportimplements ViewResolver, Ordered, InitializingBean {Nullableprivate ContentNegotiationManager contentNegotiationManager;/*** ContentNegotiationManager 的工厂用于创建 {link #contentNegotiationManager} 对象*/private final ContentNegotiationManagerFactoryBean cnmFactoryBean new ContentNegotiationManagerFactoryBean();/*** 在找不到 View 对象时返回 {link #NOT_ACCEPTABLE_VIEW}*/private boolean useNotAcceptableStatusCode false;/*** 默认 View 数组*/Nullableprivate ListView defaultViews;/*** ViewResolver 数组*/Nullableprivate ListViewResolver viewResolvers;/*** 顺序优先级最高*/private int order Ordered.HIGHEST_PRECEDENCE;
}
viewResolversViewResolver 数组。对于来说ContentNegotiatingViewResolver 会使用这些 ViewResolver们解析出所有的 View 们然后基于内容类型来获取对应的 View 们。此时的 View 结果可能是一个可能是多个所以需要比较获取到最优的 View 对象。defaultViews默认 View 数组。那么此处的默认是什么意思呢在 viewResolvers 们解析出所有的 View 们的基础上也会添加 defaultViews 到 View 结果中order顺序优先级最高。所以这也是为什么它排在最前面在上图中可以看到在 Spring Boot 中 viewResolvers 属性有三个实现类分别是 BeanNameViewResolver、ViewResolverComposite、InternalResourceViewResolverinitServletContext实现 initServletContext(ServletContext servletContext) 方法初始化 viewResolvers 属性方法如下在父类 WebApplicationObjectSupport 的父类 ApplicationObjectSupport 中可以看到因为实现了 ApplicationContextAware 接口则在初始化该 Bean 的时候会调用 setApplicationContext(Nullable ApplicationContext context) 方法在这个方法中会调用 initApplicationContext(ApplicationContext context) 这个方法这个方法又会调用initServletContext(ServletContext servletContext) 方法Override
protected void initServletContext(ServletContext servletContext) {// 1 扫描所有 ViewResolver 的 Bean 们CollectionViewResolver matchingBeans BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();// 1.1 情况一如果 viewResolvers 为空则将 matchingBeans 作为 viewResolvers 。// BeanNameViewResolver、ThymeleafViewResolver、ViewResolverComposite、InternalResourceViewResolverif (this.viewResolvers null) {this.viewResolvers new ArrayList(matchingBeans.size());for (ViewResolver viewResolver : matchingBeans) {if (this ! viewResolver) { // 排除自己this.viewResolvers.add(viewResolver);}}}// 1.2 情况二如果 viewResolvers 非空则和 matchingBeans 进行比对判断哪些未进行初始化进行初始化else {for (int i 0; i this.viewResolvers.size(); i) {ViewResolver vr this.viewResolvers.get(i);// 已存在在 matchingBeans 中说明已经初始化则直接 continueif (matchingBeans.contains(vr)) {continue;}// 不存在在 matchingBeans 中说明还未初始化则进行初始化String name vr.getClass().getName() i;obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);}}// 1.3 排序 viewResolvers 数组AnnotationAwareOrderComparator.sort(this.viewResolvers);// 2 设置 cnmFactoryBean 的 servletContext 属性this.cnmFactoryBean.setServletContext(servletContext);
}扫描所有 ViewResolver 的 Bean 们 matchingBeans情况一如果 viewResolvers 为空则将 matchingBeans 作为 viewResolvers情况二如果 viewResolvers 非空则和 matchingBeans 进行比对判断哪些未进行初始化进行初始化排序 viewResolvers 数组设置 cnmFactoryBean 的 servletContext 属性为当前 Servlet 上下文afterPropertiesSet因为 ContentNegotiatingViewResolver 实现了 InitializingBean 接口在 Sping 初始化该 Bean 的时候会调用该方法完成一些初始化工作方法如下Override
public void afterPropertiesSet() {// 如果 contentNegotiationManager 为空则进行创建if (this.contentNegotiationManager null) {this.contentNegotiationManager this.cnmFactoryBean.build();}if (this.viewResolvers null || this.viewResolvers.isEmpty()) {logger.warn(No ViewResolvers configured);}
}resolveViewName实现 resolveViewName(String viewName, Locale locale) 方法代码如下Override
Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {RequestAttributes attrs RequestContextHolder.getRequestAttributes();Assert.state(attrs instanceof ServletRequestAttributes, No current ServletRequestAttributes);// 1 获得 MediaType 数组ListMediaType requestedMediaTypes getMediaTypes(((ServletRequestAttributes) attrs).getRequest());if (requestedMediaTypes ! null) {// 2 获得匹配的 View 数组ListView candidateViews getCandidateViews(viewName, locale, requestedMediaTypes);// 3 筛选最匹配的 View 对象View bestView getBestView(candidateViews, requestedMediaTypes, attrs);// 如果筛选成功则返回if (bestView ! null) {return bestView;}}String mediaTypeInfo logger.isDebugEnabled() requestedMediaTypes ! null ? given requestedMediaTypes.toString() : ;// 4 如果匹配不到 View 对象则根据 useNotAcceptableStatusCode 返回 NOT_ACCEPTABLE_VIEW 或 null if (this.useNotAcceptableStatusCode) {if (logger.isDebugEnabled()) {logger.debug(Using 406 NOT_ACCEPTABLE mediaTypeInfo);}return NOT_ACCEPTABLE_VIEW;}else {logger.debug(View remains unresolved mediaTypeInfo);return null;}
}
调用 getMediaTypes(HttpServletRequest request) 方法获得 MediaType 数组详情见下文调用 getCandidateViews(String viewName, Locale locale, ListMediaType requestedMediaTypes) 方法获得匹配的 View 数组详情见下文调用 getBestView(ListView candidateViews, ListMediaType requestedMediaTypes, RequestAttributes attrs) 方法筛选出最匹配的 View 对象如果筛选成功则直接返回详情见下文如果匹配不到 View 对象则根据 useNotAcceptableStatusCode返回 NOT_ACCEPTABLE_VIEW 或 null其中NOT_ACCEPTABLE_VIEW 变量代码如下private static final View NOT_ACCEPTABLE_VIEW new View() {OverrideNullablepublic String getContentType() {return null;}Overridepublic void render(Nullable MapString, ? model, HttpServletRequest request, HttpServletResponse response) {response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);}
};
这个 View 对象设置状态码为 406getMediaTypesgetCandidateViews(HttpServletRequest request)方法获得 MediaType 数组如下Nullable
protected ListMediaType getMediaTypes(HttpServletRequest request) {Assert.state(this.contentNegotiationManager ! null, No ContentNegotiationManager set);try {// 创建 ServletWebRequest 对象ServletWebRequest webRequest new ServletWebRequest(request);// 从请求中获得可接受的 MediaType 数组。默认实现是从请求头 ACCEPT 中获取ListMediaType acceptableMediaTypes this.contentNegotiationManager.resolveMediaTypes(webRequest);// 获得可产生的 MediaType 数组ListMediaType producibleMediaTypes getProducibleMediaTypes(request);// 通过 acceptableTypes 来比对将符合的 producibleType 添加到 compatibleMediaTypes 结果中SetMediaType compatibleMediaTypes new LinkedHashSet();for (MediaType acceptable : acceptableMediaTypes) {for (MediaType producible : producibleMediaTypes) {if (acceptable.isCompatibleWith(producible)) {compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));}}}// 按照 MediaType 的 specificity、quality 排序ListMediaType selectedMediaTypes new ArrayList(compatibleMediaTypes);MediaType.sortBySpecificityAndQuality(selectedMediaTypes);return selectedMediaTypes;}catch (HttpMediaTypeNotAcceptableException ex) {if (logger.isDebugEnabled()) {logger.debug(ex.getMessage());}return null;}
}
getCandidateViewsgetCandidateViews(String viewName, Locale locale, ListMediaType requestedMediaTypes)方法获得匹配的 View 数组如下private ListView getCandidateViews(String viewName, Locale locale, ListMediaType requestedMediaTypes)throws Exception {// 创建 View 数组ListView candidateViews new ArrayList();// 1 来源一通过 viewResolvers 解析出 View 数组结果添加到 candidateViews 中if (this.viewResolvers ! null) {Assert.state(this.contentNegotiationManager ! null, No ContentNegotiationManager set);// 1.1 遍历 viewResolvers 数组for (ViewResolver viewResolver : this.viewResolvers) {// 1.2 情况一获得 View 对象添加到 candidateViews 中View view viewResolver.resolveViewName(viewName, locale);if (view ! null) {candidateViews.add(view);}// 1.3 情况二带有拓展后缀的方式获得 View 对象添加到 candidateViews 中for (MediaType requestedMediaType : requestedMediaTypes) {// 1.3.2 获得 MediaType 对应的拓展后缀的数组默认情况下未配置ListString extensions this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);// 1.3.3 遍历拓展后缀的数组for (String extension : extensions) {// 1.3.4 带有拓展后缀的方式获得 View 对象添加到 candidateViews 中String viewNameWithExtension viewName . extension;view viewResolver.resolveViewName(viewNameWithExtension, locale);if (view ! null) {candidateViews.add(view);}}}}}// 2 来源二添加 defaultViews 到 candidateViews 中if (!CollectionUtils.isEmpty(this.defaultViews)) {candidateViews.addAll(this.defaultViews);}return candidateViews;
}
来源一通过 viewResolvers 解析出 View 数组结果添加到 ListView candidateViews 中遍历 viewResolvers 数组情况一通过当前 ViewResolver 实现类获得 View 对象添加到 candidateViews 中情况二遍历入参 ListMediaType requestedMediaTypes将带有拓展后缀的类型再通过当前 ViewResolver 实现类获得 View 对象添加到 candidateViews 中2. 获得 MediaType 对应的拓展后缀的数组默认情况下未配置3. 遍历拓展后缀的数组4. 带有拓展后缀的方式通过当前 ViewResolver 实现类获得 View 对象添加到 candidateViews 中来源二添加 defaultViews 到 candidateViews 中getBestViewgetBestView(ListView candidateViews, ListMediaType requestedMediaTypes, RequestAttributes attrs)方法筛选出最匹配的 View 对象如下Nullable
private View getBestView(ListView candidateViews, ListMediaType requestedMediaTypes, RequestAttributes attrs) {// 1 遍历 candidateView 数组如果有重定向的 View 类型则返回它for (View candidateView : candidateViews) {if (candidateView instanceof SmartView) {SmartView smartView (SmartView) candidateView;if (smartView.isRedirectView()) {return candidateView;}}}// 2 遍历 MediaType 数组MediaTy数组已经根据pespecificity、quality进行了排序for (MediaType mediaType : requestedMediaTypes) {// 2 遍历 View 数组for (View candidateView : candidateViews) {if (StringUtils.hasText(candidateView.getContentType())) {// 2.1 如果 MediaType 类型匹配则返回该 View 对象MediaType candidateContentType MediaType.parseMediaType(candidateView.getContentType());if (mediaType.isCompatibleWith(candidateContentType)) {if (logger.isDebugEnabled()) {logger.debug(Selected mediaType given requestedMediaTypes);}attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);return candidateView;}}}}return null;
}
遍历 candidateView 数组如果有重定向的 View 类型则返回它。也就是说重定向的 View 优先级更高。遍历 MediaType 数组MediaTy数组已经根据pespecificity、quality进行了排序和 candidateView 数组如果 MediaType 类型匹配该 View 对象则返回该 View 对象。也就是说优先级的匹配规则由 ViewResolver 在 viewResolvers 的位置越靠前优先级越高。BeanNameViewResolverorg.springframework.web.servlet.view.BeanNameViewResolver实现 ViewResolver、Ordered 接口继承 WebApplicationObjectSupport 抽象类基于 Bean 的名字获得 View 对象的 ViewResolver 实现类构造方法public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {/*** 顺序优先级最低*/private int order Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
}resolveViewName实现 resolveViewName(String viewName, Locale locale) 方法根据名称获取 View 类型对应的 BeanView 对象如下Override
Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {ApplicationContext context obtainApplicationContext();// 如果对应的 Bean 对象不存在则返回 nullif (!context.containsBean(viewName)) {// Allow for ViewResolver chaining...return null;}// 如果 Bean 对应的 Bean 类型不是 View 则返回 nullif (!context.isTypeMatch(viewName, View.class)) {if (logger.isDebugEnabled()) {logger.debug(Found bean named viewName but it does not implement View);}// Since were looking into the general ApplicationContext here,// lets accept this as a non-match and allow for chaining as well...return null;}// 获得 Bean 名字对应的 View 对象return context.getBean(viewName, View.class);
}ViewResolverCompositeorg.springframework.web.servlet.view.ViewResolverComposite实现 ViewResolver、Ordered、InitializingBean、ApplicationContextAware、ServletContextAware 接口复合的 ViewResolver 实现类构造方法public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean,ApplicationContextAware, ServletContextAware {/*** ViewResolver 数组*/private final ListViewResolver viewResolvers new ArrayList();/*** 顺序优先级最低*/private int order Ordered.LOWEST_PRECEDENCE;
}afterPropertiesSet因为 ViewResolverComposite 实现了 InitializingBean 接口在 Sping 初始化该 Bean 的时候会调用该方法完成一些初始化工作方法如下Override
public void afterPropertiesSet() throws Exception {for (ViewResolver viewResolver : this.viewResolvers) {if (viewResolver instanceof InitializingBean) {((InitializingBean) viewResolver).afterPropertiesSet();}}
}resolveViewName实现 resolveViewName(String viewName, Locale locale) 方法代码如下Override
Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {// 遍历 viewResolvers 数组逐个进行解析但凡成功则返回该 View 对象for (ViewResolver viewResolver : this.viewResolvers) {// 执行解析View view viewResolver.resolveViewName(viewName, locale);// 解析成功则返回该 View 对象if (view ! null) {return view;}}return null;
}AbstractCachingViewResolverorg.springframework.web.servlet.view.AbstractCachingViewResolver实现 ViewResolver 接口继承 WebApplicationObjectSupport 抽象类提供通用的缓存的 ViewResolver 抽象类。对于相同的视图名返回的是相同的 View 对象所以通过缓存可以进一步提供性能。构造方法public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {/** Default maximum number of entries for the view cache: 1024. */public static final int DEFAULT_CACHE_LIMIT 1024;/** Dummy marker object for unresolved views in the cache Maps. */private static final View UNRESOLVED_VIEW new View() {OverrideNullablepublic String getContentType() {return null;}Overridepublic void render(Nullable MapString, ? model, HttpServletRequest request, HttpServletResponse response) {}};/** The maximum number of entries in the cache. */private volatile int cacheLimit DEFAULT_CACHE_LIMIT; // 缓存上限。如果 cacheLimit 0 表示禁用缓存/** Whether we should refrain from resolving views again if unresolved once. */private boolean cacheUnresolved true; // 是否缓存空 View 对象/** Fast access cache for Views, returning already cached instances without a global lock. */private final MapObject, View viewAccessCache new ConcurrentHashMap(DEFAULT_CACHE_LIMIT); // View 的缓存的映射/** Map from view key to View instance, synchronized for View creation. */// View 的缓存的映射。相比 {link #viewAccessCache} 来说增加了 synchronized 锁SuppressWarnings(serial)private final MapObject, View viewCreationCache new LinkedHashMapObject, View(DEFAULT_CACHE_LIMIT, 0.75f, true) {Overrideprotected boolean removeEldestEntry(Map.EntryObject, View eldest) {if (size() getCacheLimit()) {viewAccessCache.remove(eldest.getKey());return true;}else {return false;}}};
}
通过 viewAccessCache 属性提供更快的访问 View 缓存通过 viewCreationCache 属性提供缓存的上限的功能KEY 是通过 getCacheKey(String viewName, Locale locale) 方法获得缓存 KEY方法如下protected Object getCacheKey(String viewName, Locale locale) {return viewName _ locale;
}resolveViewName实现 resolveViewName(String viewName, Locale locale) 方法代码如下Override
Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {// 如果禁用缓存则创建 viewName 对应的 View 对象if (!isCache()) {return createView(viewName, locale);}else {// 获得缓存 KEYObject cacheKey getCacheKey(viewName, locale);// 从 viewAccessCache 缓存中获得 View 对象View view this.viewAccessCache.get(cacheKey);// 如果获得不到缓存则从 viewCreationCache 中获得 View 对象if (view null) {synchronized (this.viewCreationCache) {// 从 viewCreationCache 中获得 View 对象view this.viewCreationCache.get(cacheKey);if (view null) {// Ask the subclass to create the View object.// 创建 viewName 对应的 View 对象view createView(viewName, locale);// 如果创建失败但是 cacheUnresolved 为 true 则设置为 UNRESOLVED_VIEWif (view null this.cacheUnresolved) {view UNRESOLVED_VIEW;}// 如果 view 非空则添加到 viewAccessCache 缓存中if (view ! null) {this.viewAccessCache.put(cacheKey, view);this.viewCreationCache.put(cacheKey, view);}}}}else {if (logger.isTraceEnabled()) {logger.trace(formatKey(cacheKey) served from cache);}}return (view ! UNRESOLVED_VIEW ? view : null);}
}Nullable
protected View createView(String viewName, Locale locale) throws Exception {return loadView(viewName, locale);
}
Nullable
protected abstract View loadView(String viewName, Locale locale) throws Exception;逻辑比较简单主要是缓存的处理需要通过子类去创建对应的 View 对象UrlBasedViewResolverorg.springframework.web.servlet.view.UrlBasedViewResolver实现 Ordered 接口继承 AbstractCachingViewResolver 抽象类基于 Url 的 ViewResolver 实现类构造方法public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered {public static final String REDIRECT_URL_PREFIX redirect:;public static final String FORWARD_URL_PREFIX forward:;/*** View 的类型不同的实现类会对应一个 View 的类型*/Nullableprivate Class? viewClass;/*** 前缀*/private String prefix ;/*** 后缀*/private String suffix ;/*** ContentType 类型*/Nullableprivate String contentType;private boolean redirectContextRelative true;private boolean redirectHttp10Compatible true;Nullableprivate String[] redirectHosts;/*** RequestAttributes 暴露给 View 使用时的属性*/Nullableprivate String requestContextAttribute;/** Map of static attributes, keyed by attribute name (String). */private final MapString, Object staticAttributes new HashMap();/*** 是否暴露路径变量给 View 使用*/Nullableprivate Boolean exposePathVariables;Nullableprivate Boolean exposeContextBeansAsAttributes;Nullableprivate String[] exposedContextBeanNames;/*** 是否只处理指定的视图名们*/Nullableprivate String[] viewNames;/*** 顺序优先级最低*/private int order Ordered.LOWEST_PRECEDENCE;
}initApplicationContext实现 initApplicationContext() 方法进一步初始化代码如下在父类 WebApplicationObjectSupport 的父类 ApplicationObjectSupport 中可以看到因为实现了 ApplicationContextAware 接口则在初始化该 Bean 的时候会调用 setApplicationContext(Nullable ApplicationContext context) 方法在这个方法中会调用 initApplicationContext(ApplicationContext context) 这个方法这个方法又会调用initApplicationContext() 方法Override
protected void initApplicationContext() {super.initApplicationContext();if (getViewClass() null) {throw new IllegalArgumentException(Property viewClass is required);}
}在子类中会看到 viewClass 属性一般会在构造方法中设置getCacheKey重写 getCacheKey(String viewName, Locale locale) 方法忽略 locale 参数仅仅使用 viewName 作为缓存 KEY如下Override
protected Object getCacheKey(String viewName, Locale locale) {// 重写了父类的方法去除locale直接返回viewNamereturn viewName;
}也就是说不支持 Locale 特性canHandlecanHandle(String viewName, Locale locale) 方法判断传入的视图名是否可以被处理如下protected boolean canHandle(String viewName, Locale locale) {String[] viewNames getViewNames();return (viewNames null || PatternMatchUtils.simpleMatch(viewNames, viewName));
}Nullable
protected String[] getViewNames() {return this.viewNames;
}一般情况下viewNames 指定的视图名们为空所以会满足 viewNames null 代码块。也就说所有视图名都可以被处理applyLifecycleMethodsapplyLifecycleMethods(String viewName, AbstractUrlBasedView view) 方法代码如下protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {// 情况一如果 viewName 有对应的 View Bean 对象则使用它ApplicationContext context getApplicationContext();if (context ! null) {Object initialized context.getAutowireCapableBeanFactory().initializeBean(view, viewName);if (initialized instanceof View) {return (View) initialized;}}// 情况二直接返回 viewreturn view;
}createView重写 createView(String viewName, Locale locale) 方法增加了对 REDIRECT、FORWARD 的情况的处理如下Override
protected View createView(String viewName, Locale locale) throws Exception {// If this resolver is not supposed to handle the given view,// return null to pass on to the next resolver in the chain.// 是否能处理该视图名称if (!canHandle(viewName, locale)) {return null;}// Check for special redirect: prefix.if (viewName.startsWith(REDIRECT_URL_PREFIX)) { // 如果是 REDIRECT 开头创建 RedirectView 视图String redirectUrl viewName.substring(REDIRECT_URL_PREFIX.length());RedirectView view new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());String[] hosts getRedirectHosts();if (hosts ! null) {// 设置 RedirectView 对象的 hosts 属性view.setHosts(hosts);}// 应用return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);}// Check for special forward: prefix.if (viewName.startsWith(FORWARD_URL_PREFIX)) { // 如果是 FORWARD 开头创建 InternalResourceView 视图String forwardUrl viewName.substring(FORWARD_URL_PREFIX.length());InternalResourceView view new InternalResourceView(forwardUrl);// 应用return applyLifecycleMethods(FORWARD_URL_PREFIX, view);}// Else fall back to superclass implementation: calling loadView.// 创建视图名对应的 View 对象return super.createView(viewName, locale);
}
loadView实现 loadView(String viewName, Locale locale) 方法加载 viewName 对应的 View 对象方法如下Override
protected View loadView(String viewName, Locale locale) throws Exception {// x 创建 viewName 对应的 View 对象AbstractUrlBasedView view buildView(viewName);// 应用View result applyLifecycleMethods(viewName, view);return (view.checkResource(locale) ? result : null);
}其中x 处调用 buildView(String viewName) 方法创建 viewName 对应的 View 对象方法如下protected AbstractUrlBasedView buildView(String viewName) throws Exception {Class? viewClass getViewClass();Assert.state(viewClass ! null, No view class);// 创建 AbstractUrlBasedView 对象AbstractUrlBasedView view (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);// 设置各种属性view.setUrl(getPrefix() viewName getSuffix());String contentType getContentType();if (contentType ! null) {view.setContentType(contentType);}view.setRequestContextAttribute(getRequestContextAttribute());view.setAttributesMap(getAttributesMap());Boolean exposePathVariables getExposePathVariables();if (exposePathVariables ! null) {view.setExposePathVariables(exposePathVariables);}Boolean exposeContextBeansAsAttributes getExposeContextBeansAsAttributes();if (exposeContextBeansAsAttributes ! null) {view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);}String[] exposedContextBeanNames getExposedContextBeanNames();if (exposedContextBeanNames ! null) {view.setExposedContextBeanNames(exposedContextBeanNames);}return view;
}requiredViewClassrequiredViewClass() 方法定义了产生的视图代码如下protected Class? requiredViewClass() {return AbstractUrlBasedView.class;
}InternalResourceViewResolverorg.springframework.web.servlet.view.InternalResourceViewResolver继承 UrlBasedViewResolver 类解析出 JSP 的 ViewResolver 实现类构造方法public class InternalResourceViewResolver extends UrlBasedViewResolver {/*** 判断 javax.servlet.jsp.jstl.core.Config 是否存在*/private static final boolean jstlPresent ClassUtils.isPresent(javax.servlet.jsp.jstl.core.Config, InternalResourceViewResolver.class.getClassLoader());Nullableprivate Boolean alwaysInclude;public InternalResourceViewResolver() {// 获得 viewClassClass? viewClass requiredViewClass();if (InternalResourceView.class viewClass jstlPresent) {viewClass JstlView.class;}// 设置 viewClasssetViewClass(viewClass);}
}
从构造方法中可以看出视图名会是 InternalResourceView 或 JstlView 类。实际上JstlView 是 InternalResourceView 的子类。buildView重写 buildView(String viewName) 方法代码如下Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {// 调用父方法InternalResourceView view (InternalResourceView) super.buildView(viewName);if (this.alwaysInclude ! null) {view.setAlwaysInclude(this.alwaysInclude);}// 设置 View 对象的相关属性view.setPreventDispatchLoop(true);return view;
}设置两个属性Vieworg.springframework.web.servlet.ViewSpring MVC 中的视图对象用于视图渲染代码如下public interface View {String RESPONSE_STATUS_ATTRIBUTE View.class.getName() .responseStatus;String PATH_VARIABLES View.class.getName() .pathVariables;String SELECTED_CONTENT_TYPE View.class.getName() .selectedContentType;Nullabledefault String getContentType() {return null;}/*** 渲染视图*/void render(Nullable MapString, ? model, HttpServletRequest request, HttpServletResponse response)throws Exception;
}
View 接口体系的结构如下可以看到 View 的实现类非常多本文不会详细分析简单讲解两个方法在 DispatcherServlet 中会直接调用 View 的 render(Nullable MapString, ? model, HttpServletRequest request, HttpServletResponse response) 来进行渲染页面// AbstractView.java
Override
public void render(Nullable MapString, ? model, HttpServletRequest request,HttpServletResponse response) throws Exception {// 合并返回结果将 Model 中的静态数据和请求中的动态数据进行合并MapString, Object mergedModel createMergedOutputModel(model, request, response);// 进行一些准备工作修复 IE 中存在的 BUG兼容性处理prepareResponse(request, response);// 进行渲染renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}将 Model 对象与请求中的数据进行合并生成一个 Map 对象保存进入页面的一些数据进行一些准备工作修复 IE 中存在的 BUG调用 renderMergedOutputModel(MapString, Object model, HttpServletRequest request, HttpServletResponse response) 方法页面渲染如下// InternalResourceView.java
Override
protected void renderMergedOutputModel(MapString, Object model, HttpServletRequest request, HttpServletResponse response) throws Exception {// Expose the model object as request attributes.exposeModelAsRequestAttributes(model, request);// Expose helpers as request attributes, if any.// 往请求中设置一些属性Locale、TimeZone、LocalizationContextexposeHelpers(request);// Determine the path for the request dispatcher.// 获取需要转发的路径String dispatcherPath prepareForRendering(request, response);// Obtain a RequestDispatcher for the target resource (typically a JSP).// 获取请求转发器RequestDispatcher rd getRequestDispatcher(request, dispatcherPath);if (rd null) {throw new ServletException(Could not get RequestDispatcher for [ getUrl() ]: Check that the corresponding file exists within your web application archive!);}// If already included or response already committed, perform include, else forward.if (useInclude(request, response)) {response.setContentType(getContentType());if (logger.isDebugEnabled()) {logger.debug(Including [ getUrl() ]);}rd.include(request, response);} else {// Note: The forwarded resource is supposed to determine the content type itself.if (logger.isDebugEnabled()) {logger.debug(Forwarding to [ getUrl() ]);}// 最后进行转发rd.forward(request, response);}
}是不是很熟悉通过 Servlet 的 javax.servlet.RequestDispatcher 请求派发着转到对应的 URL总结本文分析了 ViewResolver 组件视图解析器根据视图名和国际化获得最终的视图 View 对象。Spring MVC 执行完处理器后生成一个 ModelAndView 对象如果该对象不为 null 并且有对应的 viewName那么就需要通过 ViewResolver 根据 viewName 解析出对应的 View 对象。在 Spring MVC 和 Spring Boot 中最主要的还是 InternalResourceViewResolver 实现类例如这么配置bean classorg.springframework.web.servlet.view.InternalResourceViewResolver!-- 自动给后面 action 的方法 return 的字符串加上前缀和后缀变成一个可用的地址 --property nameprefix value/WEB-INF/jsp/ /property namesuffix value.jsp /
/bean当返回的视图名称为 login 时View 对象的 url 就是 /WEB-INF/jsp/login.jsp调用 View 的 render 方法进行页面渲染时请求会转发到这个 url当然还有其他的 ViewResolver 实现类例如 BeanNameViewResolver目前大多数都是前后端分离的项目这个组件也许你很少用到至此《Spring MVC 源码分析》系列最后一篇文档已经讲述完了对于 Spring MVC 中大部分的内容都有分析到你会发现 Spring MVC 原来是这么回事 其中涉及到 Spring 思想相关内容在努力阅读中敬请期待~希望这系列文档能够帮助你对 Spring MVC 有进一步的理解路漫漫其修远兮~