第24章 近距离接触SpringMVC主要角色(三)

第24章 近距离接触SpringMVC主要角色

24.2 我们的亲密伙伴Controller

24.2.4 AbstractWizardFormController

在使用Eclipse或者Netbeans之类的IDE进行开发的时候,如果我们新建一个工程或者文件,这些IDE通常都会提供多步的向导帮助我们一步一步完成相应部件的创建。在某些Web应用程序中,也存在类似的操作场景,比如,要注册某个网站的会员,注册过程可能就包括多步,每一步会提示输入某一方面的信息,以帮助我们简化操作流程。对于这种向导式的简单的多页面流程实现,我们可以求助于AbstractwizardFormController,它可以帮助我们简化类似场景的开发工作。

注意:实际上,AbstractWizardFormController也只是面向简单的多页面流程处理,它所管理的多个页面表单中的数据,最终都是绑定到一个Command对象上的。只不过是每个页面绑定一部分而已。如果要在Web应用程序中处理更复杂的页面流程交互,建议集成Spring Web Flow(http://www.springframework.org/Webflow),Expert Spring MVC and Web Flow一书对Spring Web Flow进行了详尽的介绍。

1. AbstractwizardFormController浅析

说白了,AbstractWizardFormController实现类就是要处理组成向导流程的所有页面所发起的Web请求。 但与SimpleFormController处理单个表单页面不同,AbstractWizardFormController要管理多个表单页面的显示以及提交数据的处理工作。 图24-12对AbstractWizardFormController如何管理整个向导流程给出了一个概况,我们对AbstractWizardFormController的认识将在这个的基础上展开。

image-20220709111320241

Pagel、Page2和Page3是组成向导流程的页面,这些页面提交的Web请求将统一由我们的AbstractwizardFormController接收并处理。但AbstractwizardFormController如何知道当前该显示哪个页面?它又是如何判断哪个页面提交的请求是最终的结束向导流程的Web请求呢?AbstractWizardFormController将根据每个页面请求所包含的特定参数来决定如何对当前请求进行处理,这些特定的参数我们可以分为如下三类。

  • PARAM_TARGET参数。PARAM_TARGET参数用于指定目标页面,它的形式为_target后缀页面索引,比如_target0_target1等。当AbstractwizardFormController实现类接收到这种类型的参数的时候,它只是将当前请求中的相应参数绑定到Command对象上,然后根据PARAM_TARGET参数后缀的目标页面索引显示相应的页面。

  • PARAM_FINISH参数。PARAMFINISH参数的表示形式为_finish。如果AbstractWizardFormController实现类接收到该参数,则表示整个向导流程结束,可以调用processFinish(..)方法处理最终的表单数据,并将页面转向任何一个想要转到的页面。

  • PARAM_CANCEI参数。PARAM_CANCEL参数的表示形式为_cancel。当接收到该参数的时候,表示用户要取消当前的向导流程。默认情况下,AbstractWizardFormController不支持该参数的处理,但我们可以通过覆写processCancel(..)方法来改变这种默认行为。唯一要做的,可能只是返回一个ModelAndView实例,其中只包含了一个逻辑视图名。

实际上,AbstractwizardFormController实现类本质上依然是像SimpleFormController那样,分两个阶段来管理表单页面的处理,只不过是从逻辑上将单个表单页面划分为了多个表单页面,而最终绑定数据的Command对象却只有一个。AbstractWizardFormController将根据_targetX参数决定显示表单页面的某一部分,对应到视图那就是显示哪个向导页面。在该向导页面提交请求之后,AbstractWizardFormController会把它显示并提交的那部分数据绑定到唯一的那个Command对象上,只有在所有向导页面都提交之后,Command对象的数据才算绑定完成。从这样的角度来说,AbstractwizardFormController只不过是将一部分信息划分为多步来显示和处理罢了。

我们在向导(Wizard)最终结束的画面提交_finish参数之后,AbstractWizardFormController将从Commana对象获取到所有向导页面(Wizard Page)搜集来的数据,然后它就可以调用processFinish(..)方法开始“压轴戏”了。实际上,在实现AbstractWizardFormController的时候,也只有processFinish(..)方法是需要我们必须去实现的。

我想,以上的分析内容已经足以说明AbstractWizardFormController的底细。不过,如果你还意犹未尽的话,不妨再研读一下Expert Spring MVC and Web Flow一书。像如何提供数据验证,如何覆写某些方法以添加扩展逻辑之类的细节,该书都有提及。相信我,因为我已经读过了。

2. AbstractwizardFormController实例

我搜刮了FX系统的前台和后台的所有场景,也没有找到一个需要向导的流程。所以,为了演示AbstractwizardFormController的使用,我也不能免俗,创造了一个调查问卷的场景。我们将分三步引导用户完成对购车意愿的问卷调查。 当然,整个问卷调查看起来有些简单了,但如果需要,我们也可以在这个基础上提供更加友好、更加全面的问卷调查向导。

为了分步显示调查的内容,我们提供了4个页面分别显示整体的某一部分内容,它们的关系如下所述。

welcome.jsp。问卷调查向导的开始页面,提供简单的说明信息,如图24-13所示。

image-20220710133058580

carManufacturers.jsp。该页面将提供汽车厂商信息列表供用户选择,如图24-14所示。

image-20220710133109485

carPriceScope.jsp。向导流程中最后一个问卷调查页面,显示意愿购车价格区间,点击“结束问卷”提交按钮即结束当前问卷调查,见图24-15。

image-20220710133137513

surveyResult.jsp。确切地讲,该页面不能算向导的一部分,它只是调查结束后的一个结果显示页面,对应的是图24-12中的successView。当然,将具体数据转换为柱状图显示或许更讨人爱一些,该页面最终效果如图24-16所示。

image-20220710133233836

现在让我们着手实现该向导流程。

购车意愿问卷调查功能实现流程概述

(1)构建业务层支持。

我们首先定义了ICarSurveyService接口作为业务层的对外窗口,如下所示:

public interface ICarSurveyService {
    
    
	List<String> getAvailableCarManufacturers();
	List<String> getAvailableCarPriceScope();
	SurveyResult addSurvyForSum(CarTrend carTrend);
}

前两个方法将分别返回可供用户选择的调查选项,最后一个方法会将当前用户的问卷调查结果添加到系统中,然后返回一个目前为止总的调查结果。 当然,要让ICarSurveyService帮忙,我们依然需要给出它的一个实现类。不过,因为这个MockObject实现要添加模拟数据,所以看起来太长。这里就不做罗列了,感兴趣可以猜猜是怎么玩的,反正实际系统中,我们不可能捏造数据,大都是从数据库中抽取,对吧?不管怎么样,在有了ICarSurveyService的实现类之后,一定不要忘了添加到顶层WebApplicationContext中,以便相应的Controller能够获得它的鼎力支持。

CarTrend类是我们将使用的Command对象类型(当然,我们并非让“作为Command对象”成为它存在的唯一目的),其两个属性(property)将分别保存当前用户提交的不同类型的问卷结果。该类的最终定义如下方代码清单所示。

public class CarTrend {
    
    
	private String carFrom;
	private String carPriceScope;
  
	public String getCarFrom() {
    
    
		return carFrom;
  	}
	public void setCarFrom(String carFrom) {
    
    
		this.carFrom = carFrom;
  	}
	public String getCarPriceScope() {
    
    
		return carPriceScope;
  	}
  	public void setCarPriceScope(String carPriceScope) {
    
    
		this.carPriceScope = carPriceScope;
  	}
}

SurveyResult只用来保存每种问卷调查项以及每项的最终结果数据 ,其定义如下方代码清单所示。

public class SurveyResult {
    
    
	private List<CarManufacturerStatus> carManufacturerStatus;
	private List<CarPriceScopeStatus> carPriceScopeSatus;
  
	public SurveyResult() {
    
    
		carManufacturerStatus = new ArrayList<CarManufacturerStatus>();
		carPriceScopeSatus = new ArrayList<CarPriceScopeStatus>();
  	}

  	public List<CarManufacturerStatus> getCarManufacturerStatus() {
    
    
		return carManufacturerStatus;
  	}
  
	public void setCarManufacturerStatus(List<CarManufacturerStatus> carManufacturerStatus) {
    
    
		this.carManufacturerStatus = carManufacturerStatus;
  	}

  	public List<CarPriceScopeStatus> getCarPriceScopeSatus() {
    
    
		return carPriceScopeSatus;
  	}
  
	public void setCarPriceScopeSatus(List<CarPriceScopeStatus> carPriceScopeSatus) {
    
    
		this.carPriceScopeSatus = carPriceScopeSatus;
  	}
}

对于surveyResult中涉及的两个值对象,我想就不用浪费篇幅了,它们只是保持每项的名称和结果值而已。

(2)构建AbstractWizardFormController实现类。

主角出场时间,下方代码清单给出的是AbstractWizardFormController具体实现类。

public class CarTrendSurveyWizaraController extends AbstractWizardFormController {
    
    
	private ICarSurveyService carSurveyService;
	private String successView;
  
	public CarTrendSurveyWizaraController() {
    
    
		setCommandClass(CarTrend.class);
  	}
  
	@Override
	protected Map referenceData(HttpServletRequest request, int page) throws Exception {
    
    
  	Map rData = new HashMap();
		switch(page) {
    
    
			case 1:
				List<String> carManufacturers = getCarSurveyService().getAvailableCarManufacturers();
				rData.put("carManufacturers", carManufacturers);
				return rData;
			case2:
				List<String> carPriceScopes = getCarSurveyService().getAvailablèCarPriceScope();
				rData.put("carPriceScopes", carPriceScopes);
				return rData;
			default:
				return null;
    }
  }
	
  @Override
	protected ModelAndView processFinish(HttpServletRequest arg0, HttpServletResponse arg1, Object command, BindException arg3) throws Exception {
    
    
		CarTrend currentCarTrend = (CarTrend)command;
		SurveyResult sr = getCarSurveyService().addSurvyForSum(currentCarTrend);
		ModelAndView mav = new ModelAndView(getSuccessView());
		mav.addObject("surveyResult", sr);
		return mav;
  	}
	// getter和setter方法定义
}

对于“提供依赖的服务对象”和“在构造函数中指定Command对象的类型”这些固定动作,估计你都熟知了,无需再提。successView是我们特地添加的一个属性,用于提供问卷结束后应该导向的视图名称, 不要将它与SimpleFormController的successView混淆。SimpleFormController已经为我们定义了这么一个属性,而AbstractwizardFormController没有,所以,要用,我们就得自己定义。

如果对于单个表单页面来说,要为其提供附加显示数据,当然是通过referenceData(..)方法进行啦。比较幸运的是,在AbstractWizardFormController实现类中,我们也是通过覆写referenceDatal..)方法做同样的工作。只不过,该referenceData(..)方法要多一个page参数,用于标识返回的数据到底要给哪个向导页面使用,谁让我们现在要管理多个页面的显示呢!

