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 添加到 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 对象不存在,则返回 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 有进一步的理解,路漫漫其修远兮~

















![MySQL到Elasticsearch实时同步构建数据检索服务的选型与思考[转载]](https://img-blog.csdnimg.cn/img_convert/3869a9e96ec66bb7619d77d120b0fac1.png)
