处理命令:当控制器需要根据参数执行工作时,应该继承命令控制器,如AbstractCommandController(见PPT7)这个控制器会自动将参数绑定到命令对象中,并且提供了插入验证器的钩子,确保参数合法性。下面显示了RantsForVehicleController,一个用于显示特定车辆已有投诉列表的命令控制器。
public class RantsForVehicleController extends AbstractCommandController{
public RantsForVehicleController() {
setcommandClass(Vehicle.class);
setCommandName("vehicle");
}
protected ModelAndView handle(HttpServletRequest request,HttpServletResponse,Object command,
BindException errors) throws Exception {
Vehicle vehicle = (Vehicle) command;
List vehicleRants = rantService.getRantsForVehicle(vehicle);
Map model =errors.getModel();
model.put("rants", rantService.getRantsForVehicle(vehicle));
model.put("vehicle", vehicle);
return new ModelAndView("vehicleRants",model);
}
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
}
一个命令对象是一个为了简化对请求参数访问而设计的Bean。一个Spring命令对象是一个POJO,它不需要实现任何Spring的特定类。你需要在roadrantz-servlet.xml文件中注册RantsForVehicleController
<bean id="rantsForVehicleController" class="com.roadrantz.mvc.RantsForVehicleController">
<property name="rantService" ref="rantService"/>
</bean>
处理表单提交:表单控制器比命令控制器前进了一步,(见PPT8)它在接收到HTTP GET请求的时候显示一个表单,接收到一个HTTP POST请求的时候处理这个表单。另外,在处理过程中如果发生错误的话,这个控制器会知道重新显示这个表单,这样用户就可以修改错误,重新提交。为了显示表单控制器是如何工作的,考虑以下程序中的AddRantFormController。
public class AddRantFormController extends SimpleFormController {
private static final String[] ALL_STATES = {"AL","AK","AZ"};
public AddRantFormController() {
setCommandClass(Rant.class);
setCommandName("rant");
}
protected Object formBackingObject(HttpServletRequest request) throws Exception {
Rant rantForm = (Rant)super.formBackingObject(request);
rantForm.setVehicle(new Vehicle());
}
protected Map referenceDate(HttpServletRequest request) throws Exception{
Map referenceData = new HashMap();
referenceDate.put("states",ALL_STATES);
return referenceData;
}
protected ModelAndView onSumbit(Object command,BindException bindException)throws Exception{
Rant rant = (Rant)command;
rantService.addRant(rant);
return new ModelAndView(getSuccessView());
}
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
}虽然referenceData()方法是可选的,但是如果需要为 显示表单提供其他附加的信息,则可以使用这个方法。在正常的情况下,返回表单的命令对象一般是一个简单的命令类实例。不过对于AddRantFormController,Rant实例不会完成这项工作。表单会使用内嵌的Vehicle属性作为表单返回对象的一部分。因此,需要覆盖fromBackingObject()方法,以便设置vehicle属性。否则,在控制器视图绑定state和plateNumber属性时,会抛出NullPointerException异常。
那么控制器是如何知道显示投诉输入表单的。如果投诉输入成功,用户将到什么页面也不是很清楚。唯一的线索是对getSuccessView()的调用结果会提交给ModelAndView。但是,提交成功的视图又从哪里来呢?SimpleFormController被设计成尽量将视图详细信息放在控制器代码之外。不是将ModelAndView对象硬编码进来,而是像下面这样在上下文配置文件中配置控制器:
<bean id="addRantController" class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" ref="rantService" />
</bean> formView属性是控制器接收到HTTP GET请求或有任何错误发生时需要显示的视图的逻辑名。同样,successView是提交的表单成功处理后要显示的视图的逻辑名。
验证表单输入:org.springframework.validation.Validator接口为Spring MVC提供了验证功能,定义如下:
public interface Validator {
void validate(Object obj , Errors errors);
boolean supports(Class clazz);
}这个接口的实现必须验证传递给validate()方法的对象的字段,用Errors对象驳回任何非法数据。supports()方法用于帮助Spring判断该验证器是否可以用于指定类。 以下是一个用于验证Rant对象的Validator实现。
public class RantValidator implements Validator {
public boolean supports(Class clazz){
return clazz.equals(Rant.class);
}
public void validate(Object command, Errors errors) {
Rant rant = (Rant) command;
ValidationUtils.rejectIfEmpty(errors,"vehicle.state","required.state","State is required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "rantText", "required.rantText","You must enter some rant text.");
validatePlateNumber(rant.getVehicle().getPlateNumber(),errors);
}
private static final String PLATE_REGEXP = "/[a-z0-9]{2,6}/i";
private void validatePlateNumber(String plateNumber,Errors errors) {
Per15Util per15Util = new Per15Util();
if(!per15Util.match(PLATE_REGEXP,plateNumber)){
errors.reject("invalid.plateNumber","Invalid license plate number.");
}
}
}还有一件事情要做,就是让AddRantFormController使用RantValidator。你可以将一个RantValidator Bean装配到AddRantFormController Bean中:
<bean id="addRantController" class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant"/>
<property name="successView" value="rantAdded"/>
<property name="rantService" ref="rantService"/>
<property name="validator">
<bean class="com.roadrantz.mvc.RantValidator"></bean>
</property>
</bean>通过实现Validation接口,可以通过程序完全控制应用程序命令对象的验证。如果需要复杂的验证和特殊的逻辑,利用这项功能将会十分方便。对于简单的情况,例如保证填入需要的字段并按照基本格式,编写自己的Validator接口就有点麻烦了。声明性验证是一个不错的选择。
利用命令验证器进行验证:在我们深入研究用于实现声明性Validator的Spring JavaDoc之前,需要知道Spring并不提供这样的验证器。Spring没有提供任何Validator接口的实现,而是将这个任务留给了程序员。 不过Spring Modules项目(http://springmodules.dev.java.net)是Spring的一个姊妹项目,提供了几个对Spring的扩展。其中一个是使用Jakarta Commons Validator(http://jakarta.apache.org/commons/validator)提供的声明性验证的验证模块。
要使用验证模块,首先需要添加springmodules-validator,jar 如果使用Ant建立应用程序,需要下载Spring Modules发行包,找到spring-modules-0.6.jar文件,将这个JAR添加到<war>任务的<lib>中。如果使用Maven2建立应用程序,需要在pom.xml中添加下面的<dependency>:
<dependency>
<groupId>org.springmodules</groupId>
<artifactId>springmodules-validation</artifactId>
<version>0.6</version>
<scope>compile</scope>
</dependency>
另外,还需要将Jakarta Commons Validator JAR添加到应用程序的classpath中,在Maven中,按如下所示:
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.1.4</version>
<scope>compile</scope>
</dependency>
Spring Module提供的Validator实现的名称为DefaultBeanValidator。DefaultBeanValidator需要按下面的方式配置在roadrantz-servlet.xml中:
<bean id="beanValidator" class="org.springmodules.commons.validator.DefaultBeanValidator">
<property name="validatorFactory" ref="validatorFactory"></property>
</bean>
DefaultBeanValidator并不做任何实际的验证工作,而是委派Commons Validator来验证字段的值。validatorFactory Bean需要使用下面的XML进行声明:
<bean id="validatorFactory" class="org.springmodules.commons.validator.DefaultValidatorFactory">
<property name="validationConfigLocations">
<list>
<value>WEB-INF/validator-rules.xml</value>
<value>WEB-INF/validator.xml</value>
</list>
</property>
</bean> validator-rules.xml文件包含了一组预定义的验证规则,可以应用于一般的验证需求。这个文件被包含在Commons Validator发行包中,因此,你不需要自己编写--值需要简单的将其添加到应用程序的WEB-INF目录中。(PPT9)中列出了validator-rules.xml中的所有验证规则。另一个文件validation.xml定义了应用程序制定的验证规则,可有直接应用于RoadRantz应用程序。下面展示了应用到RoadRantz的validation.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1
//EN" "http://jakarta.apache.org/commons/dtds/validator_1_1.dtd" >
<form-validation>
<formet>
<form>
<field property="rantText" depends="required">
<arg0 key="required.rantText"/>
</field>
<field property="vehicle.plateNumber" depends="required,mask">
<arg0 key="invalid.plateNumber"/>
<var>
<var-name>mask</var-name>
<var-value>^[0-9A-Za-z]{2,6}$</var-value>
</var>
</field>
</form>
</formet>
</form-validation>
最后一件要做的事情是改变控制器的声明,注入新的声明性Validate实现:
<bean id="addRantController" class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" value="rantService" />
<property name="validator" value="beanValidator" />
</bean>
使用SimpleFormController的一个基本的原因是表单只有一页。
用向导处理复杂表单:AbstractWizardFormController是Spring提供的功能最强大的控制器(见PPT10)它是一种特殊类型的表单控制器,它将多个页面中的表单数据聚集到一个用于处理的命令对象中。
-
创建一个基本的向导控制器:构建一个向导控制器,它必须继承AbstractWizardFormController类,如下:
public class MotoristRegistrationController extends AbstractWizardFormController {
public MotoristRegistrationController() {
setCommandClass(Motorist.class);
setCommandName("motorist");
}
protected Object formBackingObject(HttpServletRequest request) throws Exception{
Motorist formMotorist = new Motorist();
List<Vehicle> vehicles = new ArrayList<Vehicle>();
vehicles.add(new Vehicle());
formMotorist.setVehicles(vehicles);
return formMotorist;
}
protected Map referenceData(HttpServletRequest request,Object command,Errors errors,int page)
throws Exception{
Motorist motorist = (motorist) command;
Map refData = new HashMap();
if(page==1 && request.getParameter("_target1")!= null){
refData.put("nextVehicle", motorist.getVehicles().size()-1);
}
return refData;
}
protected void postProcessPage(HttpServletRequest request,Object command,Errors errors,int page)throws Exception {
Motorist motorist = (Motorist)command;
if(page==1 && request.getParameter("_target1")!= null){
motorist.getVehicles().add(new Vehicle());
}
}
protected ModelAndView processFinish(HttpServletRequest request,HttpServletResponse response,
Object command,BindException errors)throws Exception {
Motorist motorist = (motorist) command;
// the last Vehicle is always blank...remove it
motorist.getVehicles().remove(motorist.getVehicles().size()-1);
rantService.addMotorist(motorist);
return new ModelAndView(getSuccessView(),"motorist",motorist);
}
//inject
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
//returns the last page as the success view
private String getSuccessView() {
return getPages()[getPages().length-1];
}
}
AbstractWizardFormController的唯一一个必须实现的方法是processFinish()。在用户完成表单后(一般是点击完成按钮),这个方法被调用,完成整个表单。 但是AbstractWizardFormController是如何知道哪些页面构成了整个表单呢?
<bean id="registerMotoristController" class="com.roadrantz.mvc.MotoristRegistrationController">
<property name="rantService" ref="rantService" />
<property name="pages">
<list>
<value>motoristDetailForm</value>
<value>motoristVehicleForm</value>
<value>motoristConfirmation</value>
<value>redirect:home.htm</value>
</list>
</property>
</bean>这样向导就知道了哪些页面构成了表单,一个视图逻辑名列表设置给了pages属性。这些名字最终被视图解析器解析成了一个个视图对象。但现在,只要假设这些名字会被解析成JSP文件名就可以了。
分步显示表单页面:任何向导控制器显示的第一个页面都是pages属性中列表的第一个页面。AbstractWizardFormController询问它的getTargetPage()方法。这个方法返回一个整数,它是pages属性中设置的页面列表的索引值(以0为基数)。getTargetPage()方法的默认实现是根据请求中的一个参数来决定下一步是哪个页面,这个参数以"_target"开头,以数字结尾。知道了getTargetPage()的工作原理有助于你知道如何在向导HTML页面中构造下一步和上一步按钮。例如,假设用户在"motoristVehicleForm"页面上(索引=1)。要在这个页面上创建下一步和上一步按钮,你要做就是创建提交按钮,它的名字是以"_target"开头:
<form>
...
<input type="submit" value="Back" name="_target0">
<input type="submit" value="Next" name="_target2">
</form>getTargetPage()方法的默认实现对于大多数项目已经够用了。然后,如果你喜欢为你的向导定义自己的工作流程的话,你可以覆写这个方法。
完成向导:还有一个特殊请求的参数"_finish",和"_targetX"参数一样,它可以被用于在页面上创建一个结束按钮:
<form method="POST" action="feedback.htm">
...
<input type="submit" value="Finish" name="_finish">
</form>
当AbstractWizardFormController看到请求中的"_finish"参数时,它会将控制权交给processFinish()方法,让它对表单做最后的处理。与其他表单控制器不同,AbstractWizardFormController不提供用于设置成功视图页面的内容。因此,我们在MotoristRegisttrationController中添加了一个getSuccessView()方法返回页面列表中的最后一个页面。所以,当表单已完成的方式提交时,processFinish()方法会返回一个带有视图的ModelAndView,这个视图就是页面列表中的最后一个视图。
取消向导:如何用户完成了部分注册,决定不再继续完成,除了选择直接关闭浏览器外,还有另一种选择,你可以在表单上添加一个取消按钮。
<form method="POST" action="feedback.htm">
...
<input type="submit" value="Cancel" name="_cancel">
</form>
取消按钮以"_cancel"作为它的名字,当用户按下取消按钮,浏览器将一个叫做"_cancel"的参数放到请求中。AbstractWizardFormController接收到这个参数,将控制权交给processCancel()方法。 默认情况下此方法会抛出异常,表示取消操作是不被支持的,我们要覆写这个方法,将用户带到你想让他们点击取消时看到的页面。下面processCancel()方法的实现将用户带领到成功视图。
protected ModelAndView processCancel(HttpServletRequest request, HttpServletResponse response, Object command, BindException bindException) throws Exception {
return new ModelAndView(getSucessView()) ;
}
每次验证一个向导表单:使用其他类型的命令控制器,命令对象只装载一个次。但使用向导控制器,用户每完成向导页面中的一步,都会有一个命令对象设置进来。使用向导,只做一次验证是不可行的,太早的话,找到的验证问题可能是由于用户没有完成向导而导致的。太晚的话,在完成按钮被按下后再做检查就太迟了,因为发现的问题可能越过了多个页面(用户该回到哪个页面呢?) 向导控制器在每个页面验证一次命令对象,不是只验证一次。这是通过每次页面跳转时调用validatePage()方法实现的。validatePage()方法的默认实现是空的(也就是没有验证),但是你可以覆写这个方法,做出自己的判断。 假设在motoristDetailForm页面,询问用户的邮件地址,这个字段是可选的,但是如果输入了值,必须输入一个合法的E-mail地址。下面的validatePage()方法展示了用户从motoristDetailForm页面跳转出来的时候如何验证E-mail地址。
protected void validatePage(Object command, Errors errors, int page ) {
Motorist motorist = (Motorist) command;
Motorist Validator validator = (MotoristValidator) getValidator();
if(page == 0){
validator.validateEmail(motorist.getEmail() , errors );
}
} 这里可以直接在validatePage()方法中检查E-mail。然后,向导一般有好几项字段需要验证。如果这样的话,validatePage()方法会变得很笨拙。我们建议你将验证任务委托给控制器的验证器对象的字段级验证方法,就像我们在这里调用MotoristValidator的validateEmail()方法一样。 这意味着当你配置控制器的时候,你要设置validator属性:
<bean id="registerMotoristController" class="com.roadrantz.mvc.MotoristRegistrationController">
<property name="rantService" ref="rantService" />
<property name="pages">
<list>
<value>motoristDetailForm</value>
<value>motoristVehicleForm</value>
<value>motoristConfirmation</value>
<value>redirect:home.htm</value>
</list>
</property>
<property name="validator">
<bean class="com.roadrantz.mvc.MotoristValidator" />
</property>
</bean>
一个需要注意的重要事项是,不像其他命令控制器,向导控制器从不调用它们的验证器对象的标准validate()方法。这是因为validate()方法验证整个命令对象,然而在向导中命令对象将在每个页面验证一次。
使用一次性控制器:一次性控制器比其他控制器简单很多,ThrowawayController接口可以证明:
public interface ThrowawayController {
ModelAndView execute() throws Exception ;
}
ThrowawayController接口和Controller接口不在同一个体系里。一次性控制器自己作为自己作为自己的命令对象,而不是通过一个HttpServletRequest或一个命令对象获得参数。与WebWork Action相似,都是以同样的方式工作。我们将RantsForController实现为ThrowawayController:
public class RantsForDayController implements ThrowawayController{
private Day day;
public ModelAndView execute() throws Exception {
List<Rant> dayRants = rantService.getRantsForDay(day);
return new ModelAndView("dayRants","rants",dayRants);
}
public void setDay(Date day){
this.day = day;
}
private RantService rantSrvice;
public void setRantSrvice(RantService rantSrvice) {
this.rantSrvice = rantSrvice;
}
} 你必须在DispatcherServlet的上下文配置文件中声明一次性控制器。只有一点很小的不同:
<bean id="rantsForDayController" class="com.roadrantz.mvc.RantsForDayController" scope="prototype">
<property name="rantService" ref="rantService">
</bean> scope属性已经被设置为prototype。这就是一次性控制器获取它们名字的地方。默认情况下,所有的Bean都是Singleton。 但是因为ThrowawayContoller和Controller不处于同一继承层次,DispatcherServlet不知道如何通知ThrowawayController。所以你必须告诉DispathcerServlet使用一种不同的 处理适配器。确切的说,你必须像下面这样配置ThrowawayControllerHandlerAdapter:
<bean id="throwawayHandler" class="org.springframework.web.servlet.mvc.throwaway.ThrowawayControllerHandlerAdapter" />
如果应用程序既使用了常规控制器又使用了一次性控制器,你还应该按如下方式声明SimpleControllerHandleAdapter:
<bean id="simpleHandler" class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
声明两处理器适配器使你可以在用一个应用程序中混合使用两种类型的控制器。