5.4 Struts和Ajax的集成
Struts是添加Ajax交互的理想框架。Struts良好地封装了MVC Model 2的开发细节,同时也提供了足够的对应用细节的访问机制,这样开发人员就可以根据自己的意愿定制应用程序。Struts特别突出了一个方面,就是用户输入验证的灵活性,借助Struts的各种验证扩展点,我们可以很容易地把它与Ajax集成。
下面的例子演示了Struts验证可以和Ajax集成得多么好。该例子是一个虚构的宾馆房间预订系统。它使用网页收集必要的信息,如预订日期、是否要禁烟房间、是否有任何特殊要求,以及客户的名字和电话号码等。图5-2展示了本例的网页。

图5-2 相当简单的宾馆预订系统
代码清单5-1列出了呈现这个屏幕的JSP页面。正如所看到的那样,这个JSP中并没有什么高深的技术。我们使用text和textarea这样的Struts HTML自定义标签创建输入元素,而errors标签则用来显示任何错误信息。
代码清单5-1 hotelReservation.jsp






正如所预料的那样,这个页面中需要一些输入验证。除了Special Requests文本框之外,所有的字段都是必须输入的。此外,到达日期和离开日期必须是合法的日期格式,并且电话号码的格式也必须是正确的。输入元素会在表单提交时进行验证,如果验证失败,那么就会在页面的顶部显示错误信息。到现在为止,一直都还不错。
除了刚刚提到的验证之外,还有什么其他的验证吗?在本例中,答案是(就像明尼苏达人的说法)“当然有”。用户会希望尽快知道他想要的房间是否可以预订。例如,用户可能要在某段日期内预订一间可以吸烟的房间。如果在这段日期中,宾馆只有禁烟房间,那么网页就应该告知用户他选择的日期与可以吸烟的房间这两个条件是矛盾的。这时,用户要么选择其他日期,要么选择禁烟房间。用户可以尽快知晓在他选择的日期内是否有满足其吸烟需求的房间,这难道不好吗?
有了Ajax,我们才可以真正向用户提供这种功能。我们可以使用JavaScript在浏览器中完成简单的输入验证,如必须填写的字段或格式正确的日期,并在用户提交表单之前显示验证的反馈信息。但是如果要检查房间是否可用,那么我们大概就需要访问服务器端的资源来确定用户要求的房间是否可用了。Ajax允许我们不用提交表单就能做到这一点。
为了支持这一功能,我们精心设计了输入字段的顺序。我们把到达日期、离开日期和吸烟嗜好放置在页面的最前面,这样一来,用户就可以马上看到想要的房间是否可用了。如果房间不可用,那么用户可以修改他的选择或退出页面,而无需填写其他输入字段。
5.4.1 Ajax驱动的验证
我们的例子使用Ajax来确定房间、日期和吸烟嗜好的组合选择是否有效。如果没有房间可以满足这个组合选择,那么就会在页面中显示错误信息。
既然要使用Ajax,我们就一定要编写一些讨厌的JavaScript代码。在本例中,我们将使用第3章中介绍过的Prototype库处理JavaScript编程的一些细节,如访问输入元素的值,以及创建并使用XMLHttpRequest对象。你可能会惊喜地发现,整个Ajax验证的JavaScript代码只有不到50行。代码清单5-2给出了本例中使用的全部JavaScript源代码。
代码清单5-2 hotelReservation.js



