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

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

24.2 我们的亲密伙伴Controller

24.2.3 SimpleFormController

作为“规范操作派”当门大弟子,SimpleFormController首先继承了该派“掌门人"BaseCommandController的自动数据绑定和通过Validator的数据验证功能 。不过,BaseCommandController也只传授SimpleFormController以上两种主要能力,却没有传授打通“经脉之法”。还好,BaseCommandController后继有人,AbstractFormController在BaseCommandController的“武功”基础上,发展了一套模板化的form处理流程。至此,从数据的封装,验证,再到处理流程的模板化,整个规范化体系即告建立完成。而SimpleFormController以及稍后为各位详细介绍的AbstractwizardFormController从一出生,就被纳入了这套规范化体系。SimpleFormController专门面向单一表单的处理,而AbstractWizardFormController则提供多页面向导的交互能力。

要让SimpleFormController帮助我们简化Web请求处理工作,我们首先需要了解它。下面就让我们详细看一下SimpleFormController与生俱来的三种主要功能吧!

1. 了解数据绑定

在Web应用程序中使用数据绑定的最主要好处就是,我们再也不用自己通过request.getParameter(String)方法遍历获取每个请求参数,然后根据需要转型为自己需要的类型了。SpringMVC提供的数据绑定功能帮助我们自动提取HttpServletRequest中的相应参数,然后转型为需要的对象类型。我们唯一需要做的,就是为数据绑定提供一个目标对象,这个目标对象在Spring中称为Command对象(之前说过,它类似于Struts 1.1中FormBean的概念),此后的Web处理逻辑直接同数据绑定完成的Command对象打交道即可。

对于BaseCommanaController及其子类来说,我们可以通过它们的commandClass属性设置数据绑定的目标Command对象类型,如下所示:

<bean id="commandContro1ler" c1ass="..AnySubClassOfBaseCommandController">
	<property name="commandClass" va1ue="..Command"/>
</bean>

或者直接在子类的构造方法中直接设定,如下所示:

public class BindingDemoController extends SimpleFormController {
    
    
	public BindingDemoController() {
    
    
		setCommandClass(Command.class);
		//进行其他必要设置
 	}
  	...
}

那么,SpringMVC在数据绑定过程中,是如何将请求参数绑定到我们所指定的Command对象上的呢?有关数据绑定的过程,我们可以简单概括如下。

(1)在Web请求到达之后,SpringMVC某个框架类将提取当前Web请求中的所有参数名称,然后遍历它,以获取对应每个参数的值,获取的参数名与参数值通常放入一个值对象(PropertyValue)中。最终我们将拥有所有需要绑定的参数和参数值的一个集合(Collection)。

(2)有了即将绑定到目标Command对象的数据来源之后,我们即可将这些数据根据Command对象中各个域属性定义的类型进行数据转型,然后设置到Comnand对象上。在这个过程中我们将碰到我们的老朋友BeanWrapperImpl,还记得IoC容器讲解内容中BeanWrapper第一次登场的情景吗?BeanWrapperImpl会将Command对象纳入自身管理范围,如下所示:

BeanWrapper beanWrapper = new BeanWrapperImpl(command);

然后比照参数名与Command对象的属性对应关系,以进行参数值到Command对象属性的设置,而参数值与Command对象属性间类型差异性的转换工作,则由BeanWrapperImpl所依赖的一系列自定义PropertyEditor负责。如果BeanWrapperImpl所使用的默认的PropertyEditor没有提供对某一种类型的支持,我们也可以添加自定义的PropertyEditor。这些内容你应该已经熟悉了,而我要澄清的是,在绑定过程中,参数名称与Comnand对象属性之间的对应关系是如何确定的。

假设我们有Command对象定义如下方代码清单所示。

public class CustomerMetadata {
    
    
	private String aadress;
	private String zipCode;
	private List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
	public CustomerMetadata() {
    
    
		phoneNumbers.add(new PhoneNumber());
  	}
	//getter和setter方法定义
}

public class PhoneNumber {
    
    
	private String areaCode;
	private String number;
  
	public String getAreaCode() {
    
    
		return areaCode;
	}
	public void setAreaCode(String areaCode) {
    
    
		this.areaCode = areaCode;
  	}
	public String getNumber() {
    
    
		return number;
  	}
	public void setNumber(String number) {
    
    
		this.number = number;
		//toString()等方法定义
  	}
  	// toString()等方法定义
}

那么,为了能够让请求参数对应到Command对象的相应属性,我们需要按照如下格式定义可能发送的参数名称:

<input type="text" name=”address"/>
<input type="text” name=”zipCode"/>
<input type="text” name="phoneNumbers[0].number"/>

也就是说,参数的名称对应Command对象的属性名称。对于嵌套的集合类型,SpringMVC还允许使用类似于JSTL和OGNL(Object-Graph Navigation Language)的语法来定义简单的表达式,例如phoneNumbers[0].number。表24-1给出了部分表达式的说明。

image-20220629160614126

只要我们按照以上规则指定合适的参数名称,BaseCommandController及其子类就可以保证这些参数的参数值将被正确地绑定到Command对象及其嵌套对象上。不过,在使用嵌套表达式的时候,需要注意,必须保证中间环节对象不能为空。如果我们在构造CustomerMetadata的时候不为phoneNumber属性添加至少一个元素的话,那么phoneNumbers[0].number将抛给你“亲切的”NullPointerException。因为这个时候你的phoneNumber列表中根本就不存在第一个元索(phoneNumbers[0]==null),何来null.setNumber(..)之理呢?

注意:有关绑定的参数表达式与Command对象属性之间的设置,可以进一步参照Spring参考文档的“BeanmanipulationandtheBeanWrapper”内容。

从参数获取到参数转型并绑定到Command对象,这整个流程最终以org.springframework.web.bind.ServletRequestDataBinder的形式进行了封装。该类常见使用代码如下所示:

CustomerMetadata customerMetadata = new CustomerMetadata();
ServletRequestDataBinder dataBinder = new Serv1etRequestDataBinder(customerMetadata);
// 如果必要的话,可以先进行dataBinder.registerCustomEditor(...)
dataBinder.bind(request);
Errorserrors = binder.getErrors();
...

当然,我们不用自己去写这些几乎固定套路的代码,BaseCommandController及其子类将负贵调用该类完成整个绑定过程,并返回已经绑定数据的Command对象。而之后,我们只需要使用数据绑定完成的Command对象进行后继处理就行了,比如,即刻对Command对象绑定后的数据进行数据验证工作。

2. Spring框架数据验证简介

Spring框架提供的数据验证支持并不只是局限于Spring MVC内部使用,从数据验证类所在的包名就能看出来,即org.springframework.validation。只要愿意,我们完全可以在独立运行的应用程序中使用Spring的数据验证功能。

Spring数据验证框架核心类为org.springframework.validation.Validatororg.springframework.validation.ErrorsValidator负责实现具体的验证逻辑,而Errors负责承载验证过程中出现的错误信息 ,二者之间的纽带则是Validator接口定义的主要验证方法validate(target, errors)。从如下validator的定义中可以看出这一点:

public interface Validator {
    
    
	boolean supports(Class clazz);
	void validate(Object target, Errors errors);
}

Validator具体实现类可以在执行验证逻辑的过程中,随时将验证中的错误信息添加到通过方法参数传入的Errors对象内。 这样,验证逻辑执行完成之后,我们就可通过Errors检索验证结果啦!至于Validator接口中的support(Class)方法定义,是为了进一步限定Validator实现类的职责。除非你想压缩“人力成本”,把所有的数据验证工作全都交给一个Validator实现类去做。不过,那样的话,这个Validator就惨了!

正如我们所见到的那样,Validator接口定义很简单,而实现一个Validator实际上也同样简单。在CustomerMetadata完成数据绑定之后,我们需要对其绑定后的数据做进一步的验证,比如,提供的address和zipCode不能为空。如果提供了电话,那么,电话号码必须非空而且是数字等。这时,我们就需要为CustomerMetadata提供一个Validator实现类,下方代码清单给出了对应的Validator实现类。

public class CustomerMetaDataValidator implements Validator {
    
    
	private PhoneNumber Validator phoneNumberValidator;
  
	public CustomerMetaDataValidator(PhoneNumberValidator phoneNumberValidator) {
    
    
		this.phoneNumberValidator = phoneNumberValidator;
  	}
  
	public boolean supports(Class clazz) {
    
    
		return ClassUtils.isAssignable(clazz, CustomerMetadata.class);
  	}
  
	public void validate(Object target, Errors errors) {
    
    
		CustomerMetadata customerMetadata = (CustomerMetadata)target;
		ValidationUtils.rejectIfEmpty(errors, "address", "address.empty");
		ValidationUtils.rejectIfEmpty(errors, "zipCode", "zipcode.empty");
    
		List<PhoneNumber> phoneNumbers = customerMetadata.getPhoneNumbers();
		if(CollectionUtils.isNotEmpty(phoneNumbers)) {
    
    
			for(int i = 0, size = phoneNumbers.size(); i < size; i++) {
    
    
				PhoneNumberphone = (PhoneNumber)phoneNumbers.get(i);
				errors.pushNestedPath("phoneNumbers["+i+"]");
				ValidationUtils.invokeValidator(phoneNumberValidator, phone, errors);
				errors.popNestedPath();
      	}
    }
  }
}

public class PhoneNumberValidator implements Validator {
    
    
	public boolean supports(Class clazz) {
    
    
		return ClassUtils.isAssignable(clazz, PhoneNumber.class);
  	}
	/**
	* errors.reject(errorCode); // G1obal Error with Type of ObjectError
	* errors.rejectValue(filed,errorCode,..); // Field Error with Type of FieldError
  	*/
	public void validate(Object target, Errors errors) {
    
    
		PhoneNumberphoneNumber = (PhoneNumber)target;
		if(phoneNumber == null)
			errors.reject("errorCodes.phoneNumber.is.null");
    
		if(!StringUtils.isNumeric(phoneNumber.getAreaCode()))
			errors.rejectValue("areaCode","areaCode.not.numeric","areaCodecann'tbeempty!");
    
		if(!StringUtils.isNumeric(phoneNumber.getNumber()))
			errors.rejectValue("number","phoneNumber.not.numeric");
  	}
}

当然,也可以将CustomerMetadata所持有的PhoneNumber的验证逻辑一并合在CustomerMetaDataValidator中实现。不过,为了能够各司其职,还是为PhoneNumber的验证逻辑提供一个单独可复用的Validator实现比较合适。

初看两个Validator实现可能有些一头雾水,我们一个一个进行分析,如下所述。

PhoneNumbervalidator中需要关注的地方。 我们之所以先从PhoneNumberValidator开始,是因为这个Validator实现比较单纯,可以演示通常的Validator实现场景。一个Validator实现类,首先应该通过supports(Class)方法界定自身负责的验证范围,然后才是具体的验证逻辑。“细化社会分工”,这很重要!

具体的验证逻辑通常针对两种数据实体,一种就是被验证对象本身,另一种就是被验证对象的相应属性。 如果被验证对象本身都不能通过验证,那么,这种错误我们称之为Global Error。这时,我们使用Errors的reject(String...)这组方法,向Errors中添加相应的错误信息。如果被验证对象的某个属性域不能够通过验证,那么,我们称这种错误为Field Error。这时,我们要使用Errors的rejectValue(String,String...)这组方法向Errors中添加相应的错误信息。

reject(String...)方法第一个参数是错误信息对应的errorCode,而rejectValue(String, String...)方法第一个参数是未能通过验证的属性域的名称。第二个参数才是对应错误信息的errorCode。这两个方法的差别就是这样,现在我们再回头看PhoneNumberValidator的实现,应该可以豁然开朗了。

