SpringMVC常见组件之ViewResolver分析

简介: 本文我们尝试总结分析SpringMVC体系中的视图解析器-ViewResolver。其根据name解析视图View,通常鼓励实现类考虑国际化策略实现。

本文我们尝试总结分析SpringMVC体系中的视图解析器-ViewResolver。其根据name解析视图View,通常鼓励实现类考虑国际化策略实现。


前置流程

DispatcherServlet#doDispatch--
DispatcherServlet#processDispatchResult--
DispatcherServlet#render--
DispatcherServlet#resolveViewName--
--view.render(mv.getModelInternal(), request, response);


DispatcherServlet中根据视图名称获取view代码如下所示,遍历循环viewResolvers尝试获取一个不为null的view。

@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
    Locale locale, HttpServletRequest request) throws Exception {
  if (this.viewResolvers != null) {
    for (ViewResolver viewResolver : this.viewResolvers) {
      View view = viewResolver.resolveViewName(viewName, locale);
      if (view != null) {
        return view;
      }
    }
  }
  return null;
}


ViewResolver 获取具体的View,SpringMVC调用View的Render方法来渲染视图-将model数据与视图结合在一起写入到response中。

ViewResolver 接口只有一个抽象方法如下所示:

@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;


locale表示鼓励你支持国际化策略。如果当前实现类不能解析视图,那么请返回null以使得视图解析器链可以流转下去。

【1】ViewResolver

首先我们看下其家族树图示:



ViewResolver实现主要分为四大类:


  • ① ContentNegotiatingViewResolver
  • ② AbstractCachingViewResolver
  • ③ ViewResolverComposite
  • ④ BeanNameViewResolver



子类视图解析器如下


ViewResolverComposite
AbstractCachingViewResolver
ResourceBundleViewResolver
XmlViewResolver
UrlBasedViewResolver
TilesViewResolver
ScriptTemplateViewResolver
InternalResourceViewResolver
XsltViewResolver
AbstractTemplateViewResolver
GroovyMarkupViewResolver
FreeMarkerViewResolver
ThymeleafViewResolver
AjaxThymeleafViewResolver
ContentNegotiatingViewResolver
BeanNameViewResolver

下面我们按照类别来分析其子类。

【1】ViewResolverComposite


为什么先说这个呢?ViewResolverComposite本身不是一个视图解析器,其使用了组合模式内部维护了一个private final List viewResolvers = new ArrayList<>();,将具体resolveViewName动作委派了具体的ViewResolver实例(委派模式)。是不是和HandlerMethodArgumentResolverComposite、HandlerMethodReturnValueHandlerComposite类型?


如下图所示,其实现了ViewResolver, Ordered, InitializingBean,ApplicationContextAware, ServletContextAware接口。这意味着其在实例化过程中会进行诸多操作:

Ordered,表示其可以排序;

InitializingBean,实例化时调用其afterPropertiesSet方法;

ApplicationContextAware,实例化时会调用setApplicationContext方法;

ServletContextAware,实例化时会调用setServletContext方法。


87a4435c35a3451e9bf90f00189a0747.png


我们从其源码也可以知晓,ViewResolverComposite在实例化过程中对内部维护的ViewResolver 做了增强工作

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  for (ViewResolver viewResolver : this.viewResolvers) {
    if (viewResolver instanceof ApplicationContextAware) {
      ((ApplicationContextAware)viewResolver).setApplicationContext(applicationContext);
    }
  }
}
@Override
public void setServletContext(ServletContext servletContext) {
  for (ViewResolver viewResolver : this.viewResolvers) {
    if (viewResolver instanceof ServletContextAware) {
      ((ServletContextAware)viewResolver).setServletContext(servletContext);
    }
  }
}
@Override
public void afterPropertiesSet() throws Exception {
  for (ViewResolver viewResolver : this.viewResolvers) {
    if (viewResolver instanceof InitializingBean) {
      ((InitializingBean) viewResolver).afterPropertiesSet();
    }
  }
}