正如在图5-2中看到的那样,这个网页中只有一个按钮,它会把表单提交给服务器。那么如何触发Ajax验证呢?如果回过头去看看代码清单5-1,就会看到:到达日期、离开日期和吸烟嗜好这几个输入字段都实现了onblur事件处理程序。这些onblur事件处理程序会在输入字段失去焦点时触发指定的JavaScript函数。我们想要达到的效果是:用户依次填写输入字段,一旦填写好了到达日期、离开日期和吸烟嗜好后,就会向服务器发送一个Ajax请求,确认对于用户的选择是否有可用的房间。到达日期、离开日期和吸烟嗜好的onblur事件处理程序会调用代码清单5-2中列出的validateForm函数。
validateForm函数先要调用isFormReadyForValidation函数确定用户是否填写了到达日期、离开日期和吸烟嗜好。如果到达日期、离开日期和吸烟嗜好的值都不为空,那么isFormReadyForValidation函数就会返回true;它会委托hasEntry函数检查到达日期和离开日期。
假设isFormReadyForValidation函数返回true,那么validateForm函数就会调用sendFormForValidation函数。sendFormForValidation函数负责真正发送Ajax请求。这个函数一开始使用Prototype的Form.serialize方法构造将要被发送到服务器的表单值的查询字符串。然后,它会把当前的时间戳追加到查询字符串中,确保URL的唯一性,避免浏览器缓存请求。
准备好查询字符串之后,sendFormForValidation函数就使用Prototype的Ajax.Request对象构造并发送Ajax请求。我们使用GET方法把Ajax请求发送到validate.do这一URL中。我们还声明该请求应该被异步地发送,并使用handleResponse函数处理服务器的响应。handleResponse函数简单地更新位于页面顶部的div元素的innerHTML属性,显示服务器返回的错误信息(如果有的话)。
这就是我们需要编写的JavaScript!利用Prototype库,我们只需编写不到50行代码就可以实现这些功能,相对于为应用添加的丰富功能和可用性来说,这真是很小的代价。
现在,浏览器端的编程就完成了。你已经看到了构成主页的HTML和实现Ajax请求的JavaScript。现在让我们把注意力转向服务器端,在这里Struts会帮助我们处理请求并执行必要的验证。
5.4.2 实现Struts
客户端的JavaScript编程是本例中最难的部分。如果你曾经使用过Struts或其他基于动作的Web MVC框架,那么本例的余下部分对你来说简直是轻而易举。
Struts的核心是struts-config.xml文件。struts-config.xml文件是把各种动作、动作表单、请求以及验证关联到一起形成可用的Web应用的粘合剂。我们将会在后面几页中多次提及本例的struts-config.xml文件,因此最好先来看看它。代码清单5-3给出了本例中使用的struts- config.xml文件。
代码清单5-3 struts-config.xml




这个struts-config.xml文件是通过修改Struts自带的配置文件得到的。该文件中最重要的部分是form-beans和action-mappings元素。form-beans元素含有零个或多个form-bean子元素,后者声明了应用中用到的ActionForm类。
在本应用中只有一个动作表单:ReservationForm类。ReservationForm类是一个扩展ValidatorActionForm类的简单JavaBeans风格的对象,它为每个表单元素提供了公共获取方法和设置方法。ReservationForm类的源代码如代码清单5-4所示。
代码清单5-4 ReservationForm.java







细心的读者可能会注意到,ReservationForm类扩展了Struts的ValidatorActionForm类,而不是ValidatorForm类。这一选择是有充分理由的。ValidatorActionForm和ValidatorForm都提供基于validation.xml的基本字段验证。对ValidatorActionForm来说,传入验证程序的键是action元素的path属性,它应该与form-bean的name属性匹配。对于ValidatorForm来说,传入验证程序的键是action元素的name属性,它应该与form-bean的name属性匹配。换句话说,扩展ValidatorActionForm意味着validation.xml(代码清单5-6)中的验证规则会基于请求的路径应用到请求上;扩展ValidatorForm意味着validation.xml中的验证规则会基于请求使用的表单bean应用到请求上。
struts-config.xml中最主要的内容都声明在action-mappings部分中。各个action元素真正负责把请求连接到其表单bean,并指明在被action类处理后应该把请求导航到何处。预订房间动作是第一个声明的动作,它简单地显示空的预订页面。
代码清单5-4中的下一个动作是validateReservation动作。你可能会回想起来这个动作就是代码清单5-2中处理到达日期、离开日期和吸烟嗜好验证的Ajax请求的目的地URL。这个动作映射中有很多内容,因此我们打算把它分解开来并逐一讲解。
我们通过配置validateReservation动作的name属性令其使用reservationForm表单bean——这没什么稀奇的。我们把validate属性设置为true,这样就会在调用Action对象前调用表单bean的validate方法。
validateReservation动作的input属性是这一元素的关键。这个input属性告诉Struts在出现任何验证异常时应该发送的响应。在非Ajax应用中,这个属性通常会指向与向动作提交请求的相同页面,而提交请求的JSP通常会使用Struts自定义的JSP标签把错误信息显示在页面中。但是,我们的例子会使用Ajax,因此当发生任何验证错误时,整个页面并不会被重绘。相反,只有页面的一小部分会被更新以便显示错误信息。回头查阅一下代码清单5-1。在页面的顶部有一个id属性为errors的div标签。这就是将会显示服务器返回错误信息的地方。你应该还记得代码清单5-2中的handleResponse JavaScript函数,它使用服务器返回的响应更新div中的错误信息。
ValidateReservation动作的input属性指向reservationErrors.jsp文件,后者如代码清单5-5所示。它使用Struts自定义标签来生成在验证过程中被记录下来的任何错误信息。因为这是一个只会更新页面中一小部分的Ajax请求,所以这就是需要生成的全部输出。
代码清单5-5 reservationErrors.jsp