解读CustomerMetaDatavalidator。 CustomerMetaDataValidator中有两个比较重要的点需要我们关注,如下所述。

  • 对于不能通过数据验证逻辑的属性域,最基本的做法是通过Errors对象的rejectValue(String,String,..)方法将其信息添加到Errors对象。不过,如果对应某个对象域的验证仅限于“是否为空”这样的逻辑的话,我们也可以使用ValidationUtils这个工具类所提供的一组rejectIfEmpty(..)方法来达到同样的目的。这也就是我们在验证CustomerMetadata对象的address和zipCode两个对象域的时候使用validationUtils的原因。

  • 如果要对当前对象的嵌套属性域进行验证,我们需要在调用对应嵌套对象的Validator实现类之前,调用Errors的pushNestedPath(String path)方法来明确当前被验证对象的上下文路径,并且在调用之后,通过popNestedPath()恢复之前的上下文路径。否则,当Errors对象绑定对应嵌套对象属性的错误信息的时候,会认为该属性是上层目标对象上的属性,这时就会出现绑定上的异常了。如果我们不使用pushNestedPath(String path)方法,Errors在记录number对应的错误信息的时候,同时需要记录对应该属性的值,那么它就会根据当前属性域对应的表达式到Command对象上获取。可是当它根据number到CustomerMetadata上查找的时候,就会发现,根本就找不到CustomerMetadata上有一个叫做number的属性域,自然就会抛出异常。可是,如果在此之前,我们通过pushNestedPath(String path)方法改变Errors注册属性域错误信息所使用的上下文路径,比如,变成phoneNumber[0],那么,当Errors注册number对应的错误信息的时候,就会以phoneNumber[0].number到CustomerMetadata获取对应的属性值。现在,自然就没有问题了。

基本上,实现一个validator要注意的地方也就这些了。我想,了解了以上这些实现细节之后,让你再去实现任意一个validator,你应该不会“手软”了吧!下方代码清单给出的是调用具体Validator实现类执行数据验证逻辑 的普通场景。

CustomerMetaDataValidator validator = new CustomerMetaDataValidator(new PhoneNumberValidator());
CustomerMetadata md = new CustomerMetadata();
// 为Command对象设置测试数据

BindException errors = new BindException(md, "customerMd");
ValidationUtils.invokeValidator(validator, md, errors);
assertTrue(errors.hasErrors());

Map map = errors.getBindingResult().getModel();
BindingResult result = (BindingResult)map.get("org.springframework.validation.BindingResult.customerMd");
// 遍历BindingResult以取得可用的错误信息

我们只需要构造一个具体的Command对象实例(CustomerMetadata)以及一个Errors实例(BindException),然后通过ValidationUtils调用对应的Validator实现类(不使用ValidationUtils直接调用Validator的validate()方法也一样)。调用完成之后,即数据验证完成,如果存在验证错误(可以通过errors.hasErrors()获知),我们可以遍历之前传入的errors以获取相应的错误信息,然后根据具体应用程序的场景做后继处理。

在Spring MVC中,以上Validator实现类的执行以及后继错误信息的处理,将由BaseCommandController或者其子类接管。我们通常不需要操心这些细节,唯一需要我们做的是,通过相应的setter方法,为BaseCommandController或者其子类(比如当前的SimpleFormController)提供需要使用的Validator实现,如下所示:

<bean id="commandController" class="SubClassOfBaseConmandController">
	<property name="validators">
		<list>
			<ref bean="validator1"/>
			<ref bean="validator2"/>
		</list>
	</property>
</bean>