如下代码所示,其解析视图的方法很简单:遍历viewResolvers,然后使用具体的viewResolver 去解析,返回view或者null。

@Override
@Nullable
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;
}

【2】BeanNameViewResolver

BeanNameViewResolver将视图名字解析为bean name,找到上下文中该bean实例返回。bean需要实现View接口。

① 解析视图

其解析视图的方法如下所示,尝试根据viewName从ApplicationContext 找到对应的bean。如果ApplicationContext 中没有该bean直接返回null;如果该bean不是View类型也返回null;最后返回bean的实例。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
  ApplicationContext context = obtainApplicationContext();
  if (!context.containsBean(viewName)) {
    // Allow for ViewResolver chaining...
    return 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;
  }
  return context.getBean(viewName, View.class);
}


② 实践

① 配置解析器

如下所示,配置两个视图解析器,SpringMVC.xml配置:

<!-- 配置视图解析器: 如何把 handler 方法返回值解析为实际的物理视图 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="prefix" value="/WEB-INF/views/"></property>
  <property name="suffix" value=".jsp"></property>
</bean>
<!-- ****** 配置视图  BeanNameViewResolver 解析器: 使用视图的名字来解析视图 ************ -->
<!-- 通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
  <property name="order" value="10"></property>
</bean>

InternalResourceViewResolver的order属性默认为Integer的最大值。

② 自定义View实现类

实例代码如下:

@Component
public class HelloView implements View{
  /*自定义视图,需配置BeanNameViewResolver*/
  public String getContentType() {
    return "text/html";
  }
  //视图渲染,转发前不可避免的重要一步!
  public void render(Map<String, ?> model, HttpServletRequest request,
      HttpServletResponse response) throws Exception {
    /*打印响应信息到浏览器页面*/
    response.getWriter().print("hello view, time: " + new Date());
  }
}


测试方法:

@RequestMapping("/testView")
public String testView(){
  System.out.println("testView");
  return "helloView";
  //返回bean名字
}


result as follows :

同理,可以通过继承AbstractExcelView 并实现其buildExcelDocument方法,来渲染Excel。如下所示:

public class ExcelView extends AbstractExcelView {
  @Override
  protected void buildExcelDocument(Map<String, Object> model,
      HSSFWorkbook workbook, HttpServletRequest request,
      HttpServletResponse response) throws Exception {
    // TODO Auto-generated method stub    
  }
}

【3】ContentNegotiatingViewResolver

内容协商视图解析器。简单来说就是根据viewName和Media Type(依据请求的文件名称或者Accept头)找到候选的View再根据MediaType和View的ContentType确定最优View。


如下所示,ContentNegotiatingViewResolver维护了集合defaultViews和viewResolvers表示其并不会进行“解析View”的具体动作。解析任务将会被委派给具体的ViewResolver执行。defaultViews集合提供了入口,用户可以通过该属性注入自定义视图解析器。

@Nullable
private List<View> defaultViews;
@Nullable
private List<ViewResolver> viewResolvers;

① 继承类分析


如下图所示,ContentNegotiatingViewResolver继承了WebApplicationObjectSupport并实现了ViewResolver, Ordered, InitializingBean接口。那么这里我们要有个清晰认知,这些类/接口意味着什么。



实现了Order接口意味着其拥有order属性,内部viewResolver可以按照顺序执行。

实现了InitializingBean接口


如下图所示,如果bean实现了InitializingBean接口,那么在初始化过程中一定会调用其afterPropertiesSet方法。6eae1e03b3ad4a659f22c0a9b818541b.png



我们看下其afterPropertiesSet方法。如下所示根据cnmFactoryBean的build方法获取一个contentNegotiationManager 实例。