错误信息被构建为一个无序列表。本例使用logic:messagesPresent、html:messages和bean:write标签构建错误信息的无序列表。你也可以使用html:errors这个简单的JSP标签,但是必须在消息属性文件中指明这个标签的HTML标记。
ValidateReservation动作含有两个forward元素:一个针对valid情况(即没有记录验证信息),一个针对invalid情况。valid情况的路径是名为blank.jsp的JSP文件。顾名思义,这个文件几乎不包含任何内容——事实上,它只包含一个非间断空白(nonbreaking space, )。它会除去页面中可能存在的任何信息。例如,如果用户输入了一个非法格式的日期并收到了一个验证错误,该错误信息应该在用户改正了日期并离开该输入字段后马上从页面中除去。
啊哈!这样我们就结束了对struts-config.xml中的validateReservation动作的说明。在这短短的8行XML中包含了很多内容,因此如果你还是对其中的某些配置项一知半解,那就应该重新阅读本小节,直到完全理解了所有的内容为止。
现在应该讨论一下验证的真正实现。在前面的几页中你已经看到了如何使用Ajax请求验证用户输入,并且看到了如何配置struts-config.xml文件把各个方面关联到一起,但直到现在我们也没有介绍验证的真正实现。还记得之前讨论过的Struts验证表单输入的不同方法(通过Commons Validator、表单bean的validate方法,或者甚至Action类本身)吗?本例中将会同时使用这三种方法。我们使用Commons Validator依据validation.xml文件中描述的验证规则执行简单的格式验证。对Commons Validator来说太复杂的输入验证将会通过表单bean的validate方法实现。最后,需要访问服务层和数据层的验证将会在Action类中实现。
可以把Commons Validator以及它与Struts的集成看作输入验证的第一道防线。无需编写任何Java代码,就可以通过编辑XML文件涵盖Web应用的很大一部分验证。本例将使用Commons Validator执行在表单输入提交到服务器时必须进行的格式验证,无论该表单提交是否通过Ajax请求。
代码清单5-6列出了本例中使用的validation.xml文件。本例将验证两种表单提交:验证到达日期、离开日期和吸烟嗜好的Ajax请求,以及提交整个表单的“正常”提交。
代码清单5-6 validation.xml



以上代码中列出的第一个表单是validateReservation表单。该表单通过form元素的name属性识别。因为ReservationForm类扩展了ValidatorActionForm,所以form元素的name属性代表的是该请求发送到的URL——在本例中是validateReservation.do。在该表单中会执行到达日期和离开日期的格式验证,以及到达日期、离开日期和吸烟嗜好的验证。图5-3展示了当用户输入了非法的到达及离开日期、选择了吸烟嗜好,并离开吸烟嗜好选择字段时在页面中显示的错误信息。