注意:页面的索引是以0开始的,尽量遵循的是数组索引的“传统习俗”,.targetx参数也是一个道理,_target0指的是向导的第一个页面,_target1指的是第二个页面,依次类推。

实现一个AbstractwizardFormController必须要提供的覆写方法就是processFinish(..)。我们在该方法中调用服务对象将当前的调查结果告知系统,然后将返回的总的问卷调查情况,通过ModelAndView公开给下一个页面,也就是successView对应的最终结果显示页面。以下是我们的CarTrendSurveyWizardController添加到特定于DispatcherServlet的WebApplicationContext的情景:

<bean name="/survey.do" c1ass="cn.spring21.simplefx.controllers.CarTrendSurveyWizardController">
	<property name="carSurveyService" ref="carSurveyService"/>
	<property names"successView" value="surveyResult"/>
	<property namem"pages" va1ue="we1come,carManufacturers,carPriceScope"/>
</bean>

AbstractWizardFormController需要知道所有组成向导流程的页面,所以,我们得通过pages属性告知一下。

注意:pages属性为String[]类型,所以,在通过value指定多个视图名的时候,我们采用逗号分隔多个视图名。为了清晰,也可以采用嵌套的<list>进行相同效果的表达。

(3)构建视图模板。

有关视图模板,我想只展示中间两个的代码就能够说明问题了,如下方代码清单所示。