@Override
public void afterPropertiesSet() {
  if (this.contentNegotiationManager == null) {
    this.contentNegotiationManager = this.cnmFactoryBean.build();
  }
  if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
    logger.warn("No ViewResolvers configured");
  }
}

cnmFactoryBean是什么呢?其是一个工厂bean-ContentNegotiationManagerFactoryBean。主要是生产一个contentNegotiationManager并使用ContentNegotiationStrategy配置contentNegotiationManager。如下所示其实现了FactoryBean、ServletContextAware以及InitializingBean 接口。

public class ContentNegotiationManagerFactoryBean
    implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
    //...
}   

ServletContextAware接口提供了方法 void setServletContext(ServletContext servletContext);

InitializingBean接口提供了方法void afterPropertiesSet() throws Exception;在bean属性被设置,并调用了一系列诸如BeanFactoryAware、ApplicationContextAware等提供的方法后,再调用afterPropertiesSet方法。

FactoryBean接口提供了方法T getObject() throws Exception; && Class getObjectType(); &&default boolean isSingleton() {return true;},主要用来生产一个object。


继承自WebApplicationObjectSupport


WebApplicationObjectSupport继承自ApplicationObjectSupport并实现了ServletContextAware接口。


ServletContextAware接口提供了void setServletContext(ServletContext servletContext);方法用来设置ServletContext。该方法执行时机在在bean属性设置后、ApplicationContextAware's setApplicationContext方法后但是在init方法前如InitializingBean's afterPropertiesSet及其他自定义初始化方法前。


ApplicationObjectSupport实现了ApplicationContextAware接口。ApplicationContextAware接口提供了方法void setApplicationContext(ApplicationContext applicationContext) throws BeansException;用来设置ApplicationContext 。该方法执行时机在bean属性设置后、init方法调用前如InitializingBean#afterPropertiesSet()方法及其他自定义初始化方法。注意,其同样在ResourceLoaderAware#setResourceLoader、ApplicationEventPublisherAware#setApplicationEventPublisher以及MessageSourceAware后,如果实现这些Aware接口。


ApplicationObjectSupport提供了方法protected void initApplicationContext() throws BeansException{ }让子类实现,WebApplicationObjectSupport实现了该方法。如下所示其首先调用父类的initApplicationContext方法,然后获取servletContext 交给子类的initServletContext方法。

@Override
protected void initApplicationContext(ApplicationContext context) {
  super.initApplicationContext(context);
  if (this.servletContext == null && context instanceof WebApplicationContext) {
    this.servletContext = ((WebApplicationContext) context).getServletContext();
    if (this.servletContext != null) {
      initServletContext(this.servletContext);
    }
  }
}

ContentNegotiatingViewResolver实现了initServletContext方法。

@Override
protected void initServletContext(ServletContext servletContext) {
  Collection<ViewResolver> matchingBeans =
      BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
  if (this.viewResolvers == null) {
    this.viewResolvers = new ArrayList<>(matchingBeans.size());
    for (ViewResolver viewResolver : matchingBeans) {
      if (this != viewResolver) {
        this.viewResolvers.add(viewResolver);
      }
    }
  }
  else {
    for (int i = 0; i < this.viewResolvers.size(); i++) {
      ViewResolver vr = this.viewResolvers.get(i);
      if (matchingBeans.contains(vr)) {
        continue;
      }
      String name = vr.getClass().getName() + i;
      //进行bean的初始化过程
      obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
    }
  }
  // 根据order进行排序
  AnnotationAwareOrderComparator.sort(this.viewResolvers);
  // 设置上下文
  this.cnmFactoryBean.setServletContext(servletContext);
}


