Spring MVC各组件近距离接触--下--04

2022-08-23 10:59:35 浏览数 (1)

Spring MVC各组件近距离接触--下--04

  • 引言
  • ModelAndView
    • ModelAndView中的视图信息
    • ModelAndview 中的模型数据
  • 视图定位器ViewResolver
    • AbstractCachingViewResolver
    • 可用的 ViewResolver 实现类
      • 1.面向单一视图类型的ViewResolver
      • 2.面向多视图类型的viewResolver
    • BeanNameViewResolver
    • ViewResolver小结
    • ViewResolver 查找序列(Chain Of ViewResolver)
  • 小结

引言

前面两节主要介绍了Spring mvc中的HandlerMapping和Controller,下面来介绍一下mvc中的其他常见组件。


ModelAndView

Controller在将Web请求处理完成后,会返回一个ModelAndView实例。该ModelAndView实例将包含两部分内容,一部分为视图相关内容,可以是逻辑视图名称,也可以是具体的View实例;

另一部分则是模型数据,视图渲染过程中将会把这些模式数据合并入最终的视图输出。所以,简单来说,ModelAndView实际上就是一个数据对象。

不过通过该数据对象,我们可以解除具体的Web请求处理Controller与视图渲染之间的紧密耦合,使得两个方面能够独立演化。

为了方便实例化ModelAndView,该类定义了两组参数各异的构造方法,一组使用逻辑视图名称标志视图,一组直接使用View实例标志视图,如下所示:

代码语言:javascript复制
public ModelAndView(String viewName)

public ModelAndView(String viewName,Map model)

public ModelAndview(String viewName,String modelName,Object mode10bject) public ModelAndView(View view)

public ModelAndView(View view,Map model)

public ModelAndView(View view,String modelName,Object modelobject) 

每组的第一个构造方法只接受视图信息,所以,构造完成后,我们得通过addA110bject(..)或者addobject(…)实例方法,向构造完成的ModelAndView实例添加模型数据;

每组第二个构造方法则可以同时指定视图信息和模型数据信息,一步到位;

如果要添加到模型的只有一个数据对象,那么可以使用每组的第三个构造方法,该构造方法属于第二个构造方法的简化版。

除了以上的构造方法之外,ModelAndView还有一个默认的没有参数的构造方法,如果使用该构造方法实例化对象,那么之后就需要使用其他实例方法来设置视图和模型数据信息了(听起来有点儿像废话哦)


ModelAndView内部提供的属性有下面三个:

代码语言:javascript复制
	//保存view对象或者视图名
	private Object view;

	//存放模型数据
	private ModelMap model;

    //当前ModelAndView对象内部的view和model数据是否都已经被清空了----方便对象的复用
	private boolean cleared = false;

ModelAndView中的视图信息

ModelAndView可以返回逻辑视图名,或者View实例,如果直接返回了具体的View实例,那么,DispathcerServlet将直接从ModelAndView中获取该View实例并渲染视图,如下所示:

代码语言:javascript复制
View view=null;
,,,
view=mv.getView();
view.render(mv.getModelInternal(),request,response);

如果返回的是逻辑视图名称,DispatcherServlet将寻求ViewResolver的帮助,根据ModelAndView中的逻辑视图名称获取一个可用的View实例,然后再渲染视图:

代码语言:javascript复制
View view=null;
,,,
view=resolveViewName(mv.getViewName(),mv.getModelInternal(),locale,request);
view.render(mv.getModelInternal(),request,response);

注意 虽然通过ModelAndView可以保存视图的逻辑名称或者具体的View实现类,但是我们更倾向于使用逻辑视图名来标志视图。这样可以给我们的视图选择带来很大的灵活性,除非必要,尽量不要直接返回具体的View实例。


ModelAndview 中的模型数据

ModelAndView以org.springframework.ui.ModelMap的形式来保持模型数据,通过构造方法传 入的或者通过实例方法添加的模型数据都将添加到这个ModelMap中。至于ModelMap中保持的模型数据将会在视图渲染阶段,由具体的View实现类来获取并使用。

