Fork me on GitHub

ViewResolver

注意:所有文章除特别说明外,转载请注明出处.

第14章 ViewResolver

ViewResolver主要的作用是根据视图名和Locale解析出视图,解析过程主要做两件事:解析出使用的模板和视图的类型。SpringMVC中的ViewResolver主要分为四大类:1.AbstractCachingViewResolver | 2.BeanNameViewResolver | 3.ContentNegotiatingViewResolver | 4.ViewResolverComposite 。其中在后面的三个只有一个实现类。

1.BeanNameViewResolver:通过beanName从SpringMVC容器中查找。
2.ViewResolverComposite:该类封装着多个ViewResolver的容器,解析视图时遍历封装着的ViewResolver具体解析。在ViewResolverComposite遍历成员解析视图的同时还给成员初始化。其中包括对实现了ApplicationContextAware接口的ViewResolver设置ApplicationContext、是给实现了ServletContextAware接口的ViewResolver设置ServletContext以及对实现了InitializingBean接口的ViewResolver调用afterPropertiesSet方法。

    public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean, ApplicationContextAware, ServletContextAware {
        private final List<ViewResolver> viewResolvers = new ArrayList();
        private int order = 2147483647;

        public ViewResolverComposite() {
        }

        public void setViewResolvers(List<ViewResolver> viewResolvers) {
            this.viewResolvers.clear();
            if (!CollectionUtils.isEmpty(viewResolvers)) {
                this.viewResolvers.addAll(viewResolvers);
            }

        }

        public List<ViewResolver> getViewResolvers() {
            return Collections.unmodifiableList(this.viewResolvers);
        }

        public void setOrder(int order) {
            this.order = order;
        }

        public int getOrder() {
            return this.order;
        }

        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            Iterator var2 = this.viewResolvers.iterator();

            while(var2.hasNext()) {
                ViewResolver viewResolver = (ViewResolver)var2.next();
                if (viewResolver instanceof ApplicationContextAware) {
                    ((ApplicationContextAware)viewResolver).setApplicationContext(applicationContext);
                }
            }

        }

        public void setServletContext(ServletContext servletContext) {
            Iterator var2 = this.viewResolvers.iterator();

            while(var2.hasNext()) {
                ViewResolver viewResolver = (ViewResolver)var2.next();
                if (viewResolver instanceof ServletContextAware) {
                    ((ServletContextAware)viewResolver).setServletContext(servletContext);
                }
            }

        }

        public void afterPropertiesSet() throws Exception {
            Iterator var1 = this.viewResolvers.iterator();

            while(var1.hasNext()) {
                ViewResolver viewResolver = (ViewResolver)var1.next();
                if (viewResolver instanceof InitializingBean) {
                    ((InitializingBean)viewResolver).afterPropertiesSet();
                }
            }

        }

        //主要是这个方法用来解析View的
        @Nullable
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            Iterator var3 = this.viewResolvers.iterator();

            View view;
            do {
                if (!var3.hasNext()) {
                    return null;
                }

                ViewResolver viewResolver = (ViewResolver)var3.next();
                view = viewResolver.resolveViewName(viewName, locale);
            } while(view == null);

            return view;
        }
    }

总结:这里它真正的解析过程就是最后一个方法 resolveViewName()。其它方法都是给所包含的ViewResolver做初始化。

14.1 ContentNegotiatingViewResolver

该解析器的作用是在别的解析器解析的结果上增加了对MediaType(Content-Type 媒体类型)和后缀支持。对视图的解析不是其本身完成的,而是通过封装的ViewResolver来进行的。

整个过程是:1.首先遍历所封装的ViewResolver具体视图,这里可能会解析出多个视图。2.然后使用request获取MediaType,可能也是多个结果。3.最后将这两个结果进行匹配查找出最优的视图。

属性ViewResolver的两种初始化方式:1.手动设置,不在Spring容器中,则会对它初始化。2.如果没有设置则自动获取Spring容器中除它自己外的所有ViewResolver并设置到ViewResolver中。

