處理命令:當(dāng)控制器需要根據(jù)參數(shù)執(zhí)行工作時,應(yīng)該繼承命令控制器,如AbstractCommandController(見PPT7)這個控制器會自動將參數(shù)綁定到命令對象中,并且提供了插入驗證器的鉤子,確保參數(shù)合法性。下面顯示了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;
}
}
一個命令對象是一個為了簡化對請求參數(shù)訪問而設(shè)計的Bean。一個Spring命令對象是一個POJO,它不需要實現(xiàn)任何Spring的特定類。你需要在roadrantz-servlet.xml文件中注冊RantsForVehicleController
<bean id="rantsForVehicleController" class="com.roadrantz.mvc.RantsForVehicleController">
<property name="rantService" ref="rantService"/>
</bean>
處理表單提交:表單控制器比命令控制器前進了一步,(見PPT8)它在接收到HTTP GET請求的時候顯示一個表單,接收到一個HTTP POST請求的時候處理這個表單。另外,在處理過程中如果發(fā)生錯誤的話,這個控制器會知道重新顯示這個表單,這樣用戶就可以修改錯誤,重新提交。為了顯示表單控制器是如何工作的,考慮以下程序中的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實例不會完成這項工作。表單會使用內(nèi)嵌的Vehicle屬性作為表單返回對象的一部分。因此,需要覆蓋fromBackingObject()方法,以便設(shè)置vehicle屬性。否則,在控制器視圖綁定state和plateNumber屬性時,會拋出NullPointerException異常。
那么控制器是如何知道顯示投訴輸入表單的。如果投訴輸入成功,用戶將到什么頁面也不是很清楚。唯一的線索是對getSuccessView()的調(diào)用結(jié)果會提交給ModelAndView。但是,提交成功的視圖又從哪里來呢?SimpleFormController被設(shè)計成盡量將視圖詳細(xì)信息放在控制器代碼之外。不是將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請求或有任何錯誤發(fā)生時需要顯示的視圖的邏輯名。同樣,successView是提交的表單成功處理后要顯示的視圖的邏輯名。
驗證表單輸入:org.springframework.validation.Validator接口為Spring MVC提供了驗證功能,定義如下:
public interface Validator {
void validate(Object obj , Errors errors);
boolean supports(Class clazz);
}這個接口的實現(xiàn)必須驗證傳遞給validate()方法的對象的字段,用Errors對象駁回任何非法數(shù)據(jù)。supports()方法用于幫助Spring判斷該驗證器是否可以用于指定類。 以下是一個用于驗證Rant對象的Validator實現(xiàn)。
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>通過實現(xiàn)Validation接口,可以通過程序完全控制應(yīng)用程序命令對象的驗證。如果需要復(fù)雜的驗證和特殊的邏輯,利用這項功能將會十分方便。對于簡單的情況,例如保證填入需要的字段并按照基本格式,編寫自己的Validator接口就有點麻煩了。聲明性驗證是一個不錯的選擇。
利用命令驗證器進行驗證:在我們深入研究用于實現(xiàn)聲明性Validator的Spring JavaDoc之前,需要知道Spring并不提供這樣的驗證器。Spring沒有提供任何Validator接口的實現(xiàn),而是將這個任務(wù)留給了程序員。 不過Spring Modules項目(http://springmodules.dev.java.net)是Spring的一個姊妹項目,提供了幾個對Spring的擴展。其中一個是使用Jakarta Commons Validator(http://jakarta.apache.org/commons/validator)提供的聲明性驗證的驗證模塊。
要使用驗證模塊,首先需要添加springmodules-validator,jar 如果使用Ant建立應(yīng)用程序,需要下載Spring Modules發(fā)行包,找到spring-modules-0.6.jar文件,將這個JAR添加到<war>任務(wù)的<lib>中。如果使用Maven2建立應(yīng)用程序,需要在pom.xml中添加下面的<dependency>:
<dependency>
<groupId>org.springmodules</groupId>
<artifactId>springmodules-validation</artifactId>
<version>0.6</version>
<scope>compile</scope>
</dependency>
另外,還需要將Jakarta Commons Validator JAR添加到應(yīng)用程序的classpath中,在Maven中,按如下所示:
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.1.4</version>
<scope>compile</scope>
</dependency>
Spring Module提供的Validator實現(xiàn)的名稱為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文件包含了一組預(yù)定義的驗證規(guī)則,可以應(yīng)用于一般的驗證需求。這個文件被包含在Commons Validator發(fā)行包中,因此,你不需要自己編寫--值需要簡單的將其添加到應(yīng)用程序的WEB-INF目錄中。(PPT9)中列出了validator-rules.xml中的所有驗證規(guī)則。另一個文件validation.xml定義了應(yīng)用程序制定的驗證規(guī)則,可有直接應(yīng)用于RoadRantz應(yīng)用程序。下面展示了應(yīng)用到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實現(xiàn):
<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的一個基本的原因是表單只有一頁。
用向?qū)幚韽?fù)雜表單:AbstractWizardFormController是Spring提供的功能最強大的控制器(見PPT10)它是一種特殊類型的表單控制器,它將多個頁面中的表單數(shù)據(jù)聚集到一個用于處理的命令對象中。
-
創(chuàng)建一個基本的向?qū)Э刂破?/strong>:構(gòu)建一個向?qū)Э刂破鳎仨毨^承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的唯一一個必須實現(xiàn)的方法是processFinish()。在用戶完成表單后(一般是點擊完成按鈕),這個方法被調(diào)用,完成整個表單。 但是AbstractWizardFormController是如何知道哪些頁面構(gòu)成了整個表單呢?
<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>這樣向?qū)Ь椭懒四男╉撁鏄?gòu)成了表單,一個視圖邏輯名列表設(shè)置給了pages屬性。這些名字最終被視圖解析器解析成了一個個視圖對象。但現(xiàn)在,只要假設(shè)這些名字會被解析成JSP文件名就可以了。
分步顯示表單頁面:任何向?qū)Э刂破黠@示的第一個頁面都是pages屬性中列表的第一個頁面。AbstractWizardFormController詢問它的getTargetPage()方法。這個方法返回一個整數(shù),它是pages屬性中設(shè)置的頁面列表的索引值(以0為基數(shù))。getTargetPage()方法的默認(rèn)實現(xiàn)是根據(jù)請求中的一個參數(shù)來決定下一步是哪個頁面,這個參數(shù)以"_target"開頭,以數(shù)字結(jié)尾。知道了getTargetPage()的工作原理有助于你知道如何在向?qū)TML頁面中構(gòu)造下一步和上一步按鈕。例如,假設(shè)用戶在"motoristVehicleForm"頁面上(索引=1)。要在這個頁面上創(chuàng)建下一步和上一步按鈕,你要做就是創(chuàng)建提交按鈕,它的名字是以"_target"開頭:
<form>
...
<input type="submit" value="Back" name="_target0">
<input type="submit" value="Next" name="_target2">
</form>getTargetPage()方法的默認(rèn)實現(xiàn)對于大多數(shù)項目已經(jīng)夠用了。然后,如果你喜歡為你的向?qū)Фx自己的工作流程的話,你可以覆寫這個方法。
完成向?qū)?/strong>:還有一個特殊請求的參數(shù)"_finish",和"_targetX"參數(shù)一樣,它可以被用于在頁面上創(chuàng)建一個結(jié)束按鈕:
<form method="POST" action="feedback.htm">
...
<input type="submit" value="Finish" name="_finish">
</form>
當(dāng)AbstractWizardFormController看到請求中的"_finish"參數(shù)時,它會將控制權(quán)交給processFinish()方法,讓它對表單做最后的處理。與其他表單控制器不同,AbstractWizardFormController不提供用于設(shè)置成功視圖頁面的內(nèi)容。因此,我們在MotoristRegisttrationController中添加了一個getSuccessView()方法返回頁面列表中的最后一個頁面。所以,當(dāng)表單已完成的方式提交時,processFinish()方法會返回一個帶有視圖的ModelAndView,這個視圖就是頁面列表中的最后一個視圖。
取消向?qū)?/strong>:如何用戶完成了部分注冊,決定不再繼續(xù)完成,除了選擇直接關(guān)閉瀏覽器外,還有另一種選擇,你可以在表單上添加一個取消按鈕。
<form method="POST" action="feedback.htm">
...
<input type="submit" value="Cancel" name="_cancel">
</form>
取消按鈕以"_cancel"作為它的名字,當(dāng)用戶按下取消按鈕,瀏覽器將一個叫做"_cancel"的參數(shù)放到請求中。AbstractWizardFormController接收到這個參數(shù),將控制權(quán)交給processCancel()方法。 默認(rèn)情況下此方法會拋出異常,表示取消操作是不被支持的,我們要覆寫這個方法,將用戶帶到你想讓他們點擊取消時看到的頁面。下面processCancel()方法的實現(xiàn)將用戶帶領(lǐng)到成功視圖。
protected ModelAndView processCancel(HttpServletRequest request, HttpServletResponse response, Object command, BindException bindException) throws Exception {
return new ModelAndView(getSucessView()) ;
}
每次驗證一個向?qū)П韱?/strong>:使用其他類型的命令控制器,命令對象只裝載一個次。但使用向?qū)Э刂破鳎脩裘客瓿上驅(qū)ы撁嬷械囊徊剑紩幸粋€命令對象設(shè)置進來。使用向?qū)В蛔鲆淮悟炞C是不可行的,太早的話,找到的驗證問題可能是由于用戶沒有完成向?qū)Ф鴮?dǎo)致的。太晚的話,在完成按鈕被按下后再做檢查就太遲了,因為發(fā)現(xiàn)的問題可能越過了多個頁面(用戶該回到哪個頁面呢?) 向?qū)Э刂破髟诿總€頁面驗證一次命令對象,不是只驗證一次。這是通過每次頁面跳轉(zhuǎn)時調(diào)用validatePage()方法實現(xiàn)的。validatePage()方法的默認(rèn)實現(xiàn)是空的(也就是沒有驗證),但是你可以覆寫這個方法,做出自己的判斷。 假設(shè)在motoristDetailForm頁面,詢問用戶的郵件地址,這個字段是可選的,但是如果輸入了值,必須輸入一個合法的E-mail地址。下面的validatePage()方法展示了用戶從motoristDetailForm頁面跳轉(zhuǎn)出來的時候如何驗證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。然后,向?qū)б话阌泻脦醉椬侄涡枰炞C。如果這樣的話,validatePage()方法會變得很笨拙。我們建議你將驗證任務(wù)委托給控制器的驗證器對象的字段級驗證方法,就像我們在這里調(diào)用MotoristValidator的validateEmail()方法一樣。 這意味著當(dāng)你配置控制器的時候,你要設(shè)置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>
一個需要注意的重要事項是,不像其他命令控制器,向?qū)Э刂破鲝牟徽{(diào)用它們的驗證器對象的標(biāo)準(zhǔn)validate()方法。這是因為validate()方法驗證整個命令對象,然而在向?qū)е忻顚ο髮⒃诿總€頁面驗證一次。
使用一次性控制器:一次性控制器比其他控制器簡單很多,ThrowawayController接口可以證明:
public interface ThrowawayController {
ModelAndView execute() throws Exception ;
}
ThrowawayController接口和Controller接口不在同一個體系里。一次性控制器自己作為自己作為自己的命令對象,而不是通過一個HttpServletRequest或一個命令對象獲得參數(shù)。與WebWork Action相似,都是以同樣的方式工作。我們將RantsForController實現(xiàn)為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屬性已經(jīng)被設(shè)置為prototype。這就是一次性控制器獲取它們名字的地方。默認(rèn)情況下,所有的Bean都是Singleton。 但是因為ThrowawayContoller和Controller不處于同一繼承層次,DispatcherServlet不知道如何通知ThrowawayController。所以你必須告訴DispathcerServlet使用一種不同的 處理適配器。確切的說,你必須像下面這樣配置ThrowawayControllerHandlerAdapter:
<bean id="throwawayHandler" class="org.springframework.web.servlet.mvc.throwaway.ThrowawayControllerHandlerAdapter" />
如果應(yīng)用程序既使用了常規(guī)控制器又使用了一次性控制器,你還應(yīng)該按如下方式聲明SimpleControllerHandleAdapter:
<bean id="simpleHandler" class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
聲明兩處理器適配器使你可以在用一個應(yīng)用程序中混合使用兩種類型的控制器。