5 ViewResolver

ViewResolver 视图解析器能够通过viewName视图名和locale用户区域获取View对象,从而渲染视图。 如果你想使用自定义的一个模板引擎,就得配置上相对应的ViewResolver。

5.1 BeanNameViewResolver 分析

我们还是从最简单的讲起,给你一个最初的印象。BeanNameViewResolver 可谓是真的简单,它会根据viewName从web应用上下文中获取到实现View接口的bean并返回,web应用上下文从哪来?当然是继承相应的接口,被web应用上下文在初始化时注入。

直接看代码即可:

@Override
public View resolveViewName(String viewName, Locale locale) throws BeansException {
    ApplicationContext context = getApplicationContext();
    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);
}

5.2 ViewResolverComposite 分析

凡是以Composite结尾的类,都是包含一组实现类,由其挑选合适的实现类并执行。ViewResolverComposite 也不例外。

代码如下:

@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
    for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
            return view;
        }
    }
    return null;
}

5.3 ContentNegotiatingViewResolver 分析

ContentNegotiatingViewResolver 比 ViewResolverComposite 高级一点,高级在可以通过 MediaType 来筛选出更加合适的View。

5.3.1 ContentNegotiatingViewResolver 初始化分析

ContentNegotiatingViewResolver 是一个带有初始化的类,在初始化中,它会把web应用上下文和父上下文中所有的 ViewResolver 实现类bean加载到自己的属性viewResolvers中,当然,开发者也可以在xml配置中向viewResolvers属性注入bean。同时 ContentNegotiatingViewResolver 还有一个 defaultViews属性,用于存放一组默认的View实现类bean,这个是需要xml配置的。

代码如下:

@Override
protected void initServletContext(ServletContext servletContext) {
    Collection<ViewResolver> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class).values();
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.size());
        for (ViewResolver viewResolver : matchingBeans) {
            if (this != viewResolver) {
                this.viewResolvers.add(viewResolver);
            }
        }
    }
    else {
        for (int i = 0; i < viewResolvers.size(); i++) {
            if (matchingBeans.contains(viewResolvers.get(i))) {
                continue;
            }
            String name = viewResolvers.get(i).getClass().getName() + i;
            getApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolvers.get(i), name);
        }
    }
    if (this.viewResolvers.isEmpty()) {
        logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
                "'viewResolvers' property on the ContentNegotiatingViewResolver");
    }
    AnnotationAwareOrderComparator.sort(this.viewResolvers);
    this.cnmFactoryBean.setServletContext(servletContext);
}

5.3.2 ContentNegotiatingViewResolver 处理请求分析

在前面,我们说了它会通过MediaType来筛选出更合适的View,怎么筛选的呢?来看代码吧

@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        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;
    }
}

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
        throws Exception {
    List<View> candidateViews = new ArrayList<View>();
    for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
            candidateViews.add(view);
        }
        for (MediaType requestedMediaType : requestedMediaTypes) {
            List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
            for (String extension : extensions) {
                String viewNameWithExtension = viewName + "." + extension;
                view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                if (view != null) {
                    candidateViews.add(view);
                }
            }
        }
    }
    if (!CollectionUtils.isEmpty(this.defaultViews)) {
        candidateViews.addAll(this.defaultViews);
    }
    return candidateViews;
}

private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
    for (View candidateView : candidateViews) {
        if (candidateView instanceof SmartView) {
            SmartView smartView = (SmartView) candidateView;
            if (smartView.isRedirectView()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Returning redirect view [" + candidateView + "]");
                }
                return candidateView;
            }
        }
    }
    for (MediaType mediaType : requestedMediaTypes) {
        for (View candidateView : candidateViews) {
            if (StringUtils.hasText(candidateView.getContentType())) {
                MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
                if (mediaType.isCompatibleWith(candidateContentType)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Returning [" + candidateView + "] based on requested media type '" +
                                mediaType + "'");
                    }
                    attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
                    return candidateView;
                }
            }
        }
    }
    return null;
}

代码第5行首先会获取request可接受的并且服务器支持的MediaType,如果没有且配置该bean的时候属性useNotAcceptableStatusCode设置为true,会返回Http状态码为406的结果,否则,返回null,让别的resolver再去处理,属性useNotAcceptableStatusCode默认为false。

代码第7行会调用getCandidateViews方法获取所有返回的View。实际上,除了根据viewName获取View外,还会给viewName加上一些后缀名再去获取。最后,如果defaultViews如果不为空,还会获取defaultViews中的View。