1.initServletContext

 protected void initServletContext(ServletContext servletContext) {
    //获取容器中所有的ViewResolver类型的bean,是整个spring容器,而不仅仅是springMVC获取的
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
    ViewResolver viewResolver;

    //如果没有手动注册则将容器中找到的ViewResolver设置给ViewResolvers
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList(matchingBeans.size());
        Iterator var3 = matchingBeans.iterator();

        while(var3.hasNext()) {
            viewResolver = (ViewResolver)var3.next();
            if (this != viewResolver) {
                this.viewResolvers.add(viewResolver);
            }
        }
    } else {
         //如果是手动注册,但是容器中不存在,则进行初始化
        for(int i = 0; i < this.viewResolvers.size(); ++i) {
            viewResolver = (ViewResolver)this.viewResolvers.get(i);
            if (!matchingBeans.contains(viewResolver)) {
                String name = viewResolver.getClass().getName() + i;
                this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
            }
        }
    }

    if (this.viewResolvers.isEmpty()) {
        this.logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the 'viewResolvers' property on the ContentNegotiatingViewResolver");
    }
    //按照Order属性进行排序
    AnnotationAwareOrderComparator.sort(this.viewResolvers);
    this.cnmFactoryBean.setServletContext(servletContext);
}

2.resolveViewName 解析视图的方法

@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    //使用RequestContextHolder获取RequestAttribute,进而获取request
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    //通过request获取MediaType,用作需要满足的条件
    List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
    if (requestedMediaTypes != null) {
        //获取所有的候选视图,内部通过遍历封装的viewResolver来解析
        List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
        //从多个候选视图中选出最好的一个
        View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }

    if (this.useNotAcceptableStatusCode) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
        }

        return NOT_ACCEPTABLE_VIEW;
    } else {
        this.logger.debug("No acceptable view found; returning null");
        return null;
    }
}

总结:这里整个过程是:1.首先使用request获取MediaType作为需要满足的条件。2.然后使用viewResolver解析出多个候选视图。3.最后将两者进行匹配找出最优视图。

3.getCandidateView() 获取候选视图

总结:获取候选视图逻辑:1.遍历viewResolver进行视图解析,同时将所有解析出的结果添加到候选视图。2.然后判断有没有设置默认视图,如果有责将其添加到候选视图。同时这里不仅使用逻辑视图进行解析,而且还使用了通过遍历requestedMediaTypes获取到所对应的后缀,然后添加到逻辑视图后面作为新的视图名进行解析。

4.getBestView() 获取最优视图

private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
    Iterator var4 = candidateViews.iterator();
    //判断候选视图中有没有redirect视图,如果有直接返回
    while(var4.hasNext()) {
        View candidateView = (View)var4.next();
        if (candidateView instanceof SmartView) {
            SmartView smartView = (SmartView)candidateView;
            if (smartView.isRedirectView()) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Returning redirect view [" + candidateView + "]");
                }

                return candidateView;
            }
        }
    }

    var4 = requestedMediaTypes.iterator();

    while(var4.hasNext()) {
        MediaType mediaType = (MediaType)var4.next();
        Iterator var10 = candidateViews.iterator();

        while(var10.hasNext()) {
            View candidateView = (View)var10.next();
            if (StringUtils.hasText(candidateView.getContentType())) {
                //根据候选视图获取对应的MediaType
                MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
                //判断当前MediaType是否支持从候选视图获取对应的MediaType,如text/*可以支持test/html,text/css,text/xml等所有的text类型
                if (mediaType.isCompatibleWith(candidateContentType)) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Returning [" + candidateView + "] based on requested media type '" + mediaType + "'");
                    }

                    attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, 0);
                    return candidateView;
                }
            }
        }
    }

    return null;
}

总结:1.首先判断候选视图中有没有redirect视图,如果有则直接返回,否则同时遍历从request中获取的requestMediaTypes和解析出的候选逻辑视图candidateViews。2.然后根据候选视图获取对应的MediaType,同时使用当前的requestedMediaType对其进行判断,如果支持则将所有的requestedMediaType添加到request的Attribute中,以便在视图渲染过程中使用,并将当前视图返回。

14.2 AbstractCachingViewResolver 系列

14.2.1 AbstractCachingViewResolver解析视图