我们需要为添加到ModelAndView的一组或者多组模型数据提供相应的键(Key),以便具体的View实现类可以根据这些键获取具体的模型数据,然后公开给视图模板。通常,模型中的数据对应的键需要与视图模板中的标志符相对应:

基于JSP/JSTL模板的视图实现,通常是将模型数据通过HttpServletRequest的属性(Attribute) 的形式公开给具体的模板。而像基于Velocity之类的通用模板引擎的视图实现,则会将ModelAndView中的模型数据复制到它们自己的数据获取上下文中,比如Velocity的Context。但不管什么视图类型,对应的视图模板都将可以通过添加到ModelAndView的模型数据的键来获取模型数据,并合并到最终的视图输出结果中。


视图定位器ViewResolver

我们已经知道了ViewResolver的主要职责是,根据Controller所返回的ModelAndView中的逻辑 视图名,为DispatcherServlet返回一个可用的View实例。现在是揭开viewResolver如何“尽职” 的时候了。

有ViewResolver的职责为前提,理解甚至于自己声明一个ViewResolver接口变得不再困难。实际上ViewResolver接口定义确实很简单,如下所示:

代码语言:javascript复制
public interface ViewResolver {
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

接口实现类只需要根据resolveViewName()方法中以参数形式传入的逻辑视图名(viewName)和当前Locale的值,返回相应的view实例即可。

至于每个ViewResolver实现类如何处理具体的逻辑视图名与具体的View实例之间的对应关系,则因实现类的不同而存在差异。

大部分的ViewResolver实现类,除了org.springframework.web.servlet.view.BeanNameViewResolver是直接实现ViewResolver接口,都直接或者间接继承自 org.springframe work. web.servlet.view.AbstractCachingViewResolver

因为针对每次请求都重新实例化view将可能 为Web应用程序带来性能上的损失,所以Spring MVC在AbstractCachingViewResolver这一继承层 次加入了view实例的缓存功能。AbstractCachingViewResolver默认启用view的缓存功能。对于生 产环境来说,这是合理的默认值。

不过,如果在测试或者开发环境下,我们想即刻反映相应的修改结果,可以通过setCache(false)暂时关闭AbstractCachingViewResolver的缓存功能。

Spring MVC在AbstractCachingViewResolver的基础上为我们提供了一系列的ViewResolver 实现。下面让我们来认识一下它们的庐山直面目

完整继承关系:


AbstractCachingViewResolver

AbstractCachingViewResolver负责完成对已经查询过的视图的缓存,核心方法只有一个为resolveViewName:

代码语言:javascript复制
public View resolveViewName(String viewName, Locale locale) throws Exception {
        //是否开启了缓存,默认是开启的
		if (!isCache()) {
		    //如果没有开启缓存,那么每次都新创建一个View实例
			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) {
						//创建一个新的View实例
						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   "]");
							}
						}
					}
				}
			}
			//返回view,还是要区分一下不能被解析的情况
			return (view != UNRESOLVED_VIEW ? view : null);
		}
	}

核心中的核心方法createView调用了loadView,不难想象该方法肯定要由不同的子类去实现:

代码语言:javascript复制
	protected View createView(String viewName, Locale locale) throws Exception {
		return loadView(viewName, locale);
	}
    
    protected abstract View loadView(String viewName, Locale locale) throws Exception;

可用的 ViewResolver 实现类

为了便于理解,我们可以将Spring MVC提供的ViewResolver划分为两类,一类称为“面向单一视图类型的ViewResolver,另一类则称为面向多视图类型的viewResolver。下面是这两类ViewResolver 的详细情况。


1.面向单一视图类型的ViewResolver

该类别ViewResolver的正宗名称应该是UrlBasedViewResolver(它们都直接地或者间接地 继承自该类)。使用该类别的ViewResolver,我们不需要为它们配置具体的逻辑视图名到具体View的映射关系。通常只要指定一下视图模板所在的位置,这些viewResolver就会按照逻辑视图名,抓取相应的模板文件、构造对应的view实例并返回。