方法如上所示,从ApplicationContext拿到ViewResolver放到集合Collection matchingBeans中。如果自身成员属性viewResolvers为null,那么把matchingBeans中的ViewResolver放到自身成员属性viewResolvers中(除了当前对象哦)。如果自身成员属性viewResolvers不为null,那么遍历调用除了自身之外的ViewResolver的initializeBean方法,也就是bean的初始化过程。

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
  if (System.getSecurityManager() != null) {
    AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
      invokeAwareMethods(beanName, bean);
      return null;
    }, getAccessControlContext());
  }
  else {
    invokeAwareMethods(beanName, bean);
  }
  Object wrappedBean = bean;
  if (mbd == null || !mbd.isSynthetic()) {
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
  }
  try {
    invokeInitMethods(beanName, wrappedBean, mbd);
  }
  catch (Throwable ex) {
    throw new BeanCreationException(
        (mbd != null ? mbd.getResourceDescription() : null),
        beanName, "Invocation of init method failed", ex);
  }
  if (mbd == null || !mbd.isSynthetic()) {
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
  }
  return wrappedBean;
}

这里主要核心步骤如下:


① invokeAwareMethods(beanName, bean);如BeanFactoryAware

② applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);Bean后置处理器的前置方法

③ invokeInitMethods(beanName, wrappedBean, mbd);反射调用bean的init方法以及自定义初始化方法。这里如果判断是InitializingBean类型,则会先调用其afterPropertiesSet方法。

④ applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);Bean后置处理器的后置方法

② 视图解析

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 获取请求属性对象,实现类诸如 ServletRequestAttributes
  RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
  Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
  // 获取请求接受的,并且响应能够生产的MediaType
  List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
  // 获取候选View,然后得到一个BestView
  if (requestedMediaTypes != null) {
    List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
    View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
    if (bestView != null) {
      return bestView;
    }
  }
  // 下面是异常逻辑处理
  String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
      " given " + requestedMediaTypes.toString() : "";
  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;
  }
}


这里我们主要关心两点:getCandidateViews与getBestView。


获取候选View的逻辑如下所示,拿到能直接解析viewName的viewResolver解析后的View放入List candidateViews中。然后遍历requestedMediaTypes,根据一个个viewName + '.' + extension尝试找到viewResolver解析后的View放入candidateViews 中。

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
    throws Exception {
  List<View> candidateViews = new ArrayList<>();
  if (this.viewResolvers != null) {
    Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
    for (ViewResolver viewResolver : this.viewResolvers) {
    //如果能直接拿到View则放入candidateViews
      View view = viewResolver.resolveViewName(viewName, locale);
      if (view != null) {
        candidateViews.add(view);
      }
      // 根据requestedMediaType 的extensions -尝试找到viewResolver进行解析
      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);
          }
        }
      }
    }
  }
  // 如果defaultViews不为空,放入candidateViews
  if (!CollectionUtils.isEmpty(this.defaultViews)) {
    candidateViews.addAll(this.defaultViews);
  }
  return candidateViews;
}

getBestView方法如下所示,先判断是否重定向View,如果是重定向View直接返回。然后对requestedMediaTypes和candidateViews进行遍历查找,找到一个ContentType合适的candidateView 返回,否则返回null。

@Nullable
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
// 判断重定向View
  for (View candidateView : candidateViews) {
    if (candidateView instanceof SmartView) {
      SmartView smartView = (SmartView) candidateView;
      if (smartView.isRedirectView()) {
        return candidateView;
      }
    }
  }
  // 根据mediaType 和 ContentType进行比较筛选
  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("Selected '" + mediaType + "' given " + requestedMediaTypes);
          }
          attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
          return candidateView;
        }
      }
    }
  }
  return null;
}



【4】AbstractCachingViewResolver


除了ViewResolverComposite、ContentNegotiatingViewResolver以及BeanNameViewResolver外,其余视图解析器都是AbstractCachingViewResolver的子类。如下图所示其同样继承了WebApplicationObjectSupport实现类,也就意味着其实现了ApplicationContextAware和ServletContextAware接口。这两个接口意味着什么,这里不再赘述。


27f1df1207d941c8984fca5166a502d6.png