代码第8行调用getBestView方法挑选出从第7行获取的所有View。如何挑选?首先判断有没有重定向,如果有重定向,则返回重定向的view,否则从每一个view中获取ContentType,如果ContentType可以满足request的需求,则返回该view。

5.4 AbstractCachingViewResolver 分析

以上三种 ViewResolver 实现类基本上都是从web应用上下文获取View 或者在调用别的 ViewResolver。而接下来的类则会是我们经常用到的。

AbstractCachingViewResolver 顾名思义,就是带缓存的 ViewResolver,它利用了LinkedHashMap中的一个方法,就是在添加K-V时,会判断要不要删除最早加入的K-V,所以这个类实现的是FIFO类型的缓存,默认缓存数量是1024个。

接下来简单看看代码吧

/** Default maximum number of entries for the view cache: 1024 */
public static final int DEFAULT_CACHE_LIMIT = 1024;

/** The maximum number of entries in the cache */
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;

/** Whether we should refrain from resolving views again if unresolved once */
private boolean cacheUnresolved = true;

/** Fast access cache for Views, returning already cached instances without a global lock */
private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT);

/** Map from view key to View instance, synchronized for View creation */
@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;
                }
            }
        };



@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
    if (!isCache()) {
        return createView(viewName, locale);
    }
    else {
        Object cacheKey = getCacheKey(viewName, locale);
        View view = this.viewAccessCache.get(cacheKey);
        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);
                    if (view == null && this.cacheUnresolved) {
                        view = UNRESOLVED_VIEW;
                    }
                    if (view != null) {
                        this.viewAccessCache.put(cacheKey, view);
                        this.viewCreationCache.put(cacheKey, view);
                        if (logger.isTraceEnabled()) {
                            logger.trace("Cached view [" + cacheKey + "]");
                        }
                    }
                }
            }
        }
        return (view != UNRESOLVED_VIEW ? view : null);
    }
}


/**
 * Create the actual View object.
 * <p>The default implementation delegates to {@link #loadView}.
 * This can be overridden to resolve certain view names in a special fashion,
 * before delegating to the actual {@code loadView} implementation
 * provided by the subclass.
 * @param viewName the name of the view to retrieve
 * @param locale the Locale to retrieve the view for
 * @return the View instance, or {@code null} if not found
 * (optional, to allow for ViewResolver chaining)
 * @throws Exception if the view couldn't be resolved
 * @see #loadView
 */
protected View createView(String viewName, Locale locale) throws Exception {
    return loadView(viewName, locale);
}
/**
 * Subclasses must implement this method, building a View object
 * for the specified view. The returned View objects will be
 * cached by this ViewResolver base class.
 * <p>Subclasses are not forced to support internationalization:
 * A subclass that does not may simply ignore the locale parameter.
 * @param viewName the name of the view to retrieve
 * @param locale the Locale to retrieve the view for
 * @return the View instance, or {@code null} if not found
 * (optional, to allow for ViewResolver chaining)
 * @throws Exception if the view couldn't be resolved
 * @see #resolveViewName
 */
protected abstract View loadView(String viewName, Locale locale) throws Exception;

这个类留给子类的接口是loadView方法。

5.5 XmlViewResolver 分析

XmlViewResolver 是一个比较奇葩的类,实现的功能比较奇葩,他会从指定的xml配置文件中根据beanName来获取View。是不是和 BeanNameViewResolver 很像?不过他俩的区别是:XmlViewResolver 会再生成一个应用上下文单独加载一套xml配置文件,还需要维护自己的应用上下文的生命周期。BeanNameViewResolver 则是简单地直接从现有的应用上下文获取View。

直接来看类的源码吧