该解析器提供统一的缓存功能,当视图被解析过一次就缓存起来,直到缓存被删除前视图的解析都会自动从缓存中获取。它的直接继承有三个类:

1.ResourceBundleViewResolver:通过使用properties属性配置文件解析视图。

2.XmlViewResolver:通过使用xml属性配置文件解析视图。

3.UrlBasedViewResolver:所有直接将逻辑视图作为url查找模板文件的ViewResolver的基类,这个类设置了统一的查找模板的规则,它的子类只需要确定渲染方式就可以确定视图类型,它的每一个子类都对应一种视图类型。

提示:前两种解析器的原理:1.首先根据Locale将相应的配置文件初始化到BeanFactory。2.然后直接将逻辑视图作为beanName到factory里查找。两者的loadView程序一样。

protected View loadView(String viewName, Locale locale) throws BeansException {
    BeanFactory factory = initFactory();
    try{
        return factory.getBean(viewName, View.class);
    }catch(NoSuchBeanDefinitionException ex){
        return null;
    }
}

1.AbstractCachingViewResolver解析视图过程

public View resolveViewName(String viewName, Locale locale) throws Exception {
    //是否有缓存
    if (!this.isCache()) {
        //实际创建视图
        return this.createView(viewName, locale);
    } else {
        Object cacheKey = this.getCacheKey(viewName, locale);
        //这里是通过concurrentHashMap的容器中获取的缓存
        View view = (View)this.viewAccessCache.get(cacheKey);
        if (view == null) {
            Map var5 = this.viewCreationCache;
            synchronized(this.viewCreationCache) {
                view = (View)this.viewCreationCache.get(cacheKey);
                if (view == null) {
                    //创建视图
                    view = this.createView(viewName, locale);
                    if (view == null && this.cacheUnresolved) {
                        view = UNRESOLVED_VIEW;
                    }

                    if (view != null) {
                        //这里同时在创建视图后存入缓存中,这里放到两个容器中了
                        this.viewAccessCache.put(cacheKey, view);
                        this.viewCreationCache.put(cacheKey, view);
                        if (this.logger.isTraceEnabled()) {
                            this.logger.trace("Cached view [" + cacheKey + "]");
                        }
                    }
                }
            }
        }

        return view != UNRESOLVED_VIEW ? view : null;
    }
}

@Nullable
protected View createView(String viewName, Locale locale) throws Exception {
    return this.loadView(viewName, locale);
}
//这是一个模板方法由子类实现
@Nullable
protected abstract View loadView(String var1, Locale var2) throws Exception;

总结:1.首先判断是否开启缓存功能,如果没有开启则直接调用createView()创建视图,否则检查是否已经存在缓存中。如果存在则直接获取返回,否则使用createView创建一个。2.然后保存到缓存中返回。

提示:在createView()方法内部直接调用了loadView()方法,loadView()方法是模板方法,留给子类实际创建视图,是子类解析视图的入口方法。

注意:该AbstractCachingViewResolver中有一个cacheLimit参数,是用来设置最大缓存数,设置为0表不启用缓存。设置为正数表示最多可以缓存视图的数量。

14.2.2 UrlBasedViewResolver

在该解析器中重写了父类的 getCacheKey | createView | loadView三个方法。

1.getCacheKey():该方法直接返回viewName 和原来父类的返回viewName+“_”+locale相比,子类覆盖而没有使用locale,说明这个UrlBasedViewResolver并没有使用Locale,只是用viewName。

2.createView():该方法首先检查是否可以解析传入的逻辑视图,如果不可以则返回null,然后让别的ViewResolver解析,接着分别检查是不是redirect或者forward视图,如果是则返回相应的视图,如果不是则交给父类的createView,父类中又调用loadView。

    protected View createView(String viewName, Locale locale) throws Exception {
        //检查是否支持此逻辑视图,可以配置支持的模板
        if (!this.canHandle(viewName, locale)) {
            return null;
        } else {
            String forwardUrl;
            //检查是不是redirect视图
            if (viewName.startsWith("redirect:")) {
                forwardUrl = viewName.substring("redirect:".length());
                RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
                String[] hosts = this.getRedirectHosts();
                if (hosts != null) {
                    view.setHosts(hosts);
                }

                return this.applyLifecycleMethods(viewName, view);
            //检查是不是forward视图
            } else if (viewName.startsWith("forward:")) {
                forwardUrl = viewName.substring("forward:".length());
                return new InternalResourceView(forwardUrl);
            } else {
                //如果都不是则调用父类的createView,也就会调用loadView()方法
                return super.createView(viewName, locale);
            }
        }
    }