图5-3 通过Ajax提交非法日期格式时生成的错误信息
name属性为saveReservation的第二个表单负责在用户使用Submit按钮显式提交整个表单后执行验证。它包含与validateReservation表单相同的到达及离开日期验证,但是还包括必须填写名字和手机号码的验证以及手机号码的格式验证。
一定要在表单提交时验证用户的输入
即使已经使用Ajax技术验证了表单字段,也一定要在表单提交到服务器时验证用户的输入。为什么呢?
用户也许能够禁用通过Ajax实现的验证,但是他们绝对无法禁用服务器端的验证。用户能够在浏览器中禁用JavaScript,这会使通过Ajax实现的验证毫无用处。倘若由于Ajax验证的失败而使表单提交了非法的数据,那么就会引发很严重的问题。不过,如果在提交表单时服务器端会重新执行表单验证,那么这一问题就会迎刃而解。
本例中使用正则表达式验证日期和手机号码的格式,在文件的顶部将这些格式列为常量。此外,我们还把错误信息和标签提取到名为MessageResources.properties的属性文件中。注意,该文件是在struts-config.xml文件中指定的!它包含Commons Validator使用的标准错误信息格式,同时我们也可以把用户定义的信息放在这里。代码清单5-7列出了在标准MessageResources.properties文件中添加的自定义标签和错误信息。
代码清单5-7 本例为MessageResources.properties文件添加的信息

现在,你已经看到了使用Commons Validator执行的第一层验证。下一层验证发生在表单bean自身中。回头查阅一下代码清单5-4中给出的ReservationForm类的源代码。这里,validate方法确保用户输入的到达日期位于离开日期之前。我们可不希望某些客户在到达之前就离开了。这是一个Commons Validator无法轻易实现而Java代码却能很容易完成的验证实例。
如果Commons Validator或ReservationForm的validate方法没有记录任何验证错误,那么就会将请求发送给Action处理程序。本例中的Ajax请求由ValidateReservationAction类处理。这个类扩展了Struts的Action类并重写了execute方法来验证用户输入的到达日期、离开日期和吸烟嗜好。更确切地讲,该execute方法会访问服务层,并请求服务层验证是否有满足用户请求的到达和离开日期以及吸烟嗜好的房间。ValidateReservationAction类的源代码如代码清单5-8所示。
代码清单5-8 ValidateReservationAction.java



Action类是放置这种验证程序的最合理的地方。因为需要编写Java代码,这种验证不能通过Commons Validator实现,至少不编写自定义的validator类是无法实现的。从技术上来看,我们可以在表单bean中实现这种验证,但这是不合理的,因为表单bean本身就应该是一个不包含过多业务逻辑的“哑”对象。Action正是Web层与服务层和数据层之间的桥梁。因为这种验证需要访问数据库查看用户请求的房间是否可用,所以把它放在Action类中是再适合不过的了。
你可能已经注意到了,execute方法本身并没有执行任何验证。相反,它会调用ReservationService对象执行真正的查询,看看是否有符合用户请求的到达和离开日期以及吸烟嗜好的房间。为了保持本例简单,该方法使用一个随机算法,其结果是用户请求的房间有大概三分之一的可能性不符合要求。ReservationService的源代码如代码清单5-9所示。
代码清单5-9 ReservationService.java


在前面几页中我们已经讨论了不少概念和代码,因此我们应该稍微停一下脚步并考虑一下我们一直想要实现的到底是什么。这个例子的主要目的是提供使用Ajax的表单验证,这样用户就可以在输入表单后马上得到反馈信息。这些验证不仅会执行像确保输入日期格式正确这样的简单验证,而且还会执行用户请求的到达日期、离开日期和吸烟嗜好是否能产生可用的房间这样的复杂验证。我们使用Ajax在用户离开这三个字段后把这三个选项发送给服务器以便执行验证。如果用户为到达和离开日期输入了合法的日期、选择了吸烟嗜好、离开了吸烟嗜好选择字段,并且服务器确定没有可用的房间符合这些条件,那么页面顶部就会显示出错误信息,提醒用户他请求的房间不可用,如图5-4所示。

图5-4 利用Ajax,如果请求的房间不可用,用户马上就能知道这一点
当然,如果用户无法真正地保存预订,那么这个小小的应用就无法完成。在输入所有必需的信息后,用户可以单击提交按钮来保存本次预订。当然,程序会执行所有的输入验证来确保所有必需的输入字段都有合法的选择,并且日期和电话号码都具有正确的格式。你已经在validation.xml文件和表单bean的validate方法中见过这些验证了。
我们通过struts-config.xml配置文件把保存请求发送到SaveReservationAction类。正如你在之前的ValidateReservationAction类中看到的那样,这个类也扩展了Struts的Action类并重写了execute方法。该execute方法尝试通过ReservationService类的saveReservation方法把预订请求保存到数据库中,你在代码清单5-9中已经见过这些了。注意saveReservation方法仍然会检查预订请求是否可用。如果不可用,就会抛出一个ReservationNotAvailableException并显示一个错误信息。
如果所有的输入字段都是格式正确的并且预订是可用的,应用就会保存该预订并为用户显示一个确认页面。该确认页面简单地打印出用户输入的数据,如代码清单5-10所示。图5-5展示了一次成功的预订。
代码清单5-10 reservationSuccessful.jsp



图5-5 一次成功的预订
5.4.3 Struts和Ajax的设计考虑事项
本章中给出的例子深入研究了把Ajax集成到基于Struts的应用。你已经看到我们如何使用Ajax技术扩展Struts现有的功能并赋予其新的特性。
这个例子展示了一个从头开始构建的应用,它利用Ajax在某些特定的方面改进了用户的体验。不过,为现有的应用添加这些功能同样很容易。想像一下如果没有Ajax这个应用会如何工作:如果请求的房间(根据到达日期、离开日期和吸烟嗜好)不可用,用户不会马上获得反馈信息。客户需要完成整个表单的输入、单击提交按钮,然后如果请求的房间不可用,应用就会刷新整个页面并在页面顶部显示错误信息。
就本例而言,构建这种应用的代码几乎和使用Ajax的版本一模一样。应用仍然会包含产生用户界面的JSP、一个含有用户输入的ActionForm、一个处理提交请求的Action、一个实际执行预订工作的服务,以及使用Commons Validator执行简单的表单验证。
使用Ajax的版本只是简单地添加了一个专门处理Ajax请求的新Action以及产生Ajax响应的一些JSP。validation.xml文件包含了Ajax请求的验证,并且这些规则几乎和完全提交表单版本的规则一模一样。最后产生不到50行的JavaScript代码,并且由于使用了Prototype库,这些JavaScript代码也是非常简单的。当在浏览器中禁用JavaScript时,本例中的解决方案仍然是优雅的。如果禁用JavaScript,onblur事件处理程序就永远不会被调用,这样一来Ajax验证代码根本就不会执行。但是,当把表单提交到服务器时,它仍旧会被验证,这就避免了非法输入导致的错误。
我们想要说的是,为现有的Struts应用添加Ajax特性是一项相对容易的工作,而这些Ajax特性很可能会让你的用户齐声欢呼。在Struts流行起来之前,由于不得不与底层的servlet API打交道,所以创建Web应用是十分困难的。Struts扫清了使用Java快速开发Web应用的很多障碍。你再也不用编写一大堆servlet类,并不停地为每个新的servlet修改web.xml文件了。
Struts已经减轻了与servlet API打交道的负担,不过你还可以使用Ajax使其在浏览器和服务器间异步地通信。Struts使得在服务器端编写处理Ajax请求的Action变得非常简单。事实上,如果你正在为现有的Struts应用添加Ajax,那么你可能只需编写一个新的Action类并把它与现有的对象和服务关联到一起即可。
正因为如此,没有理由不把Ajax的精华引入现有的Struts应用中。在你的应用中寻找为了更新页面的一小部分而刷新整个页面的地方。可能的例子包括显示简单的验证信息、根据用户单击表格表头排序表格,或为页面搜索添加结果。用户会因为你把这些场景中传统的整个页面刷新改进为使用Ajax而深深地爱上你。使用Struts,创建服务器端组件并把这些组件关联到你在前面章节中看到的任何Ajax框架和库会变得非常简单,你可以立刻毫不慌乱地为你的应用添加Ajax特性。