public class XmlViewResolver extends AbstractCachingViewResolver
        implements Ordered, InitializingBean, DisposableBean {

    /** Default if no other location is supplied */
    public final static String DEFAULT_LOCATION = "/WEB-INF/views.xml";


    private int order = Integer.MAX_VALUE;  // default: same as non-Ordered

    private Resource location;

    private ConfigurableApplicationContext cachedFactory;


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

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

    /**
     * Set the location of the XML file that defines the view beans.
     * <p>The default is "/WEB-INF/views.xml".
     * @param location the location of the XML file.
     */
    public void setLocation(Resource location) {
        this.location = location;
    }

    /**
     * Pre-initialize the factory from the XML file.
     * Only effective if caching is enabled.
     */
    @Override
    public void afterPropertiesSet() throws BeansException {
        if (isCache()) {
            initFactory();
        }
    }


    /**
     * This implementation returns just the view name,
     * as XmlViewResolver doesn't support localized resolution.
     */
    @Override
    protected Object getCacheKey(String viewName, Locale locale) {
        return viewName;
    }

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

    /**
     * Initialize the view bean factory from the XML file.
     * Synchronized because of access by parallel threads.
     * @throws BeansException in case of initialization errors
     */
    protected synchronized BeanFactory initFactory() throws BeansException {
        if (this.cachedFactory != null) {
            return this.cachedFactory;
        }

        Resource actualLocation = this.location;
        if (actualLocation == null) {
            actualLocation = getApplicationContext().getResource(DEFAULT_LOCATION);
        }

        // Create child ApplicationContext for views.
        GenericWebApplicationContext factory = new GenericWebApplicationContext();
        factory.setParent(getApplicationContext());
        factory.setServletContext(getServletContext());

        // Load XML resource with context-aware entity resolver.
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.setEnvironment(getApplicationContext().getEnvironment());
        reader.setEntityResolver(new ResourceEntityResolver(getApplicationContext()));
        reader.loadBeanDefinitions(actualLocation);

        factory.refresh();

        if (isCache()) {
            this.cachedFactory = factory;
        }
        return factory;
    }


    /**
     * Close the view bean factory on context shutdown.
     */
    @Override
    public void destroy() throws BeansException {
        if (this.cachedFactory != null) {
            this.cachedFactory.close();
        }
    }

}

5.6 ResourceBundleViewResolver 分析

ResourceBundleViewResolver 类允许用户指定一个属性文件,然后在属性文件中配置viewName和对应View实现类,以及viewName和对应的模板页面。

例如在xml这样配置

<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <!-- 设定属性文件名为views 默认也为views -->
    <property name="basename" value="views"></property>
</bean>

接下来在resources文件夹下创建views.properties文件,配置上以下内容

a.class=org.springframework.web.servlet.view.JstlView
a.url=/WEB-INF/jsp/a.jsp
b.class=org.springframework.web.servlet.view.JstlView
b.url=/WEB-INF/jsp/b.jsp

即可实现viewName为a时使用JstlView,模板为/WEB-INF/jsp/a.jsp。

5.7 UrlBasedViewResolver 分析

AbstractCachingViewResolver 的上两个实现类实际上用的频率很少,用的比较多的就是 UrlBasedViewResolver 的子类,在这里,我们先看看 UrlBasedViewResolver 都做了些什么事。

首先,需要设置一个viewClass,而这个class必须继承 requiredViewClass 方法获取到的类,在这个类中,requiredViewClass 方法获取到的类是 AbstractUrlBasedView

代码如下:

/**
 * Set the view class that should be used to create views.
 * @param viewClass class that is assignable to the required view class
 * (by default, AbstractUrlBasedView)
 * @see AbstractUrlBasedView
 */
public void setViewClass(Class<?> viewClass) {
    if (viewClass == null || !requiredViewClass().isAssignableFrom(viewClass)) {
        throw new IllegalArgumentException(
                "Given view class [" + (viewClass != null ? viewClass.getName() : null) +
                "] is not of type [" + requiredViewClass().getName() + "]");
    }
    this.viewClass = viewClass;
}

/**
 * Return the view class to be used to create views.
 */
protected Class<?> getViewClass() {
    return this.viewClass;
}

/**
 * Return the required type of view for this resolver.
 * This implementation returns AbstractUrlBasedView.
 * @see AbstractUrlBasedView
 */
protected Class<?> requiredViewClass() {
    return AbstractUrlBasedView.class;
}

@Override
protected void initApplicationContext() {
    super.initApplicationContext();
    if (getViewClass() == null) {
        throw new IllegalArgumentException("Property 'viewClass' is required");
    }
}

其次,它还重写了 AbstractCachingViewResolver 类的createView方法,在方法中先判断是否有匹配的viewName,在判断是不是重定向或forward,最后再调用父类方法。

代码如下:

/**
 * Overridden to implement check for "redirect:" prefix.
 * <p>Not possible in {@code loadView}, since overridden
 * {@code loadView} versions in subclasses might rely on the
 * superclass always creating instances of the required view class.
 * @see #loadView
 * @see #requiredViewClass
 */
