Spring MVC 源码- ViewResolver 组件
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 Map<String, 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.ContentNegotiatingViewResolver
org.springframework.web.servlet.view.ViewResolverComposite,默认没有实现类
org.springframework.web.servlet.view.BeanNameViewResolver
org.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.Map<String, 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, we'll 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, Class<T> strategyInterface) 方法,就是从 DispatcherServlet.properties 文件中读取 ViewResolver 的默认实现类,如下:
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
在 Spring Boot 不是通过这样初始化的,感兴趣的可以去看看
ContentNegotiatingViewResolver
org.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 List<View> defaultViews;/*** ViewResolver 数组*/@Nullableprivate List<ViewResolver> viewResolvers;/*** 顺序,优先级最高*/private int order = Ordered.HIGHEST_PRECEDENCE;
}
viewResolvers:ViewResolver 数组。对于来说,ContentNegotiatingViewResolver 会使用这些 ViewResolver们,解析出所有的 View 们,然后基于内容类型,来获取对应的 View 们。此时的 View 结果,可能是一个,可能是多个,所以需要比较获取到最优的 View 对象。
defaultViews:默认 View 数组。那么此处的默认是什么意思呢?在 viewResolvers 们解析出所有的 View 们的基础上,也会添加 defaultViews 到 View 结果中
order:顺序,优先级最高。所以,这也是为什么它排在最前面
在上图中可以看到,在 Spring Boot 中 viewResolvers 属性有三个实现类,分别是 BeanNameViewResolver、ViewResolverComposite、InternalResourceViewResolver
initServletContext
实现 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 们Collection<ViewResolver> 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 数组List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());if (requestedMediaTypes != null) {// <2> 获得匹配的 View 数组List<View> 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, List<MediaType> requestedMediaTypes) 方法,获得匹配的 View 数组,详情见下文
调用 getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) 方法,筛选出最匹配的 View 对象,如果筛选成功则直接返回,详情见下文
如果匹配不到 View 对象,则根据 useNotAcceptableStatusCode,返回 NOT_ACCEPTABLE_VIEW 或 null,其中NOT_ACCEPTABLE_VIEW 变量,代码如下:
private static final View NOT_ACCEPTABLE_VIEW = new View() {@Override@Nullablepublic String getContentType() {return null;}@Overridepublic void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);}
};
这个 View 对象设置状态码为 406
getMediaTypes
getCandidateViews(HttpServletRequest request)方法,获得 MediaType 数组,如下:
@Nullable
protected List<MediaType> getMediaTypes(HttpServletRequest request) {Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");try {// 创建 ServletWebRequest 对象ServletWebRequest webRequest = new ServletWebRequest(request);// 从请求中,获得可接受的 MediaType 数组。默认实现是,从请求头 ACCEPT 中获取List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);// 获得可产生的 MediaType 数组List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);// 通过 acceptableTypes 来比对,将符合的 producibleType 添加到 compatibleMediaTypes 结果中Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();for (MediaType acceptable : acceptableMediaTypes) {for (MediaType producible : producibleMediaTypes) {if (acceptable.isCompatibleWith(producible)) {compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));}}}// 按照 MediaType 的 specificity、quality 排序List<MediaType> selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);MediaType.sortBySpecificityAndQuality(selectedMediaTypes);return selectedMediaTypes;}catch (HttpMediaTypeNotAcceptableException ex) {if (logger.isDebugEnabled()) {logger.debug(ex.getMessage());}return null;}
}
getCandidateViews
getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)方法,获得匹配的 View 数组,如下:
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)throws Exception {// 创建 View 数组List<View> 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 对应的拓展后缀的数组(默认情况下未配置)List<String> 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 数组结果,添加到 List<View> candidateViews 中
遍历 viewResolvers 数组
情况一,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews 中
情况二,遍历入参 List<MediaType> requestedMediaTypes,将带有拓展后缀的类型再通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews 中
2. 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)
3. 遍历拓展后缀的数组
4. 带有拓展后缀的方式,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews 中
来源二,添加 defaultViews 到 candidateViews 中
getBestView
getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs)方法,筛选出最匹配的 View 对象,如下:
@Nullable
private View getBestView(List<View> candidateViews, List<MediaType> 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 的位置,越靠前,优先级越高。
BeanNameViewResolver
org.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 类型对应的 Bean(View 对象),如下:
@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 we're looking into the general ApplicationContext here,// let's accept this as a non-match and allow for chaining as well...return null;}// 获得 Bean 名字对应的 View 对象return context.getBean(viewName, View.class);
}
ViewResolverComposite
org.springframework.web.servlet.view.ViewResolverComposite,实现 ViewResolver、Ordered、InitializingBean、ApplicationContextAware、ServletContextAware 接口,复合的 ViewResolver 实现类
构造方法
public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean,ApplicationContextAware, ServletContextAware {/*** ViewResolver 数组*/private final List<ViewResolver> 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;
}
AbstractCachingViewResolver
org.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() {@Override@Nullablepublic String getContentType() {return null;}@Overridepublic void render(@Nullable Map<String, ?> 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 Map<Object, 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 Map<Object, View> viewCreationCache =new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, 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 对象
UrlBasedViewResolver
org.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 Map<String, 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 特性
canHandle
canHandle(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 代码块。也就说,所有视图名都可以被处理
applyLifecycleMethods
applyLifecycleMethods(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;
}
requiredViewClass
requiredViewClass() 方法,定义了产生的视图,代码如下:
protected Class<?> requiredViewClass() {return AbstractUrlBasedView.class;
}
InternalResourceViewResolver
org.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;
}
设置两个属性
View
org.springframework.web.servlet.View,Spring 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 Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)throws Exception;
}
View 接口体系的结构如下:

可以看到 View 的实现类非常多,本文不会详细分析,简单讲解两个方法
在 DispatcherServlet 中会直接调用 View 的 render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) 来进行渲染页面
// AbstractView.java
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,HttpServletResponse response) throws Exception {// 合并返回结果,将 Model 中的静态数据和请求中的动态数据进行合并Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);// 进行一些准备工作(修复 IE 中存在的 BUG)兼容性处理prepareResponse(request, response);// 进行渲染renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
将 Model 对象与请求中的数据进行合并,生成一个 Map 对象,保存进入页面的一些数据
进行一些准备工作(修复 IE 中存在的 BUG)
调用 renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) 方法,页面渲染,如下:
// InternalResourceView.java
@Override
protected void renderMergedOutputModel(Map<String, 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 class="org.springframework.web.servlet.view.InternalResourceViewResolver"><!-- 自动给后面 action 的方法 return 的字符串加上前缀和后缀,变成一个可用的地址 --><property name="prefix" value="/WEB-INF/jsp/" /><property name="suffix" 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 有进一步的理解,路漫漫其修远兮~
相关文章:

Spring MVC 源码- ViewResolver 组件
ViewResolver 组件ViewResolver 组件,视图解析器,根据视图名和国际化,获得最终的视图 View 对象回顾先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 ViewResolver 组件,可以回到《一个请求响应的旅行过程》中的 …...

【Hello Linux】初识冯诺伊曼体系
作者:小萌新 专栏:Linux 作者简介:大二学生 希望能和大家一起进步! 本篇博客简介:简单介绍冯诺伊曼体系 冯诺伊曼体系 冯诺伊曼体系结构的合理性 我们在Linux的第一篇博客中讲解了第一台计算机的发明是为了解决导弹的…...

mysql索引,主从多个核心主题去探索问题。
网上收集不错的优化方案 事务 mvcc 详讲 详讲 索引 索引概念 MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据 库系统还维护者满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数 据…...

前端一面必会面试题(边面边更)
哪些情况会导致内存泄漏 以下四种情况会造成内存的泄漏: 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。被遗忘的计时器或回调函数: 设置了 setInterval…...

【Hello Linux】初识操作系统
作者:小萌新 专栏:Linux 作者简介:大二学生 希望能和大家一起进步! 本篇博客简介:简单介绍下操作系统的概念 操作系统 操作系统是什么? 操作系统是管理软硬件资源的软件 为什么要设计操作系统 为什么要设…...

完美的vue3动态渲染菜单路由全程
前言: 首先,我们需要知道,动态路由菜单并非一开始就写好的,而是用户登录之后获取的路由菜单再进行渲染,从而可以起到资源节约何最大程度的保护系统的安全性。 需要配合后端,如果后端的值不匹配࿰…...

2023年CDGA考试模拟题库(301-400)
2023年CDGA考试模拟题库(301-400) 300.无附加价值的信息通常也不会被删除,因为:[1分] A.它不应该被移除,所有数据都是有价值的 B.我们可能在以后的某个阶段需更这些信息 C.规程中不明确是否应该保留 D.数据是一种资产它很可能在未来被认为是有价值的 E.规程中不明确哪些是…...

Linux-常见命令
🚜关注博主:翻斗花园代码手牛爷爷 🚙Gitee仓库:牛爷爷爱写代码 目录🚒xshell热键🚗Linux基本命令🚗ls指令🚕pwd指令🚖cd指令🚌touch指令🚍mkdir指…...

2.25测试对象分类
一.按照测试对象划分1.界面测试又称UI测试,按照界面的需求(一般是ui设计稿)和界面的设计规则,对我们软件界面所展示的全部内容进行测试和检查.对于非软件来说:颜色,大小,材质,整体是否美观对于软件来说:输入框,按钮,文字,图片...的尺寸,颜色,形状,整体适配,清晰度等等,2.可靠性…...

【Zabbix实战之部署篇】Zabbix客户端的安装部署方法
【Zabbix实战之部署篇】Zabbix客户端的安装部署方法 一、Zabbix-agent2介绍1.Zabbix-agent2简介2.Zabbix-agent2优点3.主动模式和被动模式二、环境规划1.Zabbix服务器部署链接2.IP规划三、配置客户端系统环境1.关闭selinux2.放行端口或关闭防火墙四、安装zabbix-agent21.下载za…...

【CSS】CSS 层叠样式表 ② ( CSS 引入方式 - 内嵌样式 )
文章目录一、CSS 引入方式 - 内嵌样式1、内嵌样式语法2、内嵌样式示例3、内嵌样式完整代码示例4、内嵌样式运行效果一、CSS 引入方式 - 内嵌样式 1、内嵌样式语法 CSS 内嵌样式 , 一般将 CSS 样式写在 HTML 的 head 标签中 ; CSS 内嵌样式 语法如下 : <head><style …...

MySQL事务与索引
MySQL事务与索引 一、事务 1、事务简介 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。事务用来管理 insert,update,delete 语句 事务特性…...

【编程入门】应用市场(php版)
背景 前面已输出多个系列: 《十余种编程语言做个计算器》 《十余种编程语言写2048小游戏》 《17种编程语言10种排序算法》 《十余种编程语言写博客系统》 《十余种编程语言写云笔记》 《N种编程语言做个记事本》 目标 为编程初学者打造入门学习项目,使…...

文化:你所在的团队,有多少人敢讲真话?
你好,我是叶芊。 今天我们要讨论的话题是文化,说“文化”这个词你可能会觉得很虚,那我们换个词——“做事风格”,这就和你们团队平时的协作习惯密切相关了。 做事风格,往小了讲,会影响团队成员对开会的认知…...

Linux | 项目自动化构建工具 - make/Makefile
make / Makefile一、前言二、make/Makefile背景介绍1、Makefile是干什么的?2、make又是什么?三、demo实现【见见猪跑🐖】三、依赖关系与依赖方法1、概念理清2、感性理解【父与子】3、深层理解【程序的翻译环境 栈的原理】四、多学一招&#…...

Spring源码该如何阅读?十年架构师带来的Spring源码解析千万不要错过!
写在前面最近学习了一句话,感觉自己的世界瞬间明朗,不再那么紧张焦虑恐慌,同样推荐给大家,希望我们都终有所得。“如果一个人不是发自内心地想要做一件事情,那么,他是无法改变自己的人生的。” 同样这句话用…...

sonarqube 外部扫描器 go vet golangci-lint 无法导入问题
首先,请看[外部分析报告]各种语言的报告生成 go vet 2> govet-report.out#没有golangci-lint,我从网上找到了 golangci-lint run --out-format checkstyle ./... > golangci-lint-report.xml值得注意的是,貌似不支持目录,仅…...

Tesseract-OCR 控制台怎么使用
Tesseract-OCR 控制台是一个命令行工具,可以在 Windows、Linux、macOS 等操作系统中使用。下面是使用 Tesseract-OCR 控制台进行文字识别的基本步骤:安装 Tesseract-OCR:可以到 Tesseract-OCR 的官方网站(https://github.com/tess…...

九龙证券|美股创年内最大周跌幅!美联储官员密集发声!波音重挫近5%
当地时刻2月24日,美股三大指数收盘明显跌落。道指跌1.02%,标普500指数跌1.05%,纳指跌1.69%。 大型科技股普跌,微软、亚马逊跌超2%。波音大跌4.8%,居道指跌幅榜首位,公司因机身部件有问题再次暂停向用户交付…...

C++014-C++字符串
文章目录C014-C字符串字符串目标char[]和stringchar[]char*string字符常量与字符串常量字符串的输入题目描述 字符串输出题目描述在线练习:总结C014-C字符串 在线练习: http://noi.openjudge.cn/ https://www.luogu.com.cn/ 字符串 目标 1、了解字符串…...

Android 架构 MVC MVP MVVM,这一波你应该了然于心
MVC,MVP和MVVM是软件比较常用的三种软件架构,这三种架构的目的都是分离,避免将过多的逻辑全部堆积在一个类中。在Android中,Activity中既有UI的相关处理逻辑,又有数据获取逻辑,从而导致Activity逻辑复杂不单…...

物联网在医疗保健领域的5大创新应用
如今,物联网的发展越来越迅速,我们无法低估物联网在当今世界的重要性。大多数人每天都会使用到物联网设备。例如,当你使用智能手表来跟踪你的锻炼时,你就间接地使用了物联网的功能。由于物联网为世界带来了很多有效的帮助…...

【一天一门编程语言】Haskell 语言程序设计极简教程
Haskell 语言程序设计极简教程 一、什么是 Haskell Haskell 是一种纯函数式编程语言,它把程序设计抽象化到一个更高的层次,简化程序开发工作量,能够更快更容易地完成任务。 它是一种函数式编程语言,它采用函数式编程方法&#…...

getStaticPaths函数 以及 fallback参数
getStaticPaths是Next.js的一个静态生成API,它用于在构建时确定哪些页面需要被预渲染。它需要返回一个包含params属性的对象数组,其中每个对象都代表一个路径参数集合,可以被预渲染为一个静态页面。如果所有参数都已知,它们将被硬…...

msys2+minGW方案编译ffmpeg的最佳实践
一、Win10 64bit编译环境的建立1)从http://www.msys2.org/下载 msys2-x86_64-xxx.exe2) 安装msys2到默认路径 C:\msys64\3) 运行MSYS2 w644)执行 pacman -Syu 更新系统当出现提示时,选择y5) 当窗口关闭时,重…...

理解redis的数据结构
redis为什么快? 首先可以想到内存读写数据本来就快,然后IO复用快,单线程没有静态消耗和锁机制快。 还有就是数据结构的设计快。这是因为,键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操…...

Lecture6 逻辑斯蒂回归(Logistic Regression)
目录 1 常用数据集 1.1 MNIST数据集 1.2 CIFAR-10数据集 2 课堂内容 2.1 回归任务和分类任务的区别 2.2 为什么使用逻辑斯蒂回归 2.3 什么是逻辑斯蒂回归 2.4 Sigmoid函数和饱和函数的概念 2.5 逻辑斯蒂回归模型 2.6 逻辑斯蒂回归损失函数 2.6.1 二分类损失函数 2.…...

File类及IO流说明
目录 1.File类说明 (1)构造方法创建文件 (2)创建功能 (3)File类的判断和获取功能 (4)文件删除功能 2.I/O流说明 (1).分类 3.字节流写数据 (1)说明 (2)字节流写数据的三种方式 (3)写入时实现换行和追加写入 (4)异常处理中加入finally实现资源的释放 4.字节流读数据 …...

优秀的网络安全工程师应该有哪些能力?
网络安全工程师是一个各行各业都需要的职业,工作内容属性决定了它不会只在某一方面专精,需要掌握网络维护、设计、部署、运维、网络安全等技能。目前稍有经验的薪资在10K-30K之间,全国的网络安全工程师还处于一个供不应求的状态,因…...

[C++11] auto初始值类型推导
背景:旧标准的auto 在旧标准中,auto代表“具有自动存储期的 局部变量” auto int i 0; //具有自动存储期的局部变量 //C98/03,可以默认写成int i0; static int j 0; //静态类型的定义方法实际上,我们很少使用auto,…...