image-20220710135026990

image-20220710135039656

其实,主要是想让你看看_target参数和_finish参数的样子。至于如何显示页面,其实可以因人、因项目、因太多原因而变化。

24.2.5 其他可用的Controller实现

除了我们已经了解的两大门派的几位主要弟子,Spring MVC的Controller体系下还有几个用于不同目的的Controller实现类,如下所述。

AbstractCommandController。 AbstractCommandController是规范操作派里面的少数派。它继承了BaseCommandController的数据绑定以及数据验证的功能支持,自身也定义了简单的请求处理流程,但它的主要目的不是面向表单的处理(该方面的功能已经由AbstractFormController家族接管了)。实际上,我们可以认为AbstractCommandController只是一个加强型的AbstractController。直接继承AbstractController与直接继承AbstractCommandController差不多,唯一的区别可能就是,后者进一步规范了参数的获取和验证操作,从如下的代码摘录中就可以看出这一点:

protected ModelAnaView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
	Object command = getComand(request);
	ServletRequestDataBinder binder = bindAndValidate(request, command);
	BindException errors = new BindException(binder.getBindingResult());
	return handle(request, response, command, errors);
}

继承AbstractCommandController,只需要实现handle(..)模板方法即可,因为之前数据绑定和验证都已经搞定了。这就好比继承AbstractController,我们只负责实现一个handleRequestInternal(..)模板方法,而像页面缓存的设置等功能,则由AbstractController这个父类帮我们搞定。