我们看其子类结构发现其子类仍然分为四类:


  • ① org.thymeleaf.spring5.view体系的ThymeleafViewResolver
  • ② XML结构处理的XmlViewResolver
  • ③ 处理资源国际化的ResourceBundleViewResolver
  • ④ 最常见的UrlBasedViewResolver


其提供了抽象方法loadView让子类实现,子类实现该方法返回一个View对象。AbstractCachingViewResolver会缓存子类返回的View对象放入private final Map viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT);中

@Nullable
protected View createView(String viewName, Locale locale) throws Exception {
  return loadView(viewName, locale);
}
protected abstract View loadView(String viewName, Locale locale) throws Exception;

AbstractCachingViewResolver解析视图的方法

放入如下所示,首先判断是否存在缓存实例,不存在直接走创建View逻辑。如果有缓存实例,那么根据viewName获取cacheKey(viewName + '_' + locale;) ,先尝试从viewAccessCache中获取View,如果View为null,则进入锁方法块-尝试从viewCreationCache再次获取View,如果仍旧为null,则走创建逻辑。如果最终创建的View不为努力,则放入viewAccessCache和viewCreationCache。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 如果没有缓存,直接走create逻辑
  if (!isCache()) {
    return createView(viewName, locale);
  }
  else {
  // 如果有缓存,则拿到cacheKey 然后根据key获取对应的view 
    Object cacheKey = getCacheKey(viewName, locale);
    View view = this.viewAccessCache.get(cacheKey);
    // 如果view为null,则加锁尝试创建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);
          if (view == null && this.cacheUnresolved) {
            view = UNRESOLVED_VIEW;
          }
          // 将创建后的view放入viewAccessCache与viewCreationCache
          if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
            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);
  }
}


【5】ThymeleafViewResolver

ThymeleafViewResolver将会解析视图返回一个ThymeleafView对象,ThymeleafView实现了render方法也就是视图渲染方法-将model与view结合起来,最后将信息写入到response中。

① 获取view

@Override
protected View createView(final String viewName, final Locale locale) throws Exception {
    // First possible call to check "viewNames": before processing redirects and forwards
    if (!this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
        return null;
    }
    // Process redirects (HTTP redirects)
    if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
        final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length(), viewName.length());
        final RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
        return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, REDIRECT_URL_PREFIX);
    }
    // Process forwards (to JSP resources)
    if (viewName.startsWith(FORWARD_URL_PREFIX)) {
        // The "forward:" prefix will actually create a Servlet/JSP view, and that's precisely its aim per the Spring
        // documentation. See http://docs.spring.io/spring-framework/docs/4.2.4.RELEASE/spring-framework-reference/html/mvc.html#mvc-redirecting-forward-prefix
        vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
        final String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length(), viewName.length());
        return new InternalResourceView(forwardUrl);
    }
    // Second possible call to check "viewNames": after processing redirects and forwards
    if (this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
        return null;
    }
    vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a " +
                    "{} instance will be created for it", viewName, getViewClass().getSimpleName());
    return loadView(viewName, locale);
}


这里很有意思,解释如下:


① 其首先判断是否能够处理viewName,不能处理直接返回null;

② REDIRECT_URL_PREFIX = "redirect:"判断是否为重定向view,如果是重定向view则截取viewName获得redirectUrl,创建RedirectView实例并进行其bean初始化过程;

③ FORWARD_URL_PREFIX = "forward:"判断是否为转发view,如果是转发View则截取viewName获取forwardUrl,返回一个InternalResourceView实例。

④ 如果alwaysProcessRedirectAndForward 被设置为true,但是不能处理当前viewName,返回null;

⑤ 使用ThymeleafViewResolver 解析viewName。loadView方法如下所示:

@Override
protected View loadView(final String viewName, final Locale locale) throws Exception {
    final AutowireCapableBeanFactory beanFactory = getApplicationContext().getAutowireCapableBeanFactory();
    final boolean viewBeanExists = beanFactory.containsBean(viewName);
    final Class<?> viewBeanType = viewBeanExists? beanFactory.getType(viewName) : null;
    final AbstractThymeleafView view;
    if (viewBeanExists && viewBeanType != null && AbstractThymeleafView.class.isAssignableFrom(viewBeanType)) {
        // AppCtx has a bean with name == viewName, and it is a View bean. So let's use it as a prototype!
        //
        // This can mean two things: if the bean has been defined with scope "prototype", we will just use it.
        // If it hasn't we will create a new instance of the view class and use its properties in order to
        // configure this view instance (so that we don't end up using the same bean from several request threads).
        //
        // Note that, if Java-based configuration is used, using @Scope("prototype") would be the only viable
        // possibility here.
        final BeanDefinition viewBeanDefinition =
                (beanFactory instanceof ConfigurableListableBeanFactory ?
                        ((ConfigurableListableBeanFactory)beanFactory).getBeanDefinition(viewName) :
                        null);
        if (viewBeanDefinition == null || !viewBeanDefinition.isPrototype()) {
            // No scope="prototype", so we will just apply its properties. This should only happen with XML config.
            final AbstractThymeleafView viewInstance = BeanUtils.instantiateClass(getViewClass());
            view = (AbstractThymeleafView) beanFactory.configureBean(viewInstance, viewName);
        } else {
            // This is a prototype bean. Use it as such.
            view = (AbstractThymeleafView) beanFactory.getBean(viewName);
        }
    } else {
        final AbstractThymeleafView viewInstance = BeanUtils.instantiateClass(getViewClass());
        if (viewBeanExists && viewBeanType == null) {
            // AppCtx has a bean with name == viewName, but it is an abstract bean. We still can use it as a prototype.
            // The AUTOWIRE_NO mode applies autowiring only through annotations
            beanFactory.autowireBeanProperties(viewInstance, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
            // A bean with this name exists, so we apply its properties
            beanFactory.applyBeanPropertyValues(viewInstance, viewName);
            // Finally, we let Spring do the remaining initializations (incl. proxifying if needed)
            view = (AbstractThymeleafView) beanFactory.initializeBean(viewInstance, viewName);
        } else {
            // Either AppCtx has no bean with name == viewName, or it is of an incompatible class. No prototyping done.
            // The AUTOWIRE_NO mode applies autowiring only through annotations
            beanFactory.autowireBeanProperties(viewInstance, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
            // Finally, we let Spring do the remaining initializations (incl. proxifying if needed)
            view = (AbstractThymeleafView) beanFactory.initializeBean(viewInstance, viewName);
        }
    }
    view.setTemplateEngine(getTemplateEngine());
    view.setStaticVariables(getStaticVariables());
    // We give view beans the opportunity to specify the template name to be used
    if (view.getTemplateName() == null) {
        view.setTemplateName(viewName);
    }
    if (!view.isForceContentTypeSet()) {
        view.setForceContentType(getForceContentType());
    }
    if (!view.isContentTypeSet() && getContentType() != null) {
        view.setContentType(getContentType());
    }
    if (view.getLocale() == null && locale != null) {
        view.setLocale(locale);
    }
    if (view.getCharacterEncoding() == null && getCharacterEncoding() != null) {
        view.setCharacterEncoding(getCharacterEncoding());
    }
    if (!view.isProducePartialOutputWhileProcessingSet()) {
        view.setProducePartialOutputWhileProcessing(getProducePartialOutputWhileProcessing());
    }    
    return view;    
}


代码看起来比较长,其实核心就是获取Bean的实例然后进行bean的初始化过程最后设置诸如TemplateEngine、StaticVariables、TemplateName以及CharacterEncoding等属性。这里getViewClass返回的是ThymeleafView.class。也就是说该方法会返回一个ThymeleafView实例,除非你自定义实现类继承自AbstractThymeleafView。

【6】UrlBasedViewResolver


除去ThymeleafViewResolver(对应Thymeleaf)之外,我们最熟悉的可能就是InternalResourceViewResolver(对应JSP)和FreeMarkerViewResolver(对应Freemarker)了。


① UrlBasedViewResolver的重要属性

//重定向前缀
public static final String REDIRECT_URL_PREFIX = "redirect:";
// 转发前缀
public static final String FORWARD_URL_PREFIX = "forward:";
// 视图类型
@Nullable
private Class<?> viewClass;
// prefix="/WEB-INF/jsp/", suffix=".jsp", viewname="test" ->
// "/WEB-INF/jsp/test.jsp"
private String prefix = "";
private String suffix = "";
//view的contentType
@Nullable
private String contentType;
//true表示重定向时相对于web application root
private boolean redirectContextRelative = true;
//true表示重定向时与HTTP 1.0 一致,如302;false则与HTTP 1.1一致,303
private boolean redirectHttp10Compatible = true;
//重定向主机host
@Nullable
private String[] redirectHosts;
//为view设置请求上下文中的属性
@Nullable
private String requestContextAttribute;
//Properties中的属性,你定义视图解析时的property
/** Map of static attributes, keyed by attribute name (String). */
private final Map<String, Object> staticAttributes = new HashMap<>();
// 是否将path variables放到model里面,默认为null,让具体view决定
@Nullable
private Boolean exposePathVariables;
//是否能够作为请求属性访问spring容器中的bean,默认是false
@Nullable
private Boolean exposeContextBeansAsAttributes;
//指定上下文中应该公开的bean的名称。如果该值为非null,则只有指定的bean有资格作为属性公开。
@Nullable
private String[] exposedContextBeanNames;
//设置能够被视图解析器处理的viewName
@Nullable
private String[] viewNames;
//Integer.MAX_VALUE
private int order = Ordered.LOWEST_PRECEDENCE;


为什么分析这些属性呢?我们看一下以前的一个配置实例:

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
   <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
   <property name="prefix" value="/WEB-INF/jsp/"/>
   <property name="suffix" value=".jsp"/>
</bean>

② createView方法

createView覆盖了父类的createViewf方法。如下所示,方法分为四大块:

① 根据viewNames判断是否能够处理当前viewName;

② 处理重定向请求,获取一个RedirectView然后调用initializeBean方法进行初始化;

③ 处理转发请求,获取一个InternalResourceView,然后调用initializeBean方法进行初始化;

④ 调用父类的createView方法

@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());
    String[] hosts = getRedirectHosts();
    if (hosts != null) {
      view.setHosts(hosts);
    }
    return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
  }
  // Check for special "forward:" prefix.
  if (viewName.startsWith(FORWARD_URL_PREFIX)) {
    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.
  return super.createView(viewName, locale);
}