@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)) {
        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.
    if (viewName.startsWith(FORWARD_URL_PREFIX)) {
        String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
        return new InternalResourceView(forwardUrl);
    }
    // Else fall back to superclass implementation: calling loadView.
    return super.createView(viewName, locale);
}

/**
 * Indicates whether or not this {@link org.springframework.web.servlet.ViewResolver} can
 * handle the supplied view name. If not, {@link #createView(String, java.util.Locale)} will
 * return {@code null}. The default implementation checks against the configured
 * {@link #setViewNames view names}.
 * @param viewName the name of the view to retrieve
 * @param locale the Locale to retrieve the view for
 * @return whether this resolver applies to the specified view
 * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
 */
protected boolean canHandle(String viewName, Locale locale) {
    String[] viewNames = getViewNames();
    return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
}

重定向和forward这里暂时不考虑,我们先看看正常的怎么调用。

UrlBasedViewResolver 允许设置prefix和suffix,使得开发者返回viewName时,只用返回特征值即可。例如文件在/WEB-INF/static/下,文件结尾是.html,那么设置prefix为/WEB-INF/static/,suffix为.html,如果想获取/WEB-INF/static/a.html,开发者只用设置viewName为a即可。

UrlBasedViewResolver 重写了loadView方法,实现了根据文件名加载View的基本逻辑。

/**
 * Delegates to {@code buildView} for creating a new instance of the
 * specified view class, and applies the following Spring lifecycle methods
 * (as supported by the generic Spring bean factory):
 * <ul>
 * <li>ApplicationContextAware's {@code setApplicationContext}
 * <li>InitializingBean's {@code afterPropertiesSet}
 * </ul>
 * @param viewName the name of the view to retrieve
 * @return the View instance
 * @throws Exception if the view couldn't be resolved
 * @see #buildView(String)
 * @see org.springframework.context.ApplicationContextAware#setApplicationContext
 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
 */
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
    AbstractUrlBasedView view = buildView(viewName);
    View result = applyLifecycleMethods(viewName, view);
    return (view.checkResource(locale) ? result : null);
}

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

/**
 * Creates a new View instance of the specified view class and configures it.
 * Does <i>not</i> perform any lookup for pre-defined View instances.
 * <p>Spring lifecycle methods as defined by the bean container do not have to
 * be called here; those will be applied by the {@code loadView} method
 * after this method returns.
 * <p>Subclasses will typically call {@code super.buildView(viewName)}
 * first, before setting further properties themselves. {@code loadView}
 * will then apply Spring lifecycle methods at the end of this process.
 * @param viewName the name of the view to build
 * @return the View instance
 * @throws Exception if the view couldn't be resolved
 * @see #loadView(String, java.util.Locale)
 */
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
    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;
}

5.8 InternalResourceViewResolver 分析

InternalResourceViewResolver ?内部资源视图解析器?干什么的呢?实际上,这是一个利用jstl标准标签库实现模板渲染的视图解析器。只要类路径中存在javax.servlet.jsp.jstl.core.Config类,说明jstl标准标签库可以使用。来看看它是如何设置viewClass的吧。

/**
 * Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}:
 * by default {@link InternalResourceView}, or {@link JstlView} if the JSTL API
 * is present.
 */
public InternalResourceViewResolver() {
    Class<?> viewClass = requiredViewClass();
    if (InternalResourceView.class == viewClass && jstlPresent) {
        viewClass = JstlView.class;
    }
    setViewClass(viewClass);
}

/**
 * A convenience constructor that allows for specifying {@link #setPrefix prefix}
 * and {@link #setSuffix suffix} as constructor arguments.
 * @param prefix the prefix that gets prepended to view names when building a URL
 * @param suffix the suffix that gets appended to view names when building a URL
 * @since 4.3
 */
public InternalResourceViewResolver(String prefix, String suffix) {
    this();
    setPrefix(prefix);
    setSuffix(suffix);
}

/**
 * This resolver requires {@link InternalResourceView}.
 */
@Override
protected Class<?> requiredViewClass() {
    return InternalResourceView.class;
}

5.9 AbstractTemplateViewResolver 分析

使用过jsp当模板引擎后,很多人觉得可能有点复杂,那我要是想使用别的模板引擎呢? 对喽,这里提供了一个类供模板引擎使用。这个类的作用是什么?因为模板引擎实质上就是修改字符串,跟httpRequest没有关系,所以如果要使用模板引擎,就要判断request中的属性、session中的属性要不要给模板引擎提供。AbstractTemplateViewResolver 就实现了这样的功能,它对应的View是 AbstractTemplateView。