之所有又将它们称之为面向单一视图类型的ViewResolver,是因为该类别中,每个具体的ViewResolver实现都只负责一种View类型的映射, ViewResolver与View之间的关系是一比一。

比如,我们之前一直使用的InternalResourceViewResolver,它通常就只负责到指定位置抓取JSP模板文件,并构造InternalResourceView类型的View 实例并返回。

而velocityViewResolver则只关心指定位置的Velocity模板文件(.vm),并会将逻辑 视图名映射到视图模板的文件名,然后构造VelocityView类型的View实例返回,诸如此类。


UrlBasedViewResolver类的核心方法为createView,对父类AbstractCachingViewResolver做了增强:

代码语言:javascript复制
	@Override
	protected View createView(String viewName, Locale locale) throws Exception {
		//判断当前视图解析器能否解析当前视图名,如果不能直接返回null,表示无法解析
		if (!canHandle(viewName, locale)) {
			return null;
		}
		//判断视图名是否以"redirect:"开头,表示重定向请求
		//重定义请求返回的是RedirectView
		//applyLifecycleMethods是调用RedirectView相关初始化方法以及相关后处理
		if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
			String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
			RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
			return applyLifecycleMethods(viewName, view);
		}
		// 判断视图名是否以"forward:"开头,表示转发请求
		if (viewName.startsWith(FORWARD_URL_PREFIX)) {
			String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
			return new InternalResourceView(forwardUrl);
		}
		//做完增加后,继续走父类的逻辑---父类方法只会去调用loadView
		return super.createView(viewName, locale);
	}
	
    //用户设置好当前视图解析器能够解析的视图数组,然后挨个ant匹配判断,有一个匹配上就返回true
    //如果用户没有设置能够解析的视图数组,也默认可以处理
   	protected boolean canHandle(String viewName, Locale locale) {
		String[] viewNames = getViewNames();
		return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
	}
    
    	private View applyLifecycleMethods(String viewName, AbstractView view) {
		return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
	}

另一大核心方法就是loadView:

代码语言:javascript复制
	@Override
	protected View loadView(String viewName, Locale locale) throws Exception {
	    //实例化view
		AbstractUrlBasedView view = buildView(viewName);
		//对View进行后处理
		View result = applyLifecycleMethods(viewName, view);
		//checkResource: 检查对应的视图资源是否真的存在,如果不存在,返回false
		return (view.checkResource(locale) ? result : null);
	}
   
   
	protected AbstractUrlBasedView buildView(String viewName) throws Exception {
		//UrlBasedViewResolver内部有一个viewClass属性,表示当前UrlBasedViewResolver负责创建哪种view实例
		//这里通过设置好的viewClass,反射实例化一个出来
		AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
		//设置视图的完整URL=用户设置的前缀(默认为空) viewName 用户设置的后缀(默认为空)
		view.setUrl(getPrefix()   viewName   getSuffix());
        //渲染的类型 
		String contentType = getContentType();
		if (contentType != null) {
			view.setContentType(contentType);
		}
        //用户可以在配置文件中声明一些通用属性放入当前ViewResolver生成的view实例中
		view.setRequestContextAttribute(getRequestContextAttribute());
		view.setAttributesMap(getAttributesMap());
        
        //是否暴露当前view视图的url路径到模型中去
		Boolean exposePathVariables = getExposePathVariables();
		if (exposePathVariables != null) {
			view.setExposePathVariables(exposePathVariables);
		}

		return view;
	}

属于该类别的主要ViewResolver实现类为如下几个。

  • InternalResourceviewReBolver。

它是我们使用最多的ViewResolver实现类型,它对应InternalResourceView视图类型的映射,说白了也就是处理JSP模板类型的视图映射。