总之,通过继承AbstractCommandController实现一个Controller,我们只需要关心数据绑定和验证之后的处理逻辑的实现即可。

ParameterizableViewController。 ParameterizableViewController的实现逻辑十分简单,或者说干脆就没有处理逻辑,只是返回一个只包含指定逻辑视图名的ModelAndView实例,其请求处理方法的代码如下所示:

protected ModelAnaView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
	return new ModelAndView(getViewName());
}

该Controller实现类的主要目的是为了帮助我们将“对具体视图的直接请求”纳入到SpringMVC的统一处理体系之下,这样,我们依然可以依赖于ViewResolver的映射能力向客户端屏蔽不同视图类型的差异性。比如,如果我们在页面内只是想通过超链接(HyperLink)访问某个页面(比如/WEB-INF/jsp/help/Help4sth.jsp),期间不需要任何处理,那么我们就可以在特定于DispatcherServlet的WebApplicationContext中添加如下Controller定义:

<bean name="/simpleFileRequest.do" class="org.springframework.Web.servlet.mvc.ParameterizableViewController">
	<property name="viewName" value="help/Help4sth"></property>
</bean>

通过viewName指定具体的视图名,ParameterizableViewController将不做任何处理,直接将对应“simpleFileRequest.do”的请求导向指定的视图。

UrlFilenameViewController。 使用ParameterizableViewController一次只能映射一个视图文件。如果有一组视图文件都需要不做任何处理直接返回的话,我们就得使用UrlFilenameViewController。比如,大部分系统中可能会有专用的系统帮助信息,这些帮助信息大都实现为静态信息页面,客户端对这些帮助信息页面的请求将不需要做任何预处理。假设我们的帮助页面都存放在/WEB-INF/jsp/help/目录下,为了免去一个页面配置一个处理Controller之苦,我们就可以在特定于DipatcherServlet的WebApplicationContext中添加一个UrlFilenameViewController的定义,让它全权管理对帮助页面的访问,如下所示:

<bean name="/help/*" class="org.springframework.Web.servlet.mvc.UrlFilenameViewController">
	<property name="prefix" value="help/"></property>
</bean>

客户端通过如下链接方式访问,即可访问到需要的帮助页面(对应如下链接的是/WEB-INF/jsp/help/Help4sth.jsp):

<a href="<c:url value='help/Help4sth.do'/>">Get Help</a>

我们可以通过UrlFilenameViewController的prefix和suffix属性进一步修饰UrlFilenameViewController的映射能力,有关这些内容可以参考该类的Javadoc文档。

ServletForwardingController和ServletWrappingController。 ServletForwardingController是将对当前Controller的请求转发(Forward)给当前应用中定义的某个Servlet,也就是说,将Controller与Servlet的地位拉到了同一个水平。而ServletWrappingController则是将当前应用中的某个Servlet直接包装为一个Controller,所有到ServletWrappingController的请求实际上是由它内部所包装的这个Servlet来处理的。这两种Controller实现类更多的是为了集成现有的Servlet,比如将包含某些现有逻辑的Servlet纳入到SpringMVC的处理体系中。我们可以在ServletForwardingController和ServletWrappingController的Javadoc文档中获得使用它们的详细信息。

