该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读
Spring 版本:5.2.4.RELEASE
该系列其他文档请查看:《精尽 Spring MVC 源码分析 - 文章导读》
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 对象 */ @Nullable View 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 WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean { @Nullable private ContentNegotiationManager contentNegotiationManager; /** * ContentNegotiationManager 的工厂,用于创建 {@link #contentNegotiationManager} 对象 */ private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean(); /** * 在找不到 View 对象时,返回 {@link #NOT_ACCEPTABLE_VIEW} */ private boolean useNotAcceptableStatusCode = false; /** * 默认 View 数组 */ @Nullable private List<View> defaultViews; /** * ViewResolver 数组 */ @Nullable private 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、InternalResourceViewResolver if (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 中,说明已经初始化,则直接 continue if (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 @Nullable public String getContentType() { return null; } @Override public 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 添加到 mediaTypesToUse 结果数组中 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 对象不存在,则返回 null if (!context.containsBean(viewName)) { // Allow for ViewResolver chaining... return null; } // 如果 Bean 对应的 Bean 类型不是 View ,则返回 null if (!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 @Nullable public String getContentType() { return null; } @Override public 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) { @Override protected 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 { // 获得缓存 KEY Object 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_VIEW if (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 的类型 */ @Nullable private Class<?> viewClass; /** * 前缀 */ private String prefix = ""; /** * 后缀 */ private String suffix = ""; /** * ContentType 类型 */ @Nullable private String contentType; private boolean redirectContextRelative = true; private boolean redirectHttp10Compatible = true; @Nullable private String[] redirectHosts; /** * RequestAttributes 暴露给 View 使用时的属性 */ @Nullable private String requestContextAttribute; /** Map of static attributes, keyed by attribute name (String). */ private final Map<String, Object> staticAttributes = new HashMap<>(); /** * 是否暴露路径变量给 View 使用 */ @Nullable private Boolean exposePathVariables; @Nullable private Boolean exposeContextBeansAsAttributes; @Nullable private String[] exposedContextBeanNames; /** * 是否只处理指定的视图名们 */ @Nullable private 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直接返回viewName return 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; } } // 情况二,直接返回 view return 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()); @Nullable private Boolean alwaysInclude; public InternalResourceViewResolver() { // 获得 viewClass Class<?> viewClass = requiredViewClass(); if (InternalResourceView.class == viewClass && jstlPresent) { viewClass = JstlView.class; } // 设置 viewClass setViewClass(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"; @Nullable default 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、LocalizationContext exposeHelpers(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 的理解,路漫漫其修远兮~