如果DispatcherServlet在初始化的时候,不能在自己的webApplicationContext中找到至少一个ViewResolver,那么,InternalResourceViewResolver将作为默认的ViewResolver被使用。

  • FreeMarkerViewResolver/VelocityViewReBolver。

FreeMarkerViewResolver和VelocityViewResolver分别负责对应FreeMarkerView和VelocityView类型视图的查找工作,它们将根据逻辑视图名到指定的位置获取对应的模板文件,并构造FreeMarkerView和VelocityView的实例返回给DispatcherServlet使用。

  • JasperReportsViewResolver。

JasperReportsViewResolver只关心根据逻辑视图名到指 定位置查找JasperReport类型模板文件,并返回AbstractJasperReportsView的具体子类 型View实例,例如JasperReportsCsvView或者JasperReportsHtmlView等。

  • XsltViewResolver。

只负责根据逻辑视图名查找并返回xsltView类型的View实例。


启用以上这些viewResolver,与使用InternalResourceViewResolver一样简单。

最基本的方法是,使用prefix属性指定模板所在路径,使用suffix属性指定模板文件的后缀名。

这样,在获取逻辑视图名之后,相应的ViewResolver内部就能够根据[prefix]+viewName+[suffix]这样的URL 找到对应的模板文件,并构造对应的view实例而返回了。

以velocityViewResolver的使用为例,至于其他的几个ViewResolver的使用,你基本上就可以“举一反三”了,下面给出了针对VelocityViewResolver的配置代码示例:

代码语言:javascript复制
<bean id="viewResolver"class="org.springframework.Web.servlet.view.velocity.VelocityViewResolver"> 
<property name="prefix" value="../velocity/"/>
<property name="suffix" value=".vm"/> 
</bean>

现在DispatcherServlet对视图的请求将会由VelocityViewResolver接管,VelocityViewResolver将根据传入的逻辑视图名,到指定目录下查找.vm类型的Velocity模板文件,并构造VelocityView实例返回给DispatcherServlet使用。

就跟我们所说的那样,它只负责到指定位置查找对应Velocity的单一视图类型,而不会返回其他,比如Freemarker视图对应的view实例。

对于这些ViewResolver的具体实现子类来说,套路都是固定的,重写父类的buildView方法,来额外添加一些定制的属性:

  • 当然还有一点就是会将viewClass实例化为当前ViewResolver能够解析处理的

2.面向多视图类型的viewResolver

使用面向单一视图类型的ViewResolver,我们不需要指定明确的逻辑视图名与具体视图之间的映射关系,对应的ViewResolver将自动到指定位置匹配自己所管辖的那种视图模板,并构造具体的View实例。

面向多视图类型的ViewResolver则不然。使用面向多视图类型的ViewResolver,我们需 要通过某种配置方式明确指定逻辑视图名与具体视图之间的映射关系,这可能带来配置上的烦琐。不过,好处是,面向多视图类型的ViewResolver可以顾及多种视图类型的映射管理。如果你的逻辑视图名想要映射到InternalResourceView,那么面向多视图类型的ViewResolver可以做到。如果你的 逻辑视图名想要映射到velocityView,那么,面向多视图类型的ViewResolver也可以做到。相对于只支持单一视图类型映射的情况,面向多视图类型的ViewResolver更加灵活。

面向多视图类型的ViewResolver的主要实现类有三个,它们分别是ResourceBundleViewResolver、XmlViewResolver以及BeanNameViewResolver。

以下是它们的详细情况介绍。

  • ResourceBundleViewResolver。

ResourceBundleViewResolver构建在ResourceBundle上,继 承了ResourceBundle国际化支持的能力,也是所有的ViewResolver实现类中唯一提供视图国际化支持的ViewResolver。

ResourceBundleViewResolver管理的视图的逻辑名称与具体视图的映射关系保存在properties文件中,格式符合Spring的IoC容器的properties配置格式。

ResourceBundleViewResolver内部将通过PropertiesBeanDefinitionReader加载这些配置信息。