注意:除了AbstractCommandController希望并且也必须继承使用之外,其他几个Controller实现都不是以继承为目的而存在的,只需要相应的配置即可使用。

基本上,经典意义上的Controller实现类也就这些了。下面是Controller返回的ModelAndView出场时间。

24.3 ModelAndview

通常,Controller在将Web请求处理完成后,会返回一个ModelAndview实例。该ModelAndView实例将包含两部分内容,一部分为视图相关内容,可以是逻辑视图名称,也可以是具体的View实例;另一部分则是模型数据,视图渲染过程中将会把这些模型数据合并入最终的视图输出。 所以,简单来说,ModelAndView实际上就是一个数据对象。不过通过该数据对象,我们却可以解除具体的Web请求处理Controller与视图渲染之间的紧密耦合,使得两个方面能够独立演化。

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

public ModelAndView(String viewName)
public ModelAndView(String viewName, Map model)
public ModelAndView(String viewName, String modelName, Object modelObject)

public ModelAndView(View view)
public ModelAndView(View view, Map model)
public ModelAndView(View view, String modelName, Object modelObject)

每组的第一个构造方法只接受视图信息,所以,构造完成后,我们得通过addAllObject(..)或者addObject(..)实例方法,向构造完成的ModelAndView实例添加模型数据;每组第二个构造方法则可以同时指定视图信息和模型数据信息,一步到位;如果要添加到模型的只有一个数据对象,那么可以使用每组的第三个构造方法,该构造方法属于第二个构造方法的简化版。除了以上的构造方法之外,ModelAndView还有一个默认的没有参数的构造方法,如果使用该构造方法实例化对象,那么之后就需要使用其他实例方法来设置视图和模型数据信息了(听起来有点儿像废话哦)。

24.3.1 ModelAndview中的视图信息

恰如两组构造方法所表明的那样,ModelAndView可以以逻辑视图名的形式或者View实例的形式来保存视图信息。如果ModelAndView中直接返回了具体的View实例,那么,DispatcherServlet将直接从ModelAndView中获取该View实例并渲染视图,如下所示:

View view = null;
view = mv.getView();
view.render(mv.getModelInternal(), request, response);

否则(当然是返回逻辑视图名称啦),DispatcherServlet将寻求ViewResolver的帮助,根据ModelAndView中的逻辑试图名称获取一个可用的View实例,然后再渲染视图,如下所示:

View view = null;
...
view = resolveViewName(mv.getViewName(), mv.getModelInterna1(), locale, request);
view.render(mv.getModelInternal(), request, response);

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

24.3.2 ModelAndview中的模型数据

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

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

image-20220710141722264

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

24.4 视图定位器ViewResolver

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

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

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.springframework.web.servlet.view.AbstractCachingViewResolver。因为针对每次请求都重新实例化View将可能为Web应用程序带来性能上的损失,所以Spring MVC在AbstractCachingViewResolver这一继承层次加入了View实例的缓存功能。AbstractCachingViewResolver默认启用View的缓存功能。对于生产环境来说,这是合理的默认值。不过,如果在测试或者开发环境下,我们想即刻反映相应的修改结果,可以通过setCache(false)暂时关闭AbstractCachingViewResolver的缓存功能。

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

24.4.1 可用的ViewResolver实现类

为了便于理解,我们可以将SpringMVC提供的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实例返回,诸如此类。

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

  • InternalResourceviewResolver。 它是我们使用最多的ViewResolver实现类型,它对应InternalResourceView视图类型的映射,说白了也就是处理JSP模板类型的视图映射。如果DispatcherServlet在初始化的时候,不能在自己的WebApplicationContext中找到至少一个ViewResolver,那么,InternalResourceViewResolver将作为默认的ViewResolver被使用。

  • FreeMarkerViewResolver/VelocityViewResolver。 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的使用,你基本上就可以“举一反三”了,更加详尽的配置项,可以参考对应类的Javadoc或者Professional Java Development with the Spring Framework一书中对应视图章节的介绍内容。下面给出了针对VelocityViewResolver的配置代码示例:

<bean id="viewResolver" class="org.springframework.Web.servlet.view.velocity.VelocityViewResolver">
	<propertyname="prefix" value="../velocity/"/>
	<propertyname="suffix" value=".vm"/>
</bean>

现在DispatcherServlet对视图的请求将会由VelocityViewResolver接管,VelocityViewResolver将根据传入的逻辑视图名,到指定目录下查找.vm类型的Velocity模板文件,并构造VelocityView实例返回给DispatcherServlet使用。就跟我们所说的那样,它只负责到指定位置查找对应Velocity的单一视图类型,而不会返回其他,比如Freemarker视图对应的View实例。注意关于使用Velocity作为视图技术需要附加的配置内容,可以参考稍后ResourceBundleViewResolver部分的附带信息。

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)的形式了(当然,实际上要做的事情会多一些)。

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

<bean id="resourceBundleViewResolver" class="org.springframework.Web.servlet.view.ResourceBundleViewResolver">
</bean>

如果我们没有指定properties配置文件从何处加载的话,ResourceBund1eViewResolver默认将从classpath的根路径加载以views为basename的properties文件,比如views.properties、views_zh_CN.properties等。如果我们想改变这种默认加载行为,可以通过setBasename(String)或者setBasenames(String[])方法来进行变更。以下是一个典型的ResourceBundleViewResolver使用的properties配置文件内容:

viewTemplate.class=org.springframework.Web.servlet.view.InternalResourceView
viewTemplate.(abstract)=true

help/HelpForSomething.(parent)=viewTemplate
help/HelpForSomething.url=/WEB-INF/jsp/he1p/He1pForSomething.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的配置如下:

<bean id="velocityConfig" class="org.springframework.Web.servlet.view.velocity.VelocityConfigurer">
	<property name="configLocation" value="/WEB-INF/ve1ocity-config.properties"/>
</bean>

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

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

resource.loader=classpath
c1asspath.resource.loader.description=ClasspathResourceLoader
classpath.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
c1asspath.resource.loader.path=.
velocimacro.1ibrary=

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

XmlViewResolver。 XmlViewResolver与ResourceBundleViewResolver之间最主要的区别就是,它们所采用的配置文件格式不同。ResourceBundleViewResolver按照SpringIoC容器所接受的properties配置格式配置逻辑视图名与具体视图之间的映射关系,而XmlViewResolver则是按照SpringIoC容器接受的XML配置文件格式来加载映射信息。与ResourceBundleviewResolver同样的配置信息,使用xmlViewResolver的话,内容如下方代码清单所示。

<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/he1lo.vm">
</bean>

现在,XmlViewResolver将从Classpath的根路径加载名为views.xml的配置文件。至于其他配置,比如Velocity需要的VelocityConfigurer,因为与使用何种ViewResolver没有关系,只与是否使用Velocity作为视图技术有关,所以依然需要根据情况添加到WebApplicationContext中。

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

BeanNameViewResolver。 BeanNameViewResolver可以认为是XmlViewResolver的原型版或者简化版。使用它,我们可以直接将View实例注册到当前DispatcherServlet所使用的特定的WebApplicationContext中,而不用像XmlViewResolver那样另辟一块地。不过,BeanNameViewResolver更多地用于快速搭建应用框架原型,或者构建小型的Web应用程序。对于正常的基于SpringMVC的Web应用程序,应尽量避免将可以分离出来的视图配置信息一并加入到DispatcherServlet的WebApplicationContext中。

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

<bean id="beanNameViewResolver" class="org.springframework.Web.servlet.view.BeanNameViewResolver"/>

至于具体视图的配置,参照xmlViewResolver即可。实际上,正如我们所看到的那样,这三种ViewResolver在本质上是一样的,只不过是配置的表现形式上存在差异而已。最终的配置信息都将转换为Spring IoC容器中管理的View实例,BeanNameViewResolver应该是最初的实现原型吧!

24.4.2 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中添加如下配置内容:

<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的后备查找对象。

猜你喜欢

转载自blog.csdn.net/qq_34626094/article/details/125770913