5.10 FreeMarkerViewResolver 分析

FreeMarkerViewResolver 继承了 AbstractTemplateViewResolver,实际上,FreeMarkerViewResolver提供了View的实现类 FreeMarkerView 供 UrlBasedViewResolver 创建View。

5.11 小结

看完这么多 ViewResolver 的实现,我们发现实际上通过viewName获取View实现类分为这几种。

  • 第一种是直接从web应用上下文找name为viewName的bean。
  • 第二种是从指定配置文件加载应用上下文,再在上下文中查找name为viewName的bean。
  • 第三种是通过判断viewName对应的文件是否存在来决定是否适用该View。

6 View

第5章讲完了 ViewResolver,我们发现一个干事的 ViewResolver 后边对应的时一个或多个View实现类。因此,我们来了解一下View及其实现类吧。

6.1 AbstractView 分析

6.1.1 AbstractView 初始化分析

首先,AbstractView 有两个常量,分别是:

/** Default content type. Overridable as bean property. */
public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1";

/** Initial size for the temporary output byte array (if any) */
private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 4096;

第一个常量DEFAULT_CONTENT_TYPE用于填充默认的ContentType,第二个常量OUTPUT_BYTE_ARRAY_INITIAL_SIZE是生成ByteArrayOutputStream的默认初始值。

其次,AbstractView 实现了BeanNameAware接口,可以得到自身的name。

6.1.2 AbstractView 处理请求分析

AbstractView 重写了render方法,并将代码分为三步完成请求。

/**
 * Prepares the view given the specified model, merging it with static
 * attributes and a RequestContext attribute, if necessary.
 * Delegates to renderMergedOutputModel for the actual rendering.
 * @see #renderMergedOutputModel
 */
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (logger.isTraceEnabled()) {
        logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
            " and static attributes " + this.staticAttributes);
    }

    Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
    prepareResponse(request, response);
    renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

/**
 * Creates a combined output Map (never {@code null}) that includes dynamic values and static attributes.
 * Dynamic values take precedence over static attributes.
 */
protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request,
        HttpServletResponse response) {
    @SuppressWarnings("unchecked")
    Map<String, Object> pathVars = (this.exposePathVariables ?
            (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null);
    // Consolidate static and dynamic model attributes.
    int size = this.staticAttributes.size();
    size += (model != null ? model.size() : 0);
    size += (pathVars != null ? pathVars.size() : 0);
    Map<String, Object> mergedModel = new LinkedHashMap<String, Object>(size);
    mergedModel.putAll(this.staticAttributes);
    if (pathVars != null) {
        mergedModel.putAll(pathVars);
    }
    if (model != null) {
        mergedModel.putAll(model);
    }
    // Expose RequestContext?
    if (this.requestContextAttribute != null) {
        mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
    }
    return mergedModel;
}


/**
 * Prepare the given response for rendering.
 * <p>The default implementation applies a workaround for an IE bug
 * when sending download content via HTTPS.
 * @param request current HTTP request
 * @param response current HTTP response
 */
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
    if (generatesDownloadContent()) {
        response.setHeader("Pragma", "private");
        response.setHeader("Cache-Control", "private, must-revalidate");
    }
}


/**
 * Subclasses must implement this method to actually render the view.
 * <p>The first step will be preparing the request: In the JSP case,
 * this would mean setting model objects as request attributes.
 * The second step will be the actual rendering of the view,
 * for example including the JSP via a RequestDispatcher.
 * @param model combined output Map (never {@code null}),
 * with dynamic values taking precedence over static attributes
 * @param request current HTTP request
 * @param response current HTTP response
 * @throws Exception if rendering failed
 */
protected abstract void renderMergedOutputModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

首先第一步是生成一个最终的model,在 AbstractView 中,会将其Map类型的属性 staticAttributes 合并到传入的model中。如果允许暴露 RequestContext,则将 RequestContext传入。

其次是准备响应,这里一般不会运行什么结果。

最后是将合并后的model交给子类渲染。

6.1.3 AbstractView 其他

除以上处理请求方法外,AbstractView 还提供了一些设置请求头、把ByteArrayOutputStream写入response等等供子类调用的方法,方便子类使用。