之后,根据逻辑视图名查找的操作,实际上也就简化为beanfactory.getBean(viewName)的形式了(当然,实际上要做 的事情会多一些)。

代码语言:javascript复制
	@Override
	protected View loadView(String viewName, Locale locale) throws Exception {
	   //利用PropertiesBeanDefinitionReader去读取对应的properties文件
	   //然后利用IOC去实例化这些viewBean,对应的beanName就是对应的viewName
		BeanFactory factory = initFactory(locale);
		try {
		//对应的配置文件中已经声明好了相关View和ViewName的映射关系了
			return factory.getBean(viewName, View.class);
		}
		catch (NoSuchBeanDefinitionException ex) {
			// Allow for ViewResolver chaining...
			return null;
		}
	}

使用ResourceBundleViewResolver之前,我们得先将其添加到DispatcherServlet的webApplicationContext中,如下所示:

代码语言:javascript复制
    <bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    </bean>

如果我们没有指定properties配置文件从何处加载的话,ResourceBundleViewResolver默 认将从classpath的根路径加载以views为basename的properties文件,比如views.properties、 views_zh_CN.properties等。

如果我们想改变这种默认加载行为,可以通过setBasename(String) 或者setBasenames(String[])方法来进行变更。

以下是一个典型的ResourceBundleViewResolver使用的properties配置文件内容:

代码语言:javascript复制
viewTemplate.class=org.springframework.Web.servlet.view.InternalResourceView viewTemplate.(abstract)=true

help/HelpForSomething.(parent)=viewTemplate
help/HelpForSomething.url=/WEB-INF/jsp/help/HelpForSomething.jsp 

hello.class=org.springframework.Web.servlet.view.velocity.VelocityView
hello.url=cn/spring21/simplefx/resources/velocity/hello.vm

#其他视图定义·....·

视图的bean定义主要有两个属性:class和url。

如果我们想要避免每次为同一类型的视图指定某些共同的属性,也可以定义一个模板声明,然后通过parent引用该模板声明。


注意:

如果要在ResourceBundleViewResolver中使用Velocity或者Freemarker之类的通用 模板引擎渲染的视图,那么需要在WebApplicationContext中添加相应的配置,使得视图渲染阶段能够获取模板引擎的支持。

实际上,单独使用VelocityViewResolver或者FreemarkerViewResolver也需要同样的配置。

我们以使用Velocity类型视图的配置为例,(Freemarker类型视图的配置与Velocity类型视图的配置雷同)。在应用程序的WebApplicationContext中,我们添加org.springframework.web. servlet.view.velocity.VelocityConfigurer的配置如下:

代码语言:javascript复制
<bean id="velocityConfig" class="org.springframework.Web. servlet.view. velocity.VelocityConfigurer"> 
<property name="configLocation" value="/WEB-INF/velocity-config.properties"/> 
</bean>

这样,在视图渲染阶段就可以根据该配置获取一个VelocityEngine进行视图模板与数据的合并(Merge)操作,以便最终输出视图页面。

velocity-config.properties的配置内容,完全就是特定于Velocity的内容了。你可以参考Velocity的 相关文档获取配置参数,这里可以给出一个简单的实例,如下所示:

代码语言:javascript复制
resource.loader=classpath

classpath.resource.loader.description = Classpath Resource Loader classpath.resource.loader.class=

org. apache. velocity.runtime.resource. loader.ClasspathResourceLoader classpath.resource.loader.path=.

velocimacro.library=

最后,对于Velocity(或者Freemarker)的模板文件,最好像我们给出的配置内容所指定的那样,将它们放入应用程序的classpath中进行加载,而不是依赖于默认的文件系统加载行为。


  • XmlViewResolver。

XmlViewResolver与ResourceBundleViewResolver之间最主要的区 别就是,它们所采用的配置文件格式不同。

ResourceBundleViewResolver按照Spring IoC容器 所接受的properties配置格式配置逻辑视图名与具体视图之间的映射关系,而XmlViewResolver则是按照Spring IoC容器接受的XML配置文件格式来加载映射信息。

