SpringMVC源码解析之ViewResolver

1. ViewResolver

如果ModelAndView对象中的view属性值是String类型,则根据其视图名称解析成View对象,View是用来渲染页面的,将ModelAndView中的Model填入模板中生成HTML或其他格式的文件。可以设置多个解析策略,它们将通过order属性来排序,order越小优先级越高,当前面的视图解析器解析成功后将直接返回View对象,不再执行后续的解析器。

我们前面介绍过DispatcherServlet中取得ViewResolver的集合循环遍历,调用ViewResolver的resolveViewName方法来解析视图名获取View对象,源码如下:

protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
            HttpServletRequest request) throws Exception {
    //遍历ViewResolver,调用resolveViewName方法返回View
    for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
            return view;
        }
    }
    return null;
}

ViewResolver接口的实现类如下:
ViewResolver

1.1 AbstractCachingViewResolver(带有缓存的ViewResolver)

AbstractCachingViewResolver是带有缓存的ViewResolver,它每次解析时先从缓存里查找,如果找到视图就返回,如果没有就创建新的视图,而创建新视图的方法由其子类实现,源码如下:

@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
    //判断是否有缓存数据,没有则创建新视图
    if (!isCache()) {
        return createView(viewName, locale);
    } else {
        // 获取View在缓存时的key值,不同的实现类不一样
        Object cacheKey = getCacheKey(viewName, locale);
        // ConcurrentHashMap用于缓存View
        View view = this.viewAccessCache.get(cacheKey);
        // 如果没有找到View则创建,防止并发出现问题,采用双重校验的方式进行安全创建
        if (view == null) {
            synchronized (this.viewCreationCache) {
                view = this.viewCreationCache.get(cacheKey);
                if (view == null) {
                    // Ask the subclass to create the View object.
                    view = createView(viewName, locale);
                    // cacheUnresolved表示是否缓存没有解析成功的,默认为true
                    if (view == null && this.cacheUnresolved) {
                        // 重写View的render方法为空方法,使用没有渲染能力
                        view = UNRESOLVED_VIEW;
                    }
                    // 缓存View
                    if (view != null) {
                        this.viewAccessCache.put(cacheKey, view);
                        this.viewCreationCache.put(cacheKey, view);
                        if (logger.isTraceEnabled()) {
                            logger.trace("Cached view [" + cacheKey + "]");
                        }
                    }
                }
            }
        }
        // 返回View或null
        return (view != UNRESOLVED_VIEW ? view : null);
    }
}

1.1.1 ResourceBundleViewResolver

使用ResourceBundleViewResolver可以让视图解释器支持解析多种视图,它会默认读取classpath路径下的view.properties配置文件,也可通过basename或basenames属性指定一个或多个配置文件,使用方式如下:

视图解析器注册

 <bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename" value="view"/>
 </bean>

properties配置文件view.properties

#指定逻辑视图的视图类型
index.(class)=org.springframework.web.servlet.view.JstlView
#指定逻辑视图的url
index.url=/WEB-INF/views/success.jsp

Controller返回的逻辑视图“index”将会跳转到success.jsp页面。

1.1.2 XmlViewResolver

XmlViewResolver与ResourceBundleViewResolver的作用类似,默认加载/WEB-INF/views.xml,也可以通过location属性指定。

视图解析器注册

    <bean class="org.springframework.web.servlet.view.XmlViewResolver">
        <property name="order" value="2"/>
        <property name="location" value="WEB-INF/spring-view.xml"/>
    </bean>

spring-view.xml:视图bean就是一个普通的Spring bean,在Spring bean配置文件中声明,通过XmlViewResolver加载

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        id : 逻辑视图名
        class : 视图类型
        url : 视图url位置
    -->
    <bean id="index"
          class="org.springframework.web.servlet.view.JstlView">
        <property name="url" value="/WEB-INF/views/index1.jsp" />
    </bean>
</beans>

1.1.3 UrlBasedViewResolver及其子类

UrlBasedViewResolver 提供了拼接 URL 的方式来解析视图,通过 prefix 属性拼接一个前缀,通过 suffix 属性拼接一个后缀,就得到了视图的 URL。还可以加入 redirect: 与 forword: 前缀,使用 redirect: 前缀会调用 HttpServletResponse对象的 sendRedirect() 方法进行重定向,使用 forword: 前缀会利用 RequestDispatcher的forword 方式请求转发到指定的地址。另外,使用时还要指定 viewClass 属性,表示要解析成哪种 View,比如使用FreeMarkerViewResolver时需要指定属性:<property name="viewClass" value="org.springframework.web.servlet.view.freemarker.FreeMarkerView"/>