6.2 AbstractUrlBasedView 分析

AbstractUrlBasedView 是最简单的View之一,它提供了一个url属性。还有个checkResource方法,这个方法就是在 UrlBasedViewResolver 类中loadView方法内最终检测文件是否存在的方法。

6.3 InternalResourceView 和 JstlView

我们在5.8节讲到 InternalResourceViewResolver 使用的View就是InternalResourceView和JstlView。InternalResourceView 能够渲染jsp页面,代码如下:

/**
 * Render the internal resource given the specified model.
 * This includes setting the model as request attributes.
 */
@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.
    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 resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
        }
        rd.include(request, response);
    }
    else {
        // Note: The forwarded resource is supposed to determine the content type itself.
        if (logger.isDebugEnabled()) {
            logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
        }
        rd.forward(request, response);
    }
}

如上代码,首先将model中的K-V值放到了request的属性中,接下来调用RequestDispatcher的include或forward方法执行渲染,这里就不做细究了。

如果项目中引入了JSTL标准标签库,则会使用 JstlView,引入一些JSTL需要使用的数据。

6.4 RedirectView 分析

由于在 UrlBasedViewResolver 中重写了 createView方法,判断如果viewName是以redirect开头的,就会重定向到别的页面,使用的就是 RedirectView。

在redirect时,会将相关数据保存在 FlashMap中,这个咱们在DispatcherServlet讲过,用于保存重定向前相关数据。保存好相关数据后,再向response中写入Header信息,K为Location,V为跳转的URL。

相关方法如下:

/**
 * Convert model to request parameters and redirect to the given URL.
 * @see #appendQueryProperties
 * @see #sendRedirect
 */
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    String targetUrl = createTargetUrl(model, request);
    targetUrl = updateTargetUrl(targetUrl, model, request, response);
    FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
    if (!CollectionUtils.isEmpty(flashMap)) {
        UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
        flashMap.setTargetRequestPath(uriComponents.getPath());
        flashMap.addTargetRequestParams(uriComponents.getQueryParams());
        FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
        if (flashMapManager == null) {
            throw new IllegalStateException("FlashMapManager not found despite output FlashMap having been set");
        }
        flashMapManager.saveOutputFlashMap(flashMap, request, response);
    }
    sendRedirect(request, response, targetUrl, this.http10Compatible);
}

/**
 * Send a redirect back to the HTTP client
 * @param request current HTTP request (allows for reacting to request method)
 * @param response current HTTP response (for sending response headers)
 * @param targetUrl the target URL to redirect to
 * @param http10Compatible whether to stay compatible with HTTP 1.0 clients
 * @throws IOException if thrown by response methods
 */
protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
        String targetUrl, boolean http10Compatible) throws IOException {
    String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl));
    if (http10Compatible) {
        HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
        if (this.statusCode != null) {
            response.setStatus(this.statusCode.value());
            response.setHeader("Location", encodedURL);
        }
        else if (attributeStatusCode != null) {
            response.setStatus(attributeStatusCode.value());
            response.setHeader("Location", encodedURL);
        }
        else {
            // Send status code 302 by default.
            response.sendRedirect(encodedURL);
        }
    }
    else {
        HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
        response.setStatus(statusCode.value());
        response.setHeader("Location", encodedURL);
    }
}

6.5 AbstractTemplateView 分析

AbstractTemplateView 是在 AbstractTemplateViewResolver 中使用的,抽象模板视图的作用是决定要不要将HTTPrequest中的相关信息写入model。还记得6.3节InternalResourceView和JstlView吗?在那些View中,是需要将model写入到request的属性中。而模板引擎在非web程序中也能使用,和request一点关系都没有,所以,为了能让模板引擎能够拿到request中的数据,设置了该抽象类,用于决定要不要将request中的信息写入到model中。

以下是相关属性:

/**
 * Variable name of the RequestContext instance in the template model,
 * available to Spring's macros: e.g. for creating BindStatus objects.
 */
public static final String SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE = "springMacroRequestContext";

// 是否将request属性中的信息写入到model
private boolean exposeRequestAttributes = false;

// 是否允许request的信息覆盖model的
private boolean allowRequestOverride = false;

// 是否将session中的信息写入到model
private boolean exposeSessionAttributes = false;

// 是否允许session的信息覆盖model的
private boolean allowSessionOverride = false;

// 是否暴露spring上下文
private boolean exposeSpringMacroHelpers = true;