与ResourceBundleViewResolver 同样的配置信息,使用xmlViewResolver的话,内容下所示:

代码语言:javascript复制
<bean name="viewTemplate" class="org.springframework.Web.servlet.view.InternalResourceView" abstract="true">
</bean>

<bean name="help/HelpForSomething" parent="viewTemplate">
<property name="url" value="/WEB-INF/jsp/help/HelpForSomething.jsp"/> 
</bean>

<bean name="hello" class="org.springframework.Web.servlet.view.velocity.VelocityView" p:url="cn/spring21/simplefx/resources/velocity/hello.vm">
</bean> 

XmIViewResolver默认会加载/WEB-INF/views.xml作为配置文件。不过,我们可以在将XmlViewResolver添加到webApplicationContext的时候,根据情况改变这一默认行为,例如:

代码语言:javascript复制
<bean id="xmlViewResolver" class="org.springframework.Web.servlet.view.XmlViewResolver"> 
<property name="1ocation" value="classpath:views.xm1"/>
</bean>

现在,XmlViewResolver将从Classpath的根路径加载名为views.xml的配置文件。

至于其他配置, 比如Velocity需要的VelocityConfigurer,因为与使用何种viewResolver没有关系,只与是否使 用Velocity作为视图技术有关,所以依然需要根据情况添加到webApplicationcontext中。

注意 XmlViewResolver并不支持视图的国际化(118n)。如果必须对国际化视图给予支持,需要使用ResourceBundleViewResolver。


BeanNameViewResolver

BeanNameViewResolver可以认为是XmlViewResolver的原型版或者简 化版。

使用它,我们可以直接将view实例注册到当前DispatcherServlet所使用的特定的webApplicationcontext中,而不用像XmlViewResolver那样另辟一块地。

代码语言:javascript复制
	public View resolveViewName(String viewName, Locale locale) throws BeansException {
		ApplicationContext context = getApplicationContext();
		if (!context.containsBean(viewName)) {
			// Allow for ViewResolver chaining...
			return null;
		}
		return context.getBean(viewName, View.class);
	}

不过,BeanNameViewResolver更 多地用于快速搭建应用框架原型,或者构建小型的Web应用程序。

对于正常的基于Spring MVC的Web应用程序,应尽量避免将可以分离出来的视图配置信息一并加入到DispatcherServlet的WebApplicationContext中。

至于如何启用BeanNameViewResolver作为ViewResolver,我想你现在要比我清楚,如下所示:

代码语言:javascript复制
<bean id="beanNameViewResolver" class="org. springframework. Web. servlet.view. BeanNameViewResolver"/> 

至于具体视图的配置,参照XmlViewResolver即可。


ViewResolver小结

实际上,正如我们所看到的那样,这三种ViewResolver在本质上是一样的,只不过是配置的表现形式上存在差异而已。

最终的配置信息都将转换为Spring IoC容器中管理的View实例,BeanNameViewResolver应该是最初的实现原型吧!


ViewResolver 查找序列(Chain Of ViewResolver)

虽然我们在之前的示例中一直都是使用一个InternalResourceViewResolver进行视图查找,但这并不意味着每个基于Spring MVC的Web应用程序只能使用一个ViewResolver。实际上,DispatcherServlet不但可以接受多个HandlerMapping以处理Web请求到具体Handler的映射,也可以接受多个 ViewResolver以处理视图的查找。

DispatcherServlet初始化时,将根据类型扫描自己的webApplicationContext中定义的 ViewResolver。如果查找到存在多个ViewResolver的定义,DispatcherServlet将根据这些 ViewResolver的优先级进行排序,然后当需要根据逻辑视图名查找具体的View实例的时候,将按照排序后的顺序遍历这些ViewResolver,只要期间任何一个ViewResolver返回非空的View实例,当前 查找即告结束。如果DispatcherServlet没能在当前的webApplicationcontext中找到任何的 ViewResolver定义,它将使用InternalResourceViewResolver作为默认的ViewResolver使用。