3.检查是否支持传入的逻辑视图和传入的逻辑视图是redirect和forward视图的功能。这里使用canHandle()方法检查,它通过配置的viewName属性检查,这里如果没有配置则可以解析所有逻辑视图,如果已经配置则按照配置的模式检查。

    //canHandle()方法
    protected boolean canHandle(String viewName, Locale locale) {
        String[] viewNames = getViewNames();
        return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
    }

    //loadView()方法
    protected View loadView(String viewName, Locale locale) throws Exception {
        //1.创建view
        AbstractUrlBasedView view = buildView(viewName);

        //2.使用applyLifecycleMethods()方法对创建的view初始化
        View result = applyLifecycleMethods(viewName, view);

        //3.检查view对应的模板是否存在,如果存在则将初始化视图返回
        return (View.checkResource(locale) ? result : null);
    }

    private View applyLifecycleMethods(String viewName, AbstractView view) {
        return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
    }

4.buildView()方法,它的作用是具体创建View。

    protected AbstractUrlBasedView buildView(String viewName) throws Exception {
        //这里viewClass可以在子类通过setViewClass()方法重新设置viewClass类型。
        Class<?> viewClass = this.getViewClass();
        Assert.state(viewClass != null, "No view class");

        AbstractUrlBasedView view = (AbstractUrlBasedView)BeanUtils.instantiateClass(viewClass);

        //这里给viewName加上前缀和后缀,可以通过配置设置
        view.setUrl(this.getPrefix() + viewName + this.getSuffix());

        String contentType = this.getContentType();
        if (contentType != null) {
            //contentType不为空设置给view
            view.setContentType(contentType);
        }

        view.setRequestContextAttribute(this.getRequestContextAttribute());
        view.setAttributesMap(this.getAttributesMap());
        Boolean exposePathVariables = this.getExposePathVariables();
        if (exposePathVariables != null) {
            //这个表示让view使用PathVariables,可以在ViewResolver中设置,PathVariable就是处理器中@PathVariables注释的参数
            view.setExposePathVariables(exposePathVariables);
        }

        Boolean exposeContextBeansAsAttributes = this.getExposeContextBeansAsAttributes();
        if (exposeContextBeansAsAttributes != null) {
            //这个不为空的时候设置给view,表示让view使用容器中注册的bean,此参数可以在ViewResolver中配置
            view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
        }

        String[] exposedContextBeanNames = this.getExposedContextBeanNames();
        if (exposedContextBeanNames != null) {
            //这个不为空的时候设置给view,表示让view使用容器中注册的bean,此参数可以在ViewResolver中配置
            view.setExposedContextBeanNames(exposedContextBeanNames);
        }
        return view;
    }


    public void setViewClass(@Nullable Class<?> viewClass) {
        if (viewClass != null && !this.requiredViewClass().isAssignableFrom(viewClass)) {
            throw new IllegalArgumentException("Given view class [" + viewClass.getName() + "] is not of type [" + this.requiredViewClass().getName() + "]");
        } else {
            this.viewClass = viewClass;
        }
    }

    @Nullable
    protected Class<?> getViewClass() {
        return this.viewClass;
    }

    protected Class<?> requiredViewClass() {
        return AbstractUrlBasedView.class;
    }

总结:这里我们知道UrlBasedViewResolver的子类主要的三件事情:1.通过重写 requiredViewClass()方法修改必须符合的视图类型的值。2.使用setViewClass()方法设置所用的视图类型。3.给创建出来的视图设置一些属性。

14.2.3 InternalResourceViewResolver和FreeMarkerViewResolver

InternalResourceViewResolver和FreeMarkerViewResolver两个解析器,前面的解析器是用来解析JSP的,后面的解析器是用来解析freemarker视图的。