了解了这些信息,来看看相关代码吧

@Override
protected final void renderMergedOutputModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (this.exposeRequestAttributes) {
        for (Enumeration<String> en = request.getAttributeNames(); en.hasMoreElements();) {
            String attribute = en.nextElement();
            if (model.containsKey(attribute) && !this.allowRequestOverride) {
                throw new ServletException("Cannot expose request attribute '" + attribute +
                    "' because of an existing model object of the same name");
            }
            Object attributeValue = request.getAttribute(attribute);
            if (logger.isDebugEnabled()) {
                logger.debug("Exposing request attribute '" + attribute +
                        "' with value [" + attributeValue + "] to model");
            }
            model.put(attribute, attributeValue);
        }
    }
    if (this.exposeSessionAttributes) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            for (Enumeration<String> en = session.getAttributeNames(); en.hasMoreElements();) {
                String attribute = en.nextElement();
                if (model.containsKey(attribute) && !this.allowSessionOverride) {
                    throw new ServletException("Cannot expose session attribute '" + attribute +
                        "' because of an existing model object of the same name");
                }
                Object attributeValue = session.getAttribute(attribute);
                if (logger.isDebugEnabled()) {
                    logger.debug("Exposing session attribute '" + attribute +
                            "' with value [" + attributeValue + "] to model");
                }
                model.put(attribute, attributeValue);
            }
        }
    }
    if (this.exposeSpringMacroHelpers) {
        if (model.containsKey(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE)) {
            throw new ServletException(
                    "Cannot expose bind macro helper '" + SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE +
                    "' because of an existing model object of the same name");
        }
        // Expose RequestContext instance for Spring macros.
        model.put(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE,
                new RequestContext(request, response, getServletContext(), model));
    }
    applyContentType(response);
    renderMergedTemplateModel(model, request, response);
}

/**
 * Apply this view's content type as specified in the "contentType"
 * bean property to the given response.
 * <p>Only applies the view's contentType if no content type has been
 * set on the response before. This allows handlers to override the
 * default content type beforehand.
 * @param response current HTTP response
 * @see #setContentType
 */
protected void applyContentType(HttpServletResponse response)   {
    if (response.getContentType() == null) {
        response.setContentType(getContentType());
    }
}

/**
 * Subclasses must implement this method to actually render the view.
 * @param model combined output Map, with request attributes and
 * session attributes merged into it if required
 * @param request current HTTP request
 * @param response current HTTP response
 * @throws Exception if rendering failed
 */
protected abstract void renderMergedTemplateModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

做完了这些数据整合后,把渲染视图的任务再次交给子类。

6.6 FreeMarkerView 分析

我们在开发中一般会使用的视图解析器有各种模板引擎或者jsp等等,在 AbstractUrlBasedView 和 AbstractTemplateView中,分别处理好了url和model,接下来就该真正的视图工作了。这里我们试着分析一下 FreeMarkerView,其他的模板引擎工作原理也差不多。

6.6.1 FreeMarkerView 初始化分析

FreeMarkerView 初始化会从web应用上下文中加载相关配置。

/**
 * Invoked on startup. Looks for a single FreeMarkerConfig bean to
 * find the relevant Configuration for this factory.
 * <p>Checks that the template for the default Locale can be found:
 * FreeMarker will check non-Locale-specific templates if a
 * locale-specific one is not found.
 * @see freemarker.cache.TemplateCache#getTemplate
 */
@Override
protected void initServletContext(ServletContext servletContext) throws BeansException {
    if (getConfiguration() != null) {
        this.taglibFactory = new TaglibFactory(servletContext);
    }
    else {
        FreeMarkerConfig config = autodetectConfiguration();
        setConfiguration(config.getConfiguration());
        this.taglibFactory = config.getTaglibFactory();
    }
    GenericServlet servlet = new GenericServletAdapter();
    try {
        servlet.init(new DelegatingServletConfig());
    }
    catch (ServletException ex) {
        throw new BeanInitializationException("Initialization of GenericServlet adapter failed", ex);
    }
    this.servletContextHashModel = new ServletContextHashModel(servlet, getObjectWrapper());
}

/**
 * Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext.
 * @return the Configuration instance to use for FreeMarkerViews
 * @throws BeansException if no Configuration instance could be found
 * @see #getApplicationContext
 * @see #setConfiguration
 */
protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
    try {
        return BeanFactoryUtils.beanOfTypeIncludingAncestors(
                getApplicationContext(), FreeMarkerConfig.class, true, false);
    }
    catch (NoSuchBeanDefinitionException ex) {
        throw new ApplicationContextException(
                "Must define a single FreeMarkerConfig bean in this web application context " +
                "(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
                "This bean may be given any name.", ex);
    }
}

这个 FreeMarkerConfig 类就是需要我们在xml中配置的FreeMarker配置对象,其中可能有模板路径,文件字符格式等等。

6.6.2 FreeMarkerView 处理请求分析

之前讲到 UrlBasedViewResolver 在loadView方法中会调用 AbstractUrlBasedView 的checkResource方法判断资源是否存在。

/**
 * Check that the FreeMarker template used for this view exists and is valid.
 * <p>Can be overridden to customize the behavior, for example in case of
 * multiple templates to be rendered into a single view.
 */
@Override
public boolean checkResource(Locale locale) throws Exception {
    try {
        // Check that we can get the template, even if we might subsequently get it again.
        getTemplate(getUrl(), locale);
        return true;
    }
    catch (FileNotFoundException ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("No FreeMarker view found for URL: " + getUrl());
        }
        return false;
    }
    catch (ParseException ex) {
        throw new ApplicationContextException(
                "Failed to parse FreeMarker template for URL [" +  getUrl() + "]", ex);
    }
    catch (IOException ex) {
        throw new ApplicationContextException(
                "Could not load FreeMarker template for URL [" + getUrl() + "]", ex);
    }
}

而最终的实现就是判断是否能获得相关模板文件,如果能获得就返回true。

当确认能够渲染后,最终会调用renderMergedTemplateModel方法进行最终的渲染,该方法又会调用doRender方法执行真正的渲染,

/**
 * Process the model map by merging it with the FreeMarker template.
 * Output is directed to the servlet response.
 * <p>This method can be overridden if custom behavior is needed.
 */
@Override
protected void renderMergedTemplateModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    exposeHelpers(model, request);
    doRender(model, request, response);
}

/**
 * Render the FreeMarker view to the given response, using the given model
 * map which contains the complete template model to use.
 * <p>The default implementation renders the template specified by the "url"
 * bean property, retrieved via {@code getTemplate}. It delegates to the
 * {@code processTemplate} method to merge the template instance with
 * the given template model.
 * <p>Adds the standard Freemarker hash models to the model: request parameters,
 * request, session and application (ServletContext), as well as the JSP tag
 * library hash model.
 * <p>Can be overridden to customize the behavior, for example to render
 * multiple templates into a single view.
 * @param model the model to use for rendering
 * @param request current HTTP request
 * @param response current servlet response
 * @throws IOException if the template file could not be retrieved
 * @throws Exception if rendering failed
 * @see #setUrl
 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
 * @see #getTemplate(java.util.Locale)
 * @see #processTemplate
 * @see freemarker.ext.servlet.FreemarkerServlet
 */
protected void doRender(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // Expose model to JSP tags (as request attributes).
    exposeModelAsRequestAttributes(model, request);
    // Expose all standard FreeMarker hash models.
    SimpleHash fmModel = buildTemplateModel(model, request, response);
    if (logger.isDebugEnabled()) {
        logger.debug("Rendering FreeMarker template [" + getUrl() + "] in FreeMarkerView '" + getBeanName() + "'");
    }
    // Grab the locale-specific version of the template.
    Locale locale = RequestContextUtils.getLocale(request);
    processTemplate(getTemplate(locale), fmModel, response);
}

/**
 * Process the FreeMarker template to the servlet response.
 * <p>Can be overridden to customize the behavior.
 * @param template the template to process
 * @param model the model for the template
 * @param response servlet response (use this to get the OutputStream or Writer)
 * @throws IOException if the template file could not be retrieved
 * @throws TemplateException if thrown by FreeMarker
 * @see freemarker.template.Template#process(Object, java.io.Writer)
 */
protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response)
        throws IOException, TemplateException {
    template.process(model, response.getWriter());
}

6.7 小结

View接口相比较而言还算简单,但其中门门还真不少,谁能想到 ViewResolver 竟然需要调用 AbstractUrlBasedView 的checkResource方法判断这个View是否能渲染数据。AbstractUrlBasedView 使得View的获取从通过beanName的方式转换为通过View自身的判断,真是一个神转折。

标签: java, springmvc, spring

添加新评论