ViewResolver的优先级的指定使用Ordered接口作为标准,这已经成为Spring框架内设定优先级方式的惯例了。假设我们希望主要使用ResourceBundleViewResolver进行逻辑视图名到具体View实例的查找,如果没能找到,再寻求InternalResourceViewResolver的帮助。我们可以在DispatcherServlet的webApplicationContext中添加如下配置内容:

代码语言:javascript复制
<bean id="resourceBundleViewResolver" class="org.springframework.Web.servlet.view.ResourceBundleViewResolver"> 
<property name="order" value="1"></property>
</bean>

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

相应ViewResolver的bean定义对应的id或者name属性值是任意的,DispatcherServlet将按照 类型来获取ViewResolver。如果没有为某个viewResolver指定order值的话,默认值为Integer.MAX_ VALUE,对应的是最低优先级。

如果为DispatcherServlet指定多个ViewResolver的话,不要给予InternalResourceViewResolver以及其他UrlBasedViewResolver子类过高的优先级,因为这些viewResolver 即使找不到相应的视图,也不会返回null以给我们轮询下一个ViewResolver的机会,这样,我们所指定的其他ViewResolver实际上就形同虚设。

合理的处理方式是,给予ResourceBundleViewResolver或者XmlViewResolver这种能够通过返回null以表明无法找到相应视图的ViewResolver 较高的优先级,而只是将InternalResourceViewResolver(或者其他类似行为的ViewResolver) 添加为最低优先级ViewResolver,以作为DispatcherServlet的后备查找对象。

通过查看DispathcerServlet关于视图解析部分的源码,我们可以清晰的明白其底层原理:

代码语言:javascript复制
	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		//拿到locale
		Locale locale = this.localeResolver.resolveLocale(request);
		response.setLocale(locale);

		View view;
		//isReference表示当前ModelAndView内部维护的ViewObject是viewName,还是对应的view实例
		if (mv.isReference()) {
			//如果是viewName,那么需要进行解析,得到对应的view实例,靠的就是ViewResolver
			view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException("Could not resolve view with name '"   mv.getViewName()  
						"' in servlet with name '"   getServletName()   "'");
			}
		}
		else {
			//否则直接拿到view实例即可
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView ["   mv   "] neither contains a view name nor a "  
						"View object in servlet with name '"   getServletName()   "'");
			}
		}

		// Delegate to the View object for rendering.
		if (logger.isDebugEnabled()) {
			logger.debug("Rendering view ["   view   "] in DispatcherServlet with name '"   getServletName()   "'");
		}
		try {
		//进行渲染
			view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view ["   view   "] in DispatcherServlet with name '"  
						getServletName()   "'", ex);
			}
			throw ex;
		}
	}

利用DispathcerServlet内部捕获到的ViewResolver来挨个尝试解析viewName:

代码语言:javascript复制
	protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
			HttpServletRequest request) throws Exception {
       
		for (ViewResolver viewResolver : this.viewResolvers) {
		//挨个尝试解析,有一个返回的不为空,就直接结束
			View view = viewResolver.resolveViewName(viewName, locale);
			if (view != null) {
				return view;
			}
		}
		return null;
	}

小结

本节只是勉强讲完了ViewResolver,还差一个View没讲,后续会补上。

当我们需要将两个模块进行解耦时,就需要学习spring,通过一个ModelAndView作为模块间通信的承载者。

并且还有一点就是要细分框架内各个组件的职责,每个组件都可以灵活定制,并且随意替换,这样可以极大提高框架的轻量性,灵活性。

就像搭积木一样,原生的api就像一堆及其零散的小零件,组装十分麻烦,而框架就是负责将这些零件分模块,并且提供基础平台,就像给你造好了一个毛坯房,并且还给你提供了相关水电设施接口,然后用户可以在此基础上任意装饰该房子,而不是从和水泥开始打造房子的地基。

0 人点赞