我们看下applyLifecycleMethods,如下所示调用BeanFactory的initializeBean方法对view进行初始化,核心步骤就是:


invokeAwareMethods
applyBeanPostProcessorsBeforeInitialization
invokeInitMethods
applyBeanPostProcessorsAfterInitialization
protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
  ApplicationContext context = getApplicationContext();
  if (context != null) {
    Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
    if (initialized instanceof View) {
      return (View) initialized;
    }
  }
  return view;
}


③ loadView与buildView

loadView同样覆盖了父类的方法,如下所示委派buildView方法获取一个AbstractUrlBasedView 实例,然后调用其生命周期方法进行bean的初始化。

@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);
}


buildView方法如下所示,首先根据viewClass使用方法(AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass)获取一个view实例。然后进行各种属性的赋值如url、properties、contentType等。

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
// 获取一个view实例
  AbstractUrlBasedView view = instantiateView();
  //如下都是属性的赋值
  view.setUrl(getPrefix() + viewName + getSuffix());
  view.setAttributesMap(getAttributesMap());
  String contentType = getContentType();
  if (contentType != null) {
    view.setContentType(contentType);
  }
  String requestContextAttribute = getRequestContextAttribute();
  if (requestContextAttribute != null) {
    view.setRequestContextAttribute(requestContextAttribute);
  }
  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;
}

【7】InternalResourceViewResolver

该解析器处理viewName返回一个InternalResourceViewJstlView实例。当使用链式ViewResolvers时,InternalResourceViewResolver始终需要配置为最后一个,因为它将尝试解析任何视图名称,而不管底层资源是否实际存在。

① 构造函数

// 调用无参构造函数,设置前缀和后缀
public InternalResourceViewResolver(String prefix, String suffix) {
  this();
  setPrefix(prefix);
  setSuffix(suffix);
}
//无参构造函数
public InternalResourceViewResolver() {
// 默认返回InternalResourceView
  Class<?> viewClass = requiredViewClass();
  if (InternalResourceView.class == viewClass && jstlPresent) {
    viewClass = JstlView.class;
  }
  setViewClass(viewClass);
}

② 覆盖父类的instantiateView

方法如下所示,如果viewClass是InternalResourceViewz则new InternalResourceView();如果是JstlView,则获取一个JstlView实例;否则最后调用父类的instantiateView方法。

@Override
protected AbstractUrlBasedView instantiateView() {
  return (getViewClass() == InternalResourceView.class ? new InternalResourceView() :
      (getViewClass() == JstlView.class ? new JstlView() : super.instantiateView()));
}

③ 覆盖父类的buildView方法

这里调用了父类的buildView方法,然后做了两个增强:


alwaysInclude,指定是否始终包含视图而不是转发视图。默认值为“false”。打开此标志以强制使用Servlet包含,即使可以转发

preventDispatchLoop,设置是否显式阻止转发回当前处理程序路径。默认值为“false”。对于基于约定的视图,如果将其转发回当前处理程序路径是一个确定错误,请将其切换为“true”。

@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
  InternalResourceView view = (InternalResourceView) super.buildView(viewName);
  if (this.alwaysInclude != null) {
    view.setAlwaysInclude(this.alwaysInclude);
  }
  view.setPreventDispatchLoop(true);
  return view;
}


目录
相关文章
|
6月前
|
容器
SpringMVC常见组件之HandlerExceptionResolver分析-2
SpringMVC常见组件之HandlerExceptionResolver分析-2
42 0
|
1月前
|
设计模式
SpringMVC常见组件之DataBinder数据绑定器分析
SpringMVC常见组件之DataBinder数据绑定器分析
135 0
|
1月前
|
XML 存储 Java
SpringMVC常见组件之HandlerMapping分析
SpringMVC常见组件之HandlerMapping分析
84 0
|
1月前
|
XML 缓存 前端开发
SpringMVC常见组件之HandlerAdapter分析
SpringMVC常见组件之HandlerAdapter分析
43 0
|
6月前
|
XML 前端开发 Java
SpringMVC常见组件之View分析
SpringMVC常见组件之View分析
65 0
|
6月前
|
JSON 前端开发 Java
SpringMVC常见组件之HandlerExceptionResolver分析-1
SpringMVC常见组件之HandlerExceptionResolver分析-1
55 0
java202304java学习笔记第六十二天-ssm-springMvc中组件分析
java202304java学习笔记第六十二天-ssm-springMvc中组件分析
31 0
|
存储 设计模式 前端开发
浅谈SpringMVC五大组件以及对执行原理的分析。
Spring MVC是包含在spring中的一个基于MVC设计思想的Web应用程序框架,目的是简化开发工作,提高开发效率。
浅谈SpringMVC五大组件以及对执行原理的分析。
|
前端开发 Java 程序员
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(下)
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(下)
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(下)
|
前端开发 Java Spring
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(中)
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(中)
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(中)