1.InternalResourceViewResolver

public class InternalResourceViewResolver extends UrlBasedViewResolver {
    private static final boolean jstlPresent = ClassUtils.isPresent("javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());
    @Nullable
    private Boolean alwaysInclude;
    //这里如果类型是jstlPresent,viewClass会使用JstlView.class
    public InternalResourceViewResolver() {
        Class<?> viewClass = this.requiredViewClass();
        if (InternalResourceView.class == viewClass && jstlPresent) {
            viewClass = JstlView.class;
        }

        this.setViewClass(viewClass);
    }

    public InternalResourceViewResolver(String prefix, String suffix) {
        this();
        this.setPrefix(prefix);
        this.setSuffix(suffix);
    }
    //返回的类型是InternalResourceView  
    protected Class<?> requiredViewClass() {
        return InternalResourceView.class;
    }

    public void setAlwaysInclude(boolean alwaysInclude) {
        this.alwaysInclude = alwaysInclude;
    }
    //新添加了alwaysInclude属性
    protected AbstractUrlBasedView buildView(String viewName) throws Exception {
        InternalResourceView view = (InternalResourceView)super.buildView(viewName);
        //这个表示是否在使用forward的情况下也强制使用include,默认是false,可以在注册解析器时配置
        if (this.alwaysInclude != null) {
            view.setAlwaysInclude(this.alwaysInclude);
        }
        //用于阻止循环调用,也就是请求处理完成后又转发回了原来使用的处理器的情况
        view.setPreventDispatchLoop(true);
        return view;
    }
}

2.FreeMarkerViewResolver

public class AbstractTemplateViewResolver extends UrlBasedViewResolver {
    private boolean exposeRequestAttributes = false;
    private boolean allowRequestOverride = false;
    private boolean exposeSessionAttributes = false;
    private boolean allowSessionOverride = false;
    private boolean exposeSpringMacroHelpers = true; 
    //...setter和getter方法

   protected AbstractUrlBasedView buildView(String viewName) throws Exception {
        AbstractTemplateView view = (AbstractTemplateView)super.buildView(viewName);
        //是否将requestAttributes暴露给view,默认为false
        view.setExposeRequestAttributes(this.exposeRequestAttributes);
        //当requestAttributes中存在Model中同名的参数,是否允许将Model中的值覆盖,默认false
        view.setAllowRequestOverride(this.allowRequestOverride);
        //是否将SessionAttribute暴露给view使用,默认是false
        view.setExposeSessionAttributes(this.exposeSessionAttributes);
        //当SessionAttributes中存在Model中同名的参数,是否使用requestAttributes的值将Model中的覆盖,默认false
        view.setAllowSessionOverride(this.allowSessionOverride);
        view.setExposeSpringMacroHelpers(this.exposeSpringMacroHelpers);
        return view;
    }
}

//在这里只需要覆盖requiredViewClass()方法就返回FreeMakerView类型
public class FreeMarkerViewResolver extends AbstractTemplateViewResolver {
    public FreeMarkerViewResolver() {
        this.setViewClass(this.requiredViewClass());
    }

    public FreeMarkerViewResolver(String prefix, String suffix) {
        this();
        this.setPrefix(prefix);
        this.setSuffix(suffix);
    }

    protected Class<?> requiredViewClass() {
        return FreeMarkerView.class;
    }
}

总结:大部分实现类都是继承AbstractCachingViewResolver,它提供了对解析结果进行缓存的统一解决方法,它的子类中ResourceBundlerViewResolver和XmlViewResolver分别通过properties和xml配置文件进行解析。UrlBasedViewResolver将viewName添加前后缀用作url,它的子类只需要提供视图类型就可以了。解析视图的核心工作是查找模板文件和视图类型,而查找的主要参数只有viewName,这里有三种解析思路:

1.使用viewName查找模板文件:对应URLBasedViewResolver
2.使用viewName查找视图类型:对应BeanNameViewResolver
3.使用viewName同时查找视图类型和模板文件:对应ResourceBundlerViewResolver和XmlViewResolver

本文标题:ViewResolver

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:16:56

原始链接:http://bangjinhu.github.io/undefined/第14章 ViewResolver/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.