Java Solaris 加入 SDN 参与讨论 我的社区 注册说明
 
 
 
 
 
 
Java API 文档中文版
使用 Spring 和 DWR 实现 Ajax 表单验证
 
By Eric Spiegelberg, 12/19/07  
目录

在“使用 Spring 和 DWR 实现 Ajax 表单验证”这篇文章中,我介绍了一种验证方法,就是用 Ajax 调用服务器端的验证逻辑来完成客户端验证。总体来说,这种方法可以充分利用服务器端的验证逻辑,避免代码重复,减少开发、维护的时间和成本,并提高 web 应用程序的可用性。一些非试用版软件的设计在一开始就采取这种验证方法。这种设计在某些商业系统中已运行了一年多时间(这些系统自发布到现在也接近十个月了),并且逐渐开始暴露出一些局限性。这篇文章将讨论这些局限性,然后将介绍一种改良的设计方法(这种方法不仅可以避免这些局限性,还提供了其他一些功能)。在概念上,这两篇文章非常相似。在这里,我假定您或者熟悉前一篇文章,或者熟悉 Ajax、DWRSpring Web MVC framework 的使用方法。

在深入讨论之前,我们先回顾一下通过 Ajax 验证表单输入的大致流程。当用户改变某个输入字段时,JavaScript 收集必要的表单输入信息,并将其打包发送到服务器。在服务器端,系统调用相关验证过程,并将处理结果返回给浏览器。浏览器解释结果后,通过更新浏览器 DOM 来显示或隐藏验证消息。从本质上说,浏览器的 Ajax 调用与自定义表单的提交是异步进行的,一个 JavaScript 函数用于处理服务器端的自定义响应,这种自定义响应用于动态地更新页面。

修订原因

前一种设计有局限性。为了支持通过反射来调用,需要在 Spring Validator 上强加一种设计格式。为每个输入字段创建单独的验证方法是一种好的途径,但是在复杂的表单中,要创建许多短小而且混乱的验证方法。许多读者反馈,由于各种原因,不能或不想分解他们现有的 Validator

第二,前一种设计包含许多重复的样板和硬编码的配置。每个 Ajax 验证表单必须在 dwr-config.xml 文件中配置好 Validator 的 DWR 代理,这在大型应用程序中很不灵活。调用验证过程的 JavaScript 脚本中内嵌了 DWR 代理名称,每次在新表单中使用这段脚本时,需要修改部分代码。

第三,只支持单一输入字段的验证。虽然这能满足大多数需求,但仍存在个别情况:对输入的验证非常复杂,“从属”输入字段的验证需要“父级”输入字段提供验证过程的上下文。这样的例子包括:用户的邮政编码是否存在于他所在的州内(邮政编码 55431 是否在 MN 这个州内);某信用卡号是否属于某种类型的卡(卡号是否符合 Visa 卡的格式?);密码确认输入字段是否与密码输入字段一致。由于一次仅提交单个输入字段进行验证,因此这类复杂的验证是不可能的。

第四,前一种设计含自定义的代码,与 Spring 和 DWR 中提供的功能相重复。

基于这些不足,进行重新设计的目标就是简化结构、最小化配置、把自定义代码替换成现有的 Spring 和 DWR 特性,以及支持多输入字段的验证。

使用 Spring 技术的服务器端验证

为每个 Validator 配置 DWR 代理非常困难,因此人们设计了单独的前端控制器。在性能上,他比为每个 表单使用一个控制器提高了许多。这样免除了在 Java 和 JavaScript 层的大多数配置,并且具有更好的可重用性。

现在由于每个 Ajax 验证请求都是通过单独的前端控制器来运行,这个前端控制器必须能确定,是哪一个 Spring Web MVC Controller 用于处理同一表单的提交。 一旦这种 Controller 被认可,Spring 的灵活且功能强大的 API 将为动态地执行验证过程提供所有功能。

在详细讨论之前,我们来快速回顾一下 Spring Web MVC 的工作原理。在应用程序中配置完 Spring Web MVC 后,DispatcherServlet (或其他类似的)就知道了哪个 Controller 用来处理给定 URI 的请求。典型的 dispatcherServlet.xml 配置文件包含类似于下面的代码:

...
<bean name="/shop/newAccount.do" class="AccountFormController">
<property name="petStore" ref="petStore"/>
<property name="validator" ref="accountValidator"/>
<property name="successView" value="index"/>
</bean>
...

这个映射告诉 Spring Web MVC,当接收一个来自 /shop/newAccount.do 的请求时,就把执行任务交给 AccountFormController

修订后的中间几行代码的中心思想是,如果 Ajax 验证请求包含表单动作的属性值,前端验证控制器就可以根据 DispatcherServlet 的映射关系配置来确定哪个 Controller 处理正常的表单提交。正如 DispatcherServlet 从动作 URI /shop/newAccount.do 接收正常的表单提交后让 AccountFormController 来处理一样,前端验证控制器能确定,验证请求(包含 /shop/newAccount.do)也应该由 AccountFormController 来处理。下面的 SpringAjaxController 代码正是这样做的:

...
protected BaseCommandController getController(String actionUri) {
BaseCommandController baseCommandController = null;

try
{
// Attempt to find the controller by bean name
baseCommandController = (BaseCommandController)
applicationContext.getBean(actionUri);
LOG.debug("Found baseCommandController (by bean name): " +
baseCommandController);
} catch (NoSuchBeanDefinitionException nsbde) {
LOG.debug("BaseCommandController not found by bean name, " +
searching AbstractUrlHandlerMappings");
}

if (baseCommandController == null) {
Map<String, AbstractUrlHandlerMapping> map =
applicationContext.getBeansOfType(AbstractUrlHandlerMapping.class);

for (String mappingName : map.keySet()) {
AbstractUrlHandlerMapping mapping = map.get(mappingName);

Map<String, BaseCommandController> handleMap =
mapping.getHandlerMap();
baseCommandController = handleMap.get(actionUri);

if (baseCommandController != null) {
LOG.debug("Found baseCommandController (by " +
"AbstractUrlHandlerMapping): " + baseCommandController);
break;
}
}
}

return baseCommandController;
}
...

虽然修订后的代码利用了 Spring 的高级功能,但从下面这一点可以看出新的设计还是沿用了前一种设计的思想:动态地实例化命令类,把请求输入绑定到新的命令类实例,调用与 Controller 相关联的所有 Validator,并将处理结果返回给浏览器。也能在 AccountFormController 中找到这些代码:

...
LOG.debug("Instantiating command");
Class<Object> commandClass = controller.getCommandClass();
Object command = BeanUtils.instantiateClass(commandClass);

LOG.debug("Populating command object with request values");
BeanWrapper beanWrapper = new BeanWrapperImpl(command);
beanWrapper.setPropertyValues(nameValuePairsMap);

LOG.debug("Invoking validators");
String commandName = controller.getCommandName();
Errors errors = new BindException(command, commandName);
Validator validators[] = controller.getValidators();

for (Validator validator : validators) {
ValidationUtils.invokeValidator(validator, command, errors);
}

// For each input, get the first error message and assemble
Locale locale = LocaleContextHolder.getLocale();

LOG.debug("Building output");
for (String inputId : nameValuePairsMap.keySet()) {
String inputValue = nameValuePairsMap.get(inputId);
String unqualifiedInputId = StringUtils.unqualify(inputId);
String args[] = { unqualifiedInputId, inputValue };

String message = validationMessageFormatter.getFieldErrorMessage(errors, inputId, args, locale);
if (message == null) {
// There was no validation message, return an empty String
message = "";
}

resultMap.put(inputId, message);
}

return resultMap;

实例化命令类后,来自浏览器的输入值必须绑定到这个实例。虽然 Spring Web MVC 没有完成绑定,Spring 的 BeanWrapperBeanWrapperImpl 的具体实现负责所有实际工作,包括绑定多输入字段。因为 BeanWrapper 支持嵌套路径,所以浏览器端的输入 id 和值可以用于在表单背后填充复杂的域对象。例如,如果验证请求中包含 "user.firstname=Ted&user.lastname=Anderson&user.address.zipCode=55311"BeanWrapper 将遵循 JavaBean 命名约定,把 User 对象的 firstname 的属性值设定为 Tedlastname 属性值设定为 Anderson,把 User 下面的 Address 对象的 zipCode 值设定为 55311

建立好用于处理验证消息的服务层之后,接下来将讨论如何与修改后的服务器进行通信。下面一节将介绍 DWR 如何调用前端验证控制器。

使用 DWR 技术的 Ajax 表单验证

和前一种设计相同,在客户端和中间层的 Ajax 通信过程中使用了 Direct Web Remoting (DWR)。DWR 的原理是动态生成基于 Java 类的 JavaScript 代码. 使用 servlet 和必要的 JavaScript 基础架构,浏览器端的 JavaScript 调用可以透明地传输到服务器端,调用 Java 类,然后把处理结果返回给浏览器。这就是服务器端的逻辑(在这个例子中,是 Spring Web MVC 验证逻辑)怎样通过 Ajax 呈现给客户端。

为了简化配置,修改后的设计利用了新发布的 DWR 2.0 的特性和可选的基于注释的配置。Maik Schreiber 说 ,注释可以替代 dwr.xml 配置文件(使用在 DWR 1.x 上)或与其相结合。注释避免了使用庞大的 dwr.xml 配置文件。

正如 DWR 注释文档 中所描述的,用 DWR 注释配置应用程序只需三步。首先,必须在 web.xml 文件中指定 DWR 控制器 servlet。第二,把通过 DWR 呈现的每个类的全限定名添加到 web.xml 文件中,并以逗号隔开。第三,这些类的每一个都须要由 DWR 注释来修饰。为完成这些步骤,首先用 @RemoteProxy 注释来修饰每个类。这个类名,默认用做 JavaScript 脚本名(即 JavaScript 代理对象名)。在 JavaScript 代码中尽可能少地暴露 Java 层的信息,并且 @RemoteProxy 注释的 name 属性名用于明确指定 JavaScript 代理的脚本名,这是一种良好的习惯。因为我们想要呈现给 JavaScript 的类是 Spring 托管的 bean,所以 DWR 也需要使用 SpringCreator,并通过特定的名字来定位 Spring 的 ApplicationContext。最后,用 @RemoteMethod 注释来修饰每个方法,使其可以远程访问。一般,没有用此注释修饰的方法不能被远程访问。下面是一个经过修改的 SpringAjaxController 的例子:

@RemoteProxy(name="AjaxFormValidatorJS",
creator=SpringCreator.class,
creatorParams = @Param(name = "beanName",
value = "ajaxFormValidator"))
public class SpringAjaxController implements ApplicationContextAware
{
...
@RemoteMethod
public Map<String, String< validateString(String formActionUri,
String inputIdValuePairs)
{
Map resultMap = new HashMap();
BaseCommandController controller = getControler(formActionUri);
...
}
...
}

从上面这段代码可以看出,指导 DWR 使用 SpringCreator,通过 bean 名来定位 bean,要查找的 Spring ApplicationContext 中的 bean 名是一个 ajaxFormValidator。DWR 将会生成名为 AjaxFormValidatorJS 的 JavaScript 对象,来代理基于 Java 的服务实例。这个对象有一个 validateString() 函数,在调用时执行服务器端具有相同名字的方法。

使用 DWR 注释来修饰类将得不偿失,因为这样会在类与 DWR 之间创建不必要的运行时依赖。如果想在工程中重用 Java 层(或在不能使用 DWR 的环境下),会出现许多问题。对大多数开发者来说,这仅仅是小问题,但他仍是设计上不可忽视的问题。

一旦 validateString() 方法完成验证消息的映射,他必须以 JavaScript 的形式返回给浏览器。前一种设计手动地把处理结果组合成一串名称——值对,返回给浏览器,进行手动分析。不仅提供一种在 JavaScript 中调用 Java(或在 Java 中调用 JavaScript) 的良好机制,还帮助把 Java 对象转换成可以在 JavaScript 对象中使用的代表。因为 validateString() 方法返回的是 Map,所以 DWR 要使用内建的 Converter 把 map 转换成 JavaScript 数组。下一节将介绍,这个数组由一个回调方法调用,用于显示错误消息,来动态地更新页面。

通过 JavaScript 控制验证过程

虽然修改后的设计支持输入多个字段(id 值对),避免了使用硬编码的代理引用并可将表单动作的属性值包含在 Ajax 请求中,但在概念上,用于支持 Ajax 验证的 JavaScript 代码与前一版本功能相同。有个 onChange() 事件监听程序监听着每个由 Ajax 验证的表单输入,当输入字段改变时,这个监听程序就会调用 validate() 函数。

下面是具体实现:

<script type='text/javascript'>

function validate(inputArray) {
var request = "";
var formAction = "";

if (inputArray.length == null) {
<%-- Submit a single input field and value --%>
request = formatInput(inputArray.id, inputArray.value);
formAction = getFormAction(inputArray.id);
} else {
<%-- Submit multiple input field and value pairs --%>
for (var i = 0; i < inputArray.length; i++) {
var input = document.getElementById(inputArray[i].id);
var nameValuePair = formatInput(input.id, input.value);

request = request + nameValuePair;

if (i != inputArray.length) {
request = request + "&";
}
}

formAction = getFormAction(inputArray[0].id);
}

<%-- Invoke server-side logic --%>
AjaxFormValidatorJS.validateString(formAction,
request,
handleValidationResponse);
}

function handleValidationResponse(response) {
<%-- The response is an array of id/message value pairs --%>
for (inputId in response) {
var errorElementId = inputId + ".errors";
var validationMessage = response[inputId];

dwr.util.setValue(errorElementId, validationMessage);
}
}

function getFormAction(inputId) {
var currentElement = document.getElementById(inputId);

while (currentElement != null) {
if (currentElement.tagName.toLowerCase() == "form") {
<%-- Drop the http://servername --%>
var formAction = "";
var locationFragements = currentElement.action.split("/");

for (var i = 4; i < locationFragements.length; i++) {
formAction = formAction + "/" + locationFragements[i];
}

return formAction;
} else {
currentElement = currentElement.parentNode;
}
}
}
...

validate() 函数负责初始化验证过程,他的输入或者是单一表单输入元素或者是表单输入元素数组。参数的 length 属性指明输入是单一输入元素还是输入元素数组。在参数是单一表单输入元素的情况下,元素的 id 和值组合成名称——值对。在参数是表单输入元素数组的情况下,数组中每个元素的 id 和输入值也组合成名称——值对。无论是哪种情况,最终结果都是一串 id——值对,他们被发送到服务器端进行验证。正是这种能力(处理单一元素和元素数组)才使客户端对多输入字段的验证提供了支持。

正如在前面所看到的,服务器端的验证逻辑需要包含在请求中的表单的 action 属性值。看看前面的示例代码,getFormAction() 函数在输入元素的 onChange() 时间触发时执行,向上遍历浏览器的 DOM,一直找到 form 元素为止。使用 form 元素的引用读取表单的 action 属性值,并把他作为验证请求的一部分传送到服务器。通过动态确定元素的属性值,JavaScript 代码更加具有灵活性和易用性。这样,此代码可以在任何表单上运行,也可以将他包含在所有网页中(Tiles 或者 SiteMesh 这样的工具可以使这些工作变得更加容易)。

使用 DWR 生成的 AjaxFormValidatorJS 对象,请求 字符串(包括输入名称——值和表单动作属性值)被发送到服务器,浏览器等待响应。

当响应从服务器返回后,调用 handleValidationResponse() 回调函数,并传递服务器生成的 DOM 元素的 id 数组和验证消息。发送到服务器的每一个输入元素在响应数组中将会有一个相应的进入点。 handleValidationResponse() 函数遍历整个数组,将元素的 id 和验证消息传递到 DWR 的 code>dwr.util.setvalue() 函数(他更新验证消息的特定元素)。浏览器的 DOM 立即更新,这种消息的显示/隐藏的效果构成客户端的验证形式,大大提高了 web 应用程序与用户交互的效率。

结束语

本文讨论了前一种设计的缺点:不能将 Ajax 表单验证方便地集成到基于 web 的应用程序中。讨论完这些缺点后,我们还介绍了一种改进且简化的设计(使用单一前端控制器、DWR 注释 Converter、更新的 JavaScript),从而极大地简化了配置,并最大限度地提高了可重用性。将自定义代码替换成 Spring 函数,不但简化了代码而且还可支持多输入字段的验证。最后,我们不用再将设计格式强加给 Spring Validator,因此经过修订后的应用程序代码不用重新构造便可支持 Ajax。

参考资料

Eric Spiegelberg 是明尼阿波利斯的一位 Java/EE 顾问,擅长基于 web 的软件开发。