源码分析:

@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.
    // 如果是redirect:开头的viewName,那么就采用RedirectView
    if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
        String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
        RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
        view.setHosts(getRedirectHosts());
        return applyLifecycleMethods(viewName, view);
    }
    // Check for special "forward:" prefix.
    // 如果是forward:开头的viewName,直接返回InternalResourceView
    if (viewName.startsWith(FORWARD_URL_PREFIX)) {
        String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
        return new InternalResourceView(forwardUrl);
    }
    // 如果不是redirect:和forward:开头就调用父类AbstractUrlBasedView的createView
    //createView会继续调用UrlBasedViewResolver的loadView
    return super.createView(viewName, locale);
}


@Override
protected View loadView(String viewName, Locale locale) throws Exception {
    //构建一个AbstractUrlBasedView view
    AbstractUrlBasedView view = buildView(viewName);
    View result = applyLifecycleMethods(viewName, view);
    //检查资源是否存在并返回
    return (view.checkResource(locale) ? result : null);
}

/**
 * UrlBasedViewResolver的子类重写了buildView也会先调用UrlBasedViewResolver的buildView
 */
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    //根据配置的ViewClass去创建一个`AbstractUrlBasedView`对象
    AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());

    //以下会根据配置设置View的各种属性

    //根据前缀和后缀去设置相对路径
    view.setUrl(getPrefix() + viewName + getSuffix());
    //设置View的ContentType
    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();
    //   true  all Views resolved by this resolver will expose path variables
    //  false - no Views resolved by this resolver will expose path variables
    //  null - individual Views can decide for themselves (this is used by the default)
    if (exposeContextBeansAsAttributes != null) {
        view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
    }
    String[] exposedContextBeanNames = getExposedContextBeanNames();
    if (exposedContextBeanNames != null) {
        view.setExposedContextBeanNames(exposedContextBeanNames);
    }

    return view;
}

查看源码可以发现:UrlBasedViewResolver子类一些相似的特性:

  • 添加了一些新的属性
  • 重写了buildView:先调用UrlBasedViewResolver的buildView方法获取View对象,再给新属性赋值
  • 重写了requiredViewClass:支持的View类型
  • 默认构造器调用setViewClass()方法设置ViewClass属性值
常用的UrlBasedViewResolver支持的视图类型
InternalResourceViewResolverJstlView
FreeMarkerViewResolverFreeMarkerView
GroovyMarkupViewResolverGroovyMarkupView
VelocityViewResolverVelocityView
VelocityLayoutViewResolverVelocityLayoutView
XsltViewResolverXsltView

注意事项:loadView方法返回return (view.checkResource(locale) ? result : null);,其中view.checkResource(locale)表示检查资源文件是否存在,当我们使用多个视图解析器且包含InternalResourceViewResolver时,我们应该将InternalResourceViewResolver的order设置为最大值,因为jstlView没有重写AbstractUrlBasedView的checkResource方法,总是返回true,导致后续的ViewResolver无法执行(可能是个bug)。

1.2 BeanNameViewResolver

BeanNameViewResolver 是通过视图名称去容器中获取对应的View对象,所以在使用前需要将View 对象注册到容器中,源码如下:

@Override
public View resolveViewName(String viewName, Locale locale) throws BeansException {
    ApplicationContext context = getApplicationContext();
    //根据viewName去容器中查找View对象
    if (!context.containsBean(viewName)) {
        if (logger.isDebugEnabled()) {
            logger.debug("No matching bean found for view name '" + viewName + "'");
        }
        // Allow for ViewResolver chaining...
        return null;
    }
    if (!context.isTypeMatch(viewName, View.class)) {
        if (logger.isDebugEnabled()) {
            logger.debug("Found matching bean for view name '" + viewName +
                    "' - to be ignored since 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;
    }
    return context.getBean(viewName, View.class);
}

1.3 ContentNegotiatingViewResolver

ContentNegotiatingViewResolver本身不解析解析视图,而是用来整合所有的ViewResolver类,每次请求都会遍历所有的ViewResolver,然后找到最合适的处理View。源码如下:

@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    //获取Request的MediaType集合
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        //通过遍历ViewResolver,获取所有符合条件的View
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        //遍历所有的SmartView,SmartView默认是RedirectView返回
        //否则,根据MediaType最合适的第一个View返回
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }
    if (this.useNotAcceptableStatusCode) {
        if (logger.isDebugEnabled()) {
            logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
        }
        return NOT_ACCEPTABLE_VIEW;
    }
    else {
        logger.debug("No acceptable view found; returning null");
        return null;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值