在实际的项目开发过程中,除了使用以上编程方式实现数据验证工作,我们还可以借助于Commons Validator(http://commons.apache.org/validator/)或者Valang(http://opensource.atlassian.com/confluence/spring/display/MODULES/Using+Valang+validator)实现声明式的数据验证,详细内容可以参考Expert Spring MVC and Web Flow一书,其中对使用Valang进行声明式数据验证进行了详尽的介绍,我们就不在这里进行赘述了。

3. 深入表单(form)处理流程

SimpleFormController及其父类AbstractFormController最主要的一个特点就是对表单的处理流程进行了统一。 可以毫不夸张地说,只要掌握了AbstractFormController以及其子类的表单处理流程,我们就基本掌握了整个本派(当然指的是“规范操作派”)武功之精髓。

AbstractFormController以模板方法模式从顶层界定了主体上的流程处理逻辑,而处理流程中某些特定的动作则留给其子类SimpleFormController(以及AbstractWizardFormController)实现。 但以模板方法模式实现的整个流程控制,并非真得就像“模板”所听起来的那样死板,我们可以通过覆写其中的某些方法以添加自定义的行为逻辑,体现了整个流程的可扩展性和灵活性。

对于整个模板流程逻辑,AbstractFormController和SimpleFormController的Javadoc中有详细的描述。但为了能够帮助大家更容易地了解这个流程,以便之后的开发过程中能够灵活驾驭SimpleFormController,我们将结合图24-9详细讲述该“武术套路”。

image-20220629215635195

我们将AbstractFormController的流程逻辑整合到SimpleFormController的最终流程逻辑中,从而可以看到一个完整的流程实现逻辑(AbstractFormController只是提供了表单处理流程的主体框架)。下面是我们对整个表单处理流程的描述。

在Web请求到达SimpleFormController之后,SimpleFormController将首先通过方法isFormSubmission(request)判明当前请求是否为表单提交请求,isFormSubmission(request)的默认实现如下所示:

protected boolean isFormSubmission(HttpServletRequest request) {
    
    
  return"POST".equals(request.getMethod());
}

也就是说,只要是以POST形式发送的Web请求,SimpleFormController将认为当前Web请求即为表单提交。我们可以通过覆写该方法以改变默认的判定标准。

整个表单处理流程将以isFormSubmission(request)的判定结果为基准,划分为 “表单显示阶段”和“处理表单提交阶段” 两个逻辑处理区段。如果isFormSubmission(request)返回false,通常表示初次Web请求,这时我们需要为用户显示相应的交互表单,这也就是“表单显示阶段”将要做的事情。否则,将认为用户已经在表单中编辑完数据,需要处理,SimpleFormController将启用“处理表单提交阶段”流程处理逻辑。下面是我们对这两个阶段流程处理逻辑的详细分析。

表单显示阶段流程分析

(1)创建或者获取表单对应的Backing Object。 我也不知道为什么将该对象叫做form Backing Object。实际上它就是对应绑定表单数据的Command对象。不过,我想我们只要知道这个对象干什么的就可以了,没有必要在名字上深究。

此时,formBackingObject()方法将默认通过反射实例化我们为SimpleFormController指定的Command对象实例 。对于像“新添加某个实体到系统数据库”这样的场景来说,formBackingObject()方法的默认行为就足够了,对于之前不存在的实体,添加到系统之前,它本来就是一个“空白”对象嘛!

但是某些时候formBackingObject()的默认行为不能符合当前场景需求。比如,如果我要更新一个实体的信息,那么,在为用户显示表单的时候,就应该将要更新的实体的数据加载到表单中,以便用户在原有数据的基础上进行更改。这时,我们就需要覆写formBackingObject()方法,改为自己管理FormBackingObject的初始化。比如,从数据库中加载数据到将要返回给表单显示的Form Backing Object中。

注意:图24-9中所有像formBacking0bject()方法那样下面加横线的方法,都是可以覆写以添加自定义行为的地方,这是SimpleFormController为我们提供的扩展点。

(2)初始化DataBinder。 因为BaseCormandController门下所有门徒都采用将请求参数绑定到Command对象的处理方式,这当然也包括SimpleFormController,所以,我们就得在执行请求参数到Command对象的数据绑定之前,初始化一个可用的DataBinder实例。具体点儿说,就是我们在24.2.3节的第1小节所介绍的ServletRequestDataBinder。

在有了一个可用的ServletRequestDataBinder之后,我们可以对其进行定制,比如添加自定义的PropertyEditor以支持某些特殊数据类型的数据绑定,或者排除某些不想绑定的请求参数,这些定制行为可以通过覆写initBinder()方法引入,例如:

@Override
protected void initBinder(HttpServletRequest reguest, Serv1etRequestDataBinder binder) throws Exception {
    
    
	PropertyEditor propertyEditor = ...;
	binder.registerCustomEditor(SpecialClass.class, propertyEditor);
	binder.setDisallowedFie1ds(new String[]("parameter1", "parameter2"));
}

ServletRequestDataBinder准备完毕之后,就可以随时用于数据绑定操作了。

(3)执行数据绑定。 这一步只有在bindOnNewForm属性被设置为true的情况下才会触发执行。bindOnNewForm默认值是false,所以,通常情况下是不会在显示表单之前执行任何绑定操作的。但是,如果针对SimpleFormController发起的初次请求中存在某些参数,并且我们想将它们绑定到创建好的Command对象,那么就可以在SimpleFormController的构造方法或者bean定义上设置bindOnNewForm为true (如下代码清单所示),剩下的事情就交给SimpleFormController去管了。

public class YourFormController extends SimpleFormController {
	public YourFormController() {
		setBindOnNewForm(true);
		// ...其他设置,如setCommandClass(...)
	}
}

或者

<bean id=".." class="..YourFormController">
	<property name="bindOnNewForm" value="true"/>
</bean>

在将bindOnNewForm设置为true触发了数据绑定操作之后,ServletRequestDataBinder的bind(request)方法将被调用,从而完成要求的数据绑定。之后,如果我们想对绑定后的Commnana对象做进一步的定制,可以覆写onBindOnNewForm(..)方法以插入自定义逻辑。

(4)处理表单的显示。 在最终显示表单页面之前,还有最后一点儿工作要做,如下所述。

  • 如果SimpleFormController的sessionForm属性被设置为true的话(实际上是其父类AbstractFormController定义的该属性),我们会将绑定后的Command对象存入HttpSession。这样在提交表单之后,也就是“处理表单提交阶段”,可以重新获取该Command对象,而不是又重新生成一个(那样的话,之前对Command对象的数据定制将不复存在)。

  • Form Backing Object更多的面向表单的各个字段,与表单中的字段形成双向的绑定关系。 比如AdminGroup的groupName属性对应表单中的<input name="groupName" type="radio" value="${group.groupName}"/>。但某些情况下,表单页面的显示还需要Form Backing Object所包含的数据之外的信息,比如,如果我们的表单中需要提供某种动态信息的可选择下拉列表,那么,这时可能就需要从数据库中抽取将构建下拉列表所需要的数据。这种类型的数据信息可以通过覆写referenceData(..)方法来提供,SimpleFormController将会把通过referenceData(..)返回的模型数据添加到即将返回的ModelAndView中。

如果你还是无法搞清楚Form Backing Object中的数据与通过referenceData(..)返回的数据有何区别,我们来看一个实例。

假设我们的下拉列表框显示一年中的12个月,在referenceData(..)中,我们可能需要添加如下方代码清单所示代码逻辑。

@Override
protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception {
    
    
	Map data = new HashMap();
  
	List<String> months = new ArrayList<String>();
	months.add("January");
	months.add("February");
	// 添加其他月.....
	data.put("months", months);
	return data;
}

视图模板中将取得referenceData(..)所返回的数据进行显示,如下方代码清单所示。

<select name="monthOfCreation">
  <c:forEach var="month" items="$(months)">
    <c:choose>
      <c:when test="${month eq monthOfCreation)">
        <option value="${month}" selected="true">${month}</option>
      </c:when>
      <c:otherwise>
        <option value="${month}">${month}</option>
      </c:otherwise>
    </c:choose>
  </c:forEach>
</select>

referenceData(..)所返回的应该是对应要遍历的数据,而我们的Form Backing Object中存放的实际上只是遍历的数据中的某个值。具体到我们的示例就是,视图模板中${monthOfCreation}对应的值,应该来自我们的Form Backing Object,而${months}则来自referenceData(..)方法。

总地来说,referenceData(..)是返回可供选择的数据列表。而Form Backing Object通常只是保存数据列表中选择后的那个值。 当然,将这样的数据一并添加到Form Backing Object返回也是可以的,但各自的语义也就有些含混了。

在以上准备工作完成之后,SimpleFormController就会将所有这些“搜刮”来的数据一并放到某个ModelAndView实例中并返回(该ModelAndView将使用formView属性所指定的逻辑视图名)。剩下会发生什么我想你已经知道了。

处理表单提交阶段的流程分析

(1)获取要绑定的目标对象。 用户提交表单之后,我们首先会判断SimpleFormController的sessionForm属性是否为true。如果是,我们将从HttpSession中获取之前已经存入的Comnand对象。否则,与“表单显示阶段”一样,通过formBackingObject()方法重新生成一个Command对象以备后继数据绑定之用。实际上,除了做一下简单的条件判断,该步骤与“表单显示阶段”的同一步骤目的是相同的,都是为了能够为后继操作提供一个Command对象。

(2)初始化DataBinder。 这一步与“表单显示阶段”同一步骤完全相同,没有必要多说了吧?

(3)执行数据绑定/数据验证。 在用户通过表单提交数据之后,我们需要将这些新的数据绑定到Command对象。在绑定的实现上,与“表单显示阶段”同一步骤原理相同,只不过,绑定完成后为我们提供的扩展点是onBind(..)方法。我们可以覆写该方法对绑定后的Command对象做进一步的定制。

与显示表单阶段不同,完成数据绑定之后,还需要对用户提交的数据进行验证,否则,非法的数据将有可能危害整个系统的安全。通常,数据验证可以在两个地方进行,一个是客户端,通过客户端脚本(比如Javascript)做初步的数据验证,各浏览器对同一脚本的支持存在差异先不提,最主要的问题是,如果有意,用户可以轻易避开客户端通过脚本提供的这段数据验证“安全网”,所以,我们得在另一个地方拉起最主要的一道数据验证“安全网”,那就是在服务器端,而我们当前所处的位置,就是SimpleFormController为我们提供的构筑服务器端数据验证“安全网”的地方。

数据验证逻辑的执行实际上是不需要我们关心的。要实现服务器端数据验证,我们所要做的只是为SimpleFormController指定一组用于对当前表单提交的数据进行数据验证的validator实现类就行了,也就是说,我们是通过配置相应的Validator来提供具体的数据验证逻辑的。

虽然Validator的调用我们不能干预,但是,如果需要在数据验证完成后对Command对象做一些“手脚”,我们还是有机会的。通过覆写onBindAndvalidate(..)方法,我们可以在继续剩下的逻辑之前插入一些自定义逻辑。

(4)处理表单提交。 胜利就在眼前,不过少安毋躁,在允许我们调用相应的业务对象处理最终的数据之前,SimpleFormController还要做进一步的“安检”工作。

SimpleFormController会首先检查数据验证后是否存在验证错误。如果存在验证错误,它将会把处理流程导向“显示表单阶段”的最后一步,也就说,重新显示表单页面。只不过,这时,表单页面除了显示最初的数据之外,还得显示相应的验证错误信息。

如果没有验证错误,SimpleFormController再检查一下当前Web请求是否只是一个FormChangeRequest,也就是说,当前Web请求只是对表单的数据进行了一些变动,而不是最终的提交。这时,我们依然不能处理数据,而是需要将视图重新导向表单的显示页面,就与刚才出现验证错误时候我们所做的一样。不过,在将流程导向表单页面显示之前,我们可以通过覆写onFormChange()方法添加某些自定义逻辑,比如更改Command对象数据状态。

注意:SimpleFormController默认的isFormChangeRequest(…)返回false,即不支持该类型Web请求,我们可以通过覆写该方法以提供自定义的判定标准。

在以上这些检查通过之后,我们就可以真正地处理表单提交的数据了。SimpleFormController允许我们在如下两个地方提供表单数据的处理逻辑实现。

我们可以通过覆写方法doSubmitAction(Object command)来添加针对当前表单数据的处理逻辑,最常见的就是直接调用相应的服务对象。doSubmitAction(command)方法执行之后,SimpleFormController将构建一个ModelAndView将视图导向successView属性所指定的视图。

实际上,doSubmitAction(command)只是onSubmit(..)方法所公开的一个回调方法。当前者执行完毕之后,是由onSubmit(..)来构建相应的ModelAndView以“打扫战场”的。所以,如果感觉doSubmitAction(command)方法不能为我们提供更大的发挥余地,那么,我们完全可以直接覆写onSubmit(..)方法来实现具体的处理逻辑。只不过,在最后要自己构建一个ModelAndView并返回。当然,通常情况下,直接覆写doSubmitAction(cormand)方法应该就足够了。

注意:SimpleFormController提供了重载的onSubmit(…)方法,我们可以根据需要选择覆写哪个。

在视图被导向successView属性所指定的页面之后,我们的表单处理流程就算结束了。整个流程处理逻辑看起来可能很复杂,让人有些望而却步。但实际上,要实现一个SimpleFormController以处理与表单的交互很简单。通常也就是设置一下要绑定的Commnand对象,然后覆写一下doSubmitAction(command)方法即可。至于其他,像是否要添加Validator,是否要将Command对象存入session,以及要不要在表单显示之前就进行数据绑定之类的逻辑,完全可以根据应用程序的需要进行添加。下方代码清单给出了一个最基本的SimpleFormController实现类。

public class HelloWorldFormController extends SimpleFormController {
    
    
	public HelloworldFormController() {
    
    
		setCommandClass(YourCommandObject.class);
		setCommandName("optional");
  	}
  
	@Override
	protected void doSubmitAction(Object command) throws Exception {
    
    
		System.out.println("He1lo, we have got a command to process." + command.toString());
  	}
}

当然,HelloWorld永远只是一个玩具,还是让我带你进入更真实一些的世界吧!

4. SimpleFormController应用演示

书接上回,话说我们为了演示MultiActionController的使用,而引入了操作员组管理的处理场景,当时虽然给出了整个的流程,但仅对使用MultiActionController实现的部分进行了介绍。现在我们要完成图24-7中整个流程图的后半部分实现。

在用户通过组管理画面groupAdmin.jsp提交“添加新操作员组”或者“更新某个操作员组”的请求之后,MultiActionController实现会将这两种请求转发给后继的Controller进行处理。正如我们所看到的那样,不管是要添加数据还是要更新数据,都需要首先为用户提供一个表单页面,然后再对用户通过表单提交的数据进行处理。这里所谓的后继Controller,通过继承SimpleFormController来实现是最为合适的方式。

下面是对“操作员组添加以及更新操作实现流程”的简短介绍。通过对比两个不同的SimpleFormController,以期望大家可以从中窥得SimpleFormController的各种奥秘以及使用方式。

添加新的操作员组实现流程

我们先来看一下添加操作员组的视图画面,以便先对这个要实现的功能有一个感性的认识,如图24-10所示。虽然组名是区分各个组的主要标志,但本质上来说,各个组之间的本质差别则是每个组所持有的权限。我们添加新组也好,更新旧组也好,说白了,是对每个组所持有的权限进行操作。

image-20220630125148714

(1)业务层逻辑说明。 新建组的时候会有一套默认的权限序列供我们选择,这些默认的权限序列需要通过某个服务对象取得。所以,除了之前的IGroupAdminService及其实现类之外,现在还得提供一个IAuthorityService以及它的实现类,我们暂且提供如下方代码清单所示的参考代码。

public interface IAuthorityService {
    
    
	List<AuthorityRule> getAvailableAuthorityRules();
}

public class MockAuthorityService implements IAuthorityService {
    
    
	public List<AuthorityRule> getAvailableAuthorityRules() {
    
    
		List<AuthorityRule> rules = new ArrayList<AuthorityRule>();
  
	AuthorityRule swapSettingRule = new AuthorityRule();
	swapSettingRule.setId(1L);
	swapSettingRule.setAuthorityName("SWAP设定");
	rules.add(swapSettingRule);
    
	AuthorityRule swapHistoryMgtRule = new AuthorityRule();
	swapHistoryMgtRule.setAuthorityName("SWAP历史记录");
	swapHistoryMgtRule.setId(2L);
	rules.add(swapHistoryMgtRule);
    
	AuthorityRule orderMgtRule = new AuthorityRule();
	orderMgtRule.setAuthorityName("Order管理");
	orderMgtRule.setId(3L);
	rules.add(orderMgtRule);
    
	AuthorityRule cashflowMgtRule = new AuthorityRule();
	cashflowMgtRule.setAuthorityName("资金管理");
	cashflowMgtRule.setId(4L);
	rules.add(cashflowMgtRule);
    
	return rules;
  	}
}

实际系统中,我们可能需要从数据库提取相应数据,以具体的实现类来替换掉当前提供的模拟数据实现类MockAuthorityService。

(2)构建处理请求的SimpleFormController实现类。 我们扩展SimpleFormController实现了一个GroupCreateController,用于显示新添加操作员组表单,并最终处理表单提交后的数据。 GroupCreateController的定义见下方代码清单。

public class GroupCreateController extends SimpleFormController {
    
    
	private IGroupAdminService groupAdminService;
	private IAuthorityService authorityService;
  
	public GroupCreateController() {
    
    
		setCommandClass(AdminGroup.class);
		setCommandName("adminGroup");
  	}
  
	@Override
	protected Object formBackingObject(HttpServletRequest request) throws Exception {
    
    
		AdminGroup group = new AdminGroup();
		List<AuthorityRule> rules = getAuthorityService().getAvailableAuthorityRules();
		group.setRules(rules);
		return group;
  	}

	@Override
	protected void doSubmitAction(Object command) throws Exception {
    
    
		getGroupAdminService().createGroup((AdminGroup)command);
  	}
	// getter和setter方法定乂
}

该实现可能需要关注的点有如下几个。

  • 在构造方法内,我们通过setCommandClass(..)方法为GroupCreateController指定了将要使用的Command对象的类型,并且通过setCommandName(..)方法重新设置了GroupCreateController对应使用的Command对象的名称,视图模板将会根据该名称绑定Command对象数据到表单。setCommandName(..)的调用并非必须的,如果不明确设置Command名称的话,getCommandName()的值为“command”,也就是说,默认情况下,我们可以在视图中使用${command.property}的形式访问Cormand对象的属性值。

  • 我们覆写了formBackingObject(..)方法,默认的formBackingObject(..)只是通过反射初始化了一个“空白”的Command对象。在我们的场景中,这样的行为是不够的。即使是新建组,我们也需要提供一组默认的权限供新建组时候选择。所以,我们通过覆写formBackingObject()方法来自定义Command对象的初始化,在实例化Command对象的基础上,同时为其添加了附加数据,即默认的权限序列。

  • 在组建立成功之后,我们就会重新将视图导向最初的操作员组信息列表视图(groupAdmin.jsp),并不需要传递多余的数据。所以,我们只需要覆写doSubmitAction(command)方法来处理表单提交的数据即可。

GroupCreateController实现完成之后,我们需要将它添加到WebApplicationContext中,以便提供相应依赖的注入以及进一步细化对象配置,如下所示:

<bean name="1createGroup.do" class="cn.spring21.simplefx.controllers.GroupCreateController">
	<property name="groupAdminService" ref="groupAdminService"/>
	<property name="authorityService" ref="authorityService"/>
	<property name="va1idator" ref="adminGroupValidator"/>
	<property name="formView" value="createGroup"/>
	<property name="successView" va1ue="redirect:groupAdmin.do"/>
</bean>

groupAdminService和authorityService两个属性分别对应当前Controller实现所依赖的服务对象,它们随着应用和流程不同而变化。而validator(或者validators)、formView和successView属性则是每个SimpleFormController实现类都应该提供的,尤其是formView和successView。SimpleFormController最终将根据这两个属性把视图导向表单页面以及表单处理成功后的页面。

表单提交后,通常需要提供相应的Validator进行数据的验证。如何实现Validator进行数据验证我们之前已经谈过了,这里只罗列一下我们的实现吧!如下所示:

public class AcminGroupValidator implements Validator {
    
    
	public boolean supports(Class clazz) {
    
    
		return ClassUtils.isAssignable(clazz, AdminGroup.class);
  	}
  
	public void validate(Object target, Errors errors) {
    
    
		ValidationUtils.rejectIfEmptyOrWhitespace(errors,"groupName","group.name.empty");
  	}
}

我们的验证逻辑很简单,只是不允许新建组的组名为空而已。为了能够获取数据验证失败后的确切的错误信息,我们得在应用的顶层WebApplicationContext中定义一个MessageSource实现,名称为“messageSource"。这样,当需要显示错误信息的时候,在根据验证结果显示错误信息的代码中,就可以根据Errors中的错误码(errorCode)从WebApplicationContext中获取具体的错误信息了,如下所示:

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
	<property name="basename" value="messages"></property>
</bean>

关于MessageSource的更多信息可以回顾第二部分的相关内容。

(3)构建视图。 有了页面显示的初步印象,下方代码清单给出的JSP模板代码看起来也应该很好理解了。

<jsp:root version="1.2" xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:display="urn:jsptld:http://displaytag.sf.net."
xmlns:c="urn:jsptld:http://java.sun.com/jsp/jstl/core"
xmlns:form="urn:jspt1d:http://www.springframework.org/tags/form">
	<jsp:directive.page contentType="text/html;charset=UTF8"/>
	<jsp:include page="/WEB-INF/jsp/header.jsp" flush="true"/>
  
	<jsp:boay>
 	 <form:form commandName="adminGroup">
		<form:errors path="*"></form:errors><br/>
		组名:<form:input path="groupName"/>
		<input type="submit" value="创建" name="create"/><br/>
		
     <display:table name="adminGroup.rules" id="row">
				<display:column property="authorityName" title="权限"></display:column>
				<display:co1umn title="查询">
					<form:checkbox path="rules[$(row_rowNum-1)].canRead"/>
				</display:column>
				<display:column title="更新">
					<form:checkbox path="rules[$(row_rowNum-1)].canWrite"/>
				</display:column>
			</display:table>
			描述:<br/>
			<form:textarea path="description" cols="80"/>
		</form:form>
	</jsp:body>
</jsp:root>  

我们混杂了displayTag和Spring的form标签库(Tag Library)两种标签库进行模板内容的表达。有关它们的使用和更多细节可以参考相应文档,这里就不再赘述了,需要强调如下几点。

因为我们在GroupCreateController的构造方法中,将Comnand对象的默认名称从“command"变成了“adminGroup”,所以,使用Spring的form标签的时候,需要通过commandName明确指定当前的Comnand对象的名称。

不管使用什么标签或者表达式语言(EL,Expression Language)来表达模板内容,如果想让SpringMVC帮我们将表单参数自动绑定到Command对象,一定要保证表单中的各个元素的name属性使用了正确的表达式,例如rules[0].canRead或者rules[0].canWrite

更新现有操作员组信息的实现流程

更新页面与新建组的页面几乎完全相同,唯一的区别只是提交按钮的名字从创建变成了更新,如图24-11所示。

image-20220630205053968

更新操作员组的SimpleFormController实现类使用的服务对象与GroupCreateController所使用的相同。所以,我们略过业务对象相关内容,直接开始构建需要的SimpleFormController实现类。

(1)构建处理表单的SimpleFormController。

当用户要更新一个组的信息的时候,他将在现有的“组信息列表”页面选择要更新的组,然后提交更新请求。接收到请求的MultiActionController将通过重定向的方式,将后继流程控制权转交给我们的SimplepormController实现类。我们的实现类同样需要知道用户要更新的是哪一个组。所以,在MultiActionController的实现类GroupAdminDelegate中,我们将groupName添加到了返回的ModelAndView中。这样,重定向之后,发起请求的路径将类似于如下所示的样子:

http://host:port/simplefx/updateGroup.do?groupName=AdminGroupOne

该路径对应的Web请求将被映射给我们的SimpleFormController处理。

与新添加一个组所使用的SimpleFormController实现类不同,更新用的SimpleFormController存在些许差异,其定义见下方代码清单。

public class GroupUpdateController extends SimpleFormController {
    
    
	private IGroupAdminService groupAdminService;
	private IAuthorityService authorityService;
  
	public GroupUpdateController() {
    
    
		setComnandClass(AdminGroup.class);
		setCorrmandName("adminGroup");
		setSessionForm(true);
		setBindOnNewForm(true);
  	}
  
	@Override
	protected void onBindOnNewForm(HttpServletRequest request, Object command) throws Exception {
    
    
		AdminGroup group = (AdminGroup)command;
		List<AuthorityRule> rules = getAuthorityRulesFor(group);
		group.setRules(rules);
  	}
  
	@Override
	protected ModelAndView onSubmit(Object command) throws Exception {
    
    
		AdminGroup group = (AdminGroup)command;
		getGroupAdminService().updateGroup(group);
		ModelAndViewmav = new ModelAndView(getSuccessView());
		mav.addObject("groupName", group.getGroupName());
		return mav;
  	}

  //getter方法、setter方法以及其他helper方法定义
}

使用的服务对象是一样的,不一样的地方是如下几点。

在构造方法中,我们启用了两个新的属性,即sessionForm和bindOnNewForm属性,如下所述。

通过setSessionForm(true),我们告知GroupUpdateController在处理“表单显示”到“表单提交”的整个过程中,将使用保存到HttpSession的同一个Command对象,而不是每次都新建一个。

最初发起请求的路径为http://host:port/simplefx/updateGroup.do?groupName==AdminGroupOne,这意味着我们需要获取groupName参数以确定对哪一个组进行数据更新。在过去,我们当然是通过request.getParameter("groupName")来获取参数值。但SimpleFormController的第一个特色就是支持参数到command对象的绑定,所以,我们通过将bindOnNewForm设置为true来告知GroupUpdateController在“显示表单阶段”就进行参数绑定。

我们覆写了onBindOnNewForm(..)方法,该操作与设置bindOnNewForm属性为true是如影随形的。在参数绑定到Command对象之后,我们就得根据绑定到Command对象的参数值,来加载操作员组的原有信息到表单显示。现在看来,加载原有数据的逻辑也只能在onBindOnNewForm()方法内添加了。不过,实际上,要达成类似的效果,我们也可以在formBackingObject(..)中进行,如下方代码清单所示。

@Override
protected Object formBackingObject(HttpServletRequest request) throws Exception {
    
    
	AdminGroup group = new AdminGroup();
	String groupName = ServletRequestUtils.getStringParameter(request, "groupName");
	group.setGroupName(groupName);
	List<AuthorityRule> rules = getAuthorityRulesFor(group);
	group.setRules(rules);
	returng roup;
}

不过,在formBackingObject()中添加加载数据的逻辑,就要自已去处理参数获取等工作,有自动绑定参数的功能放着不用,多少有些违反门规的意思吧!

处理最终表单提交请求的时候,我们覆写了onSubmit(..)方法以取得更大的权限。因为像GroupCreateController那样只覆写一个doSubmitAction(..)方法在这里是不够的。我们希望在更新成功之后,依然将视图导向更新页面。这时,我们就得提供请求参数,以便GroupUpdateController能够根据这个参数正确显示页面数据。所以,我们得向返回的ModelAndView中添加参数,doSubmitAction(..)是不公开ModelAndView给我们的。

至于将GroupUpdateController添加到WebApplicationContext,就有些老生常谈了,如下所示:

<bean name="/updateGroup.do" class="cn.spring21.simplefx.controllers.GroupUpdateController">
	<property name="groupAdminService" ref="groupAdminService"/>
	<property name="authorityService" ref="authorityService"/>
	<property name="validator" ref="adminGroupValidator"/>
	<property name="formView" value="updateGroup"/>
	<property name="successView" value="redirect:updateGroup.do"/>
</bean>

(2) 构建视图。

组信息更新的视图与添加组的视图模板代码没有多大变化。如果可能,我们还可以进一步模块化两个视图的模板,组信息更新视图对应的定义代码如下方代码清单所示。

<jsp:root version="1.2" xmlns:jsp="http://java.sun.com/JSP/Page"
	xmlns:display="urn:jspt1a:http://displaytag.sf.net"
	xmlns:c="urn:jspt1d:http://java.sun.com/jsp/jstl/core"
	xmlns:form="urn:jspt1d:http://www.springframework.org/tags/form">
	<jsp:directive.page contentType="text/html;charset=UTF8"/>
	<jsp:include page="/WEB-INF/jsp/header.jsp" f1ush="true"/>
  
	<jsp:body>
		<form:form commandName="adminGroup" action="updateGroup.do">
			<form:errors path="*"></form:errors><br/>
			组名:<form:input path="groupName" readonly="true"/>
			<input type="submit" value="更新" name="update"/>
			<br/>
      
			<display:table name="adminGroup.rules" id="row">
				<display:column property="authorityName" title="权限"></display:co1umn>
				<display:column title="查询">
					<form:checkbox path="rules[${row_rowNum-1}].canRead"/>
				</display:column>
				<display:column title="更新">
					<form:checkbox path="rules[${row_rowNum-1}].canWrite"/>
				</display:column>
			</display:table>
			描述:<br/>
			<form:textarea path="description" cols="80*/>
		</form:form>
		<br/>
	<a href="groupAdmin.do">返回组管理画面</a>
	</jsp:body>
</jsp:root>

除了在添加新组的视图部分强调的问题,我再追加一个,这个问题很容易让人觉得“丈二和尚摸不着头脑”。问题的关键在于<form:form>的action属性,如果我们不明确指定该属性的话,表单将提交到最初Web请求发起的路径,这会造成什么问题呢?

以我们当前更新组的Web请求为例,在MultiAcitonController重定向Web请求之后,发送到GroupUpdateController处理的Web请求路径对应.../siplefx/updateGroup.do?groupName=AdminGroupOne。表单显示的时候,GroupUpdateController抽取参数绑定到Command对象,然后在视图中,groupName的值将被赋予<form:input path="groupName" readonly="true"/>,到这里没有任何问题。现在用户提交表单,如果我们没有明确指定action属性指向何处,那么,表单默认提交到发起请求的路径,也就是.../simplefx/updateGroup.do?groupName=AdminGroupOne。这样,路径中存在一个groupName参数,表单中也存在一个groupName参数(<form:input path=”groupName" readonly="true"/>也将以groupName为参数名被提交)。数据绑定的时候,CroupUpdateController将发现当前请求中存在两个groupName参数,就会将它们当作一个string数组而绑定给Cormand对象的groupName属性,结果可想而知。我们最终要处理的Comrmand对象的groupName可能变成了“AdminGroupOne,AdminGroupOne”。而实际上,应该为“AdminGroupOne"。

至于解决方法,当然是重新明确指定一下action对应的提交路径。

至此SimpleFormController的旅程已到终点了,不知你是否已经完全掌握该类的秉性了呢?(什么?还没有?那只能自己再查阅更多相关资料咯!)

猜你喜欢

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