本文我们尝试总结分析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方法。
我们从其源码也可以知晓,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方法。
我们看下其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接口。这两个接口意味着什么,这里不再赘述。
我们看其子类结构发现其子类仍然分为四类:
- ① 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
返回一个InternalResourceView
或JstlView
实例。当使用链式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; }