Seam - 无缝集成 JSF,第 3 部分: 用于 JSF 的 Ajax

用 Seam Remoting 和 Ajax4jsf 无缝熔接客户机和服务器

JSF 基于组件的方法论促进了抽象,但大多数 Ajax 实现由于公开了底层的 HTTP 交换而使之大受干扰。在 无缝集成 JSF 系列最后的这篇文章中,Dan Allen 展示了如何使用 Seam Remoting API 和 Ajax4jsf 组件与服务器上的受管 bean 通信,就好像这些 bean 与浏览器同在本地一样。您将了解利用 Ajax 作为 JSF 事件驱动架构的一种自然改进是多么地容易,以及如何在不影响 JSF 组件模型的前提下实现这一目的。

Dan Allen (dan.allen@mojavelinux.com), 高级 Java 工程师, CodeRyte, Inc.

Dan AllenDan Allen 目前是 CodeRyte 的一名高级 Java 工程师。他还是一名热情的开放源码拥护者,每当他看到企鹅时就会有一点疯狂。 从 Cornell 大学毕业并获得材料科学与工程学位,Dan 对 Linux 和开放源码软件非常着迷。从那以后,他就沉浸在 Web 应用程序领域,最近几年则专攻 Java 相关的技术,包括 Spring、Hibernate、Maven 2 和丰富的 JSF 堆栈。您可以在 http://www.mojavelinux.com 订阅 Dan 的 blog,以跟踪他的开发经验。



2007 年 6 月 25 日

时下,大多数 Java™开发人员都很看好 mashup,所以您可能会困惑:Seam 与号称 Web 2.0 的技术,尤其是 Ajax,如何能集成。若能使用 Seam 启动 JSF 中的部分页面更新或者用 Google Map 协助 JSF 应用程序 mashup,那将非常酷,不是么?您不仅能这么做,而且还非常容易。

更多 Ajax 技术资源!

请访问 Ajax 技术资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何关于 Ajax 的新信息都能在这里找到。

无缝集成 JSF系列的最后一篇文章中,我将为您展示如何使用 Seam Remoting API 和 Ajax4jsf 组件来协助基于 JSF 应用程序中的 Ajax 风格的交互。正如您将会看到的,结合 Seam 和 Ajax 的最大好处在于它让您可以享用所有 Web 2.0 的奢侈东西,而同时又 不需要陷于使用 JavaScript XMLHttpRequest 对象的痛苦之中。借助 Seam Remoting 和 Ajax4jsf,可以与服务器上的受管 bean 通信,就好像这些 bean 与浏览器同在本地一样。浏览器和服务器状态保持同步,而且永远无需处理促成它们之间通信的低层 API。

我首先会为您展示 Seam 是如何推动 Ajax 编程的基于组件的新方式的。您将学会如何使用 Seam Remoting API 来通过 Ajax 进行 JavaScript 和服务器端对象间的通信。一旦理解了这种面向 Ajax 的新(且简单的)方式,您就可以使用它来增强 Open 18 应用程序,方法如下:

  • 在 Open 18 球场目录和 Google Maps 之间创建一个 mashup。
  • 使用 Ajax4jsf 合并应用程序的球场目录页和球场细节页。
  • 重新访问应用程序的 Spring 集成并让 Spring bean 在 Seam Remoting 的生命周期可用。

升级到 Seam 1.2.1.GA

Seam 世界发展迅猛,自本系列上一篇文章发表以来,该框架的新版本业已发布。本文中的示例引用的是 Seam 1.2.1 或更高版本。在 Seam 的更早版本中,服务器端的 remoting 功能完全由 SeamRemotingServlet实现。而在最近的版本中,该功能性已重构到 Seam Remoting 包,而且还用了一个通用的 ResourceServlet来指派非 JSF 请求的处理,比如 remoting 调用。 此外,remoting 库现在还被打包成一个单独的 JAR,即 jboss-seam-remoting.jar。除了运行本系列 “Seam- 无缝集成 JSF,第 1 部分 : 为 JSF 量身定做的应用程序框架” 中的示例所需的 JAR 之外,还需要将该 JAR 包括到应用程序类路径中。

Open 18 和 Google Maps 之间的 mashup 让用户可以定位地图中的高尔夫球场目录中的位置。将此球场目录和球场细节页合并起来(并将低层代码 Ajax 化)可以让您显示球场的细节信息而无需加载新页。将 Spring bean 和 Seam Remoting 相集成让您可以捕获 Google Maps 位置标记的重定位并能将相关球场的经度和纬度存储到数据库中。如您所见,结果就是会产生所有高尔夫球员都喜欢使用的 Web 2.0 风格的应用程序,这真是让人印象深刻!

如果您曾经深受涉及到大量 JavaScript 的过于复杂的 Ajax 编程之苦,如果到目前为止,您都由于不想面对其复杂性而一直尽量避免使用 Ajax,那么本文所要教授的内容将会帮助您免除这种担心。在重构应用程序时,您需要进行一些 JavaScript 编码,但与大多数 Ajax 实现不同,JavaScript 并不会占据您代码中的大部分;相反,它只扩展了服务器端的 Java 对象。

通向 Ajax 的不同之路

正如在应用程序中希望避免显式的内存管理一样,您亦 希望必须要处理低层的 Ajax 请求协议。这么做只会带来更大的麻烦(更确切地说,是更多的麻烦),比如多浏览器支持、数据封送处理、并发冲突、服务器负载以及定制 servlet 和 servlet 过滤器。其中您想要避免的最大的麻烦是无意间公开的无状态的请求 - 响应范例,但该范例是基于组件的框架,比如 JSF,所想要隐藏的。

JSF 生命周期通过对底层的 servlet 模型屏蔽应用程序代码促进了面向组件的设计。为了保持处理 Ajax 的这种抽象性,您可以将低层的这些琐碎工作交由 Seam Remoting 或 Ajax4jsf 处理。这两个库均可负责通过 Ajax 交互将 JSF 组件熔合到浏览器时所需的管道处理。图 1 显示了实际应用中的 Seam Remoting。当事件触发时,比如用户单击了一个按钮,消息就会异步发送给服务器上的组件。一旦收到响应,它就会用来对此页进行增量更新。用来处理浏览器和服务器端组件间的交互的低层通信协议都藏于 API 之后。

关于本系列

Seam - 无缝集成 JSF讲述了 Seam 是第一个真正适合 JSF 的应用程序框架,能够修正其他扩展框架无法修正的主要弱点。阅读该系列的文章,你可以自己判断 Seam 是不是对 JSF 的适当补充。

在图 1 所示的用例中,用户能看到单击按钮后所发生的方法调用的结果。在研究此用例时,有两个要点需要注意: (1) 该页永远无法刷新;(2) 客户端代码与组件上的方法进行透明通信,而不是显式地构建然后再请求 URL。标准的 HTTP 请求在后台使用,但客户端代码永远无需直接与 HTTP 协议交互。

图 1. Seam Remoting 熔合 JSF 组件和浏览器
Seam Remoting 的一个用例示意图

Seam Remoting 和 Ajax4jsf

Seam Remoting 和 Ajax4jsf 是两个独特的库,可分别服务于 JSF 的 “Ajax 化” 的目的。两个库均使用 Ajax 来引入交互模型,其中浏览器和服务器间的通信可以在后台异步发生,并对用户不可见。没有必要为了执行服务器上的方法而浪费用户页面重载的时间。在这些库所发出的 Ajax 请求中由服务器检索到的信息可用来增量地 “实时” 更新页面的状态。两个库均可配备生命周期,此生命周期可以在浏览器需要的时候恢复(restore)组件的状态。这种 Ajax 交互并不是真的请求而是一种 “恢复并执行”。浏览器像是 “敲敲” 服务器的 “肩膀”,请它在服务器端的一个受管 bean 上执行一个方法并返回结果。

虽然这两个库工作起来有些差别,但它们并不是相互排斥的。由于它们都采用的是 JSF 组件模型,所以二者可以很容易地相互结合,这将在本文后面的部分详细介绍。目前,我们只需分别考虑二者各自将 Ajax 风格的交互引入 JSF 应用程序的方式:

  • Seam Remoting提供了 JavaScript API,可以使用这些 API 来像访问本地对象一样来访问 JavaScript 中的服务器端组件,以便通过方法调用发送和检索数据。Seam Remoting 使用定制的、非 JSF 生命周期来使该浏览器能够与服务器端的组件通信。只有 Seam 容器和其组件可以在这些请求期间被恢复。透明协议是 Ajax,但您无需费心数据包如何传输的细节。
  • Ajax4jsf则通过完全隐藏 JavaScript 的使用让抽象更进了一步。它将所有逻辑都包裹在基本 UI 组件内。Ajax4jsf 通过完整的 JSF 生命周期接受 Ajax 请求。因而,支持 Ajax 的组件可以在不触发浏览器导航事件的前提下执行动作处理程序、升级 JSF 组件树以及重新呈现该页的某些部分。同样地,通信也是通过 Ajax 实现的,但所有这些均在后台发生,页面开发人员不可见。Ajax4jsf 面向组件的方法让 Ajax 功能得以成为 JSF 很自然的一部分,而不是格格不入的外来者。

我将深入探究这些方式,但我们还是先来看看 Ajax 的基础知识吧。


缩小差异

要想让应用程序成为 Ajax/Web 2.0 意义上的 “富” 应用程序,Web 浏览器(也即客户机)必须能直接访问服务器上的组件。由于在客户机和服务器间存在着巨大差异,让这个目标成为现实还是很有挑战性的。差异的一个方面(即网络)存在于客户机浏览器,另一方面存在于服务器和其组件。Ajax 应用程序的一个目标是让它们可以相互 “通话”。

实际上,在大多数传统的 Web 应用程序中,客户机和服务器可以正常通信,但交互性就完全是另一回事了。服务器发起对话,浏览器收听对话。您如果以前曾陷于 类对话之中,实在不足为怪。在没有 Ajax 通信的世界里,浏览器可以发送对任何 URL 的同步请求,但它必须要呈现服务器发回的 HTML。这类交互性的另一个不足之处是等待时间很多。

若只有 HTTP 这种没有什么发展的语言,浏览器客户机将对服务器如何生成 HTML 完全束手无策,进而也就无从知道其组件的内容。从浏览器的立场上看,页面生成过程完全是个黑盒子。浏览器可以以 URL 形式询问服务器不同的问题并将打包成请求参数和 POST 数据的提示一并传递,但它其实并不 “讲” 服务器的语言。如果想要给浏览器提供一个到应用程序服务器端动作的视图,则需要建立更复杂的通信手段。这种面向页面的确定性方法对此无能为力。

Web 浏览器作为 Ajax 客户机

Seam Remoting 和 Ajax4jsf 对打破客户和浏览器组件的隔阂所采用的方式有所不同,所以很有必要知道如何利用好这二者。Seam Remoting 提供了浏览器本地语言 JavaScript 形式的 API,通过这些 API 服务器端组件上的方法就可以被访问到。若要将访问权赋予这些方法,它们必须通过 @Remote注释被显式地标记为 “remote”。

Seam Remoting 中的调用机制与 Java RMI 类似,二者都使用本地代理对象或 “存根” 来允许 JavaScript 调用位于远程服务器上的组件。就客户而言,该存根对象 就是远端对象。该存根负责实现实际远端对象上的方法执行。当远端方法被调用时,响应就会将此方法调用的返回值封装起来。返回值被编制处理成一个 JavaScript 对象。因此,Seam Remoting 就使浏览器可以 “讲” 服务器的本地语言。Java 代码和 JavaScript 现已合二为一,这对于那些认为这二者原本就是一种语言的人多少有点出乎意料。

与 Ajax 的对话

Seam 通向 Ajax 的方法让浏览器与服务器状态的复杂细节,比如哪些对话是当前的活动对话,利害相关。使用 Seam Remoting 来控制对话内容的能力会对日后需要在 conversation 作用域内的对象上进行远程调用十分重要。

另一方面,Ajax4jsf 提供了 JSF 组件标记来声明性地关联 UI 事件和服务器端受管 bean 上的动作处理程序。这些动作处理程序方法不需要被标记成 “remote”。相反,它们都是一些传统的 JSF 动作处理程序,或者是受管 bean 上的公共方法,这些方法或者无实参或者接受 ActionEvent

与 Seam Remoting 不同,Ajax4jsf 可以将 JSF 组件树中的更改返回到浏览器。这些更改以 XHTML 片段的形式返回。这些段与此页中单个的 JSF 组件相关联并证明其自身为部分页更新。因此,此页的隔离区域可以通过新的 标记由浏览器重新呈现。这些段都是特殊请求的,或者通过使用 Ajax4jsf 组件标记的 reRender属性,或者通过 将模板的多个区域封装到用属性 ajaxRendered="true"标示的输出面板中。reRender属性表示的是应该被重现的一个特定的组件集,由其组件 ID 加以引用。相比之下,使用 ajaxRendered="true"的方式就有些太过全面了,要求只要 Ajax4jsf 管理的 Ajax 请求完成,所有 “由 Ajax 呈现” 的区域都要更新。

Ajax4jsf 和 Seam Remoting 使浏览器从基本的 HTML 呈现器成长为一种成熟的 Ajax 客户机。当集成这两种框架时,应用程序 的确会让人兴奋不已。实际上(这个消息有点出人意料,所以您最好坐下来听),综合 Seam Remoting 和 Ajax4jsf 的功能可以让您从开发自己定制的 Ajax JSF 组件中解脱出来!您可以在 Ajax 通信中采用任何现有的、非 Ajax 启用的 JSF 组件,方法是在其声明中嵌套 a4j:support标记。如果您工作于 UI 组件之外(正如在后面的 Google Maps mashup 中所做的那样)且需要查询服务器端的组件以获取信息、更新该组件或指导它来执行操作,您可以用 Seam Remoting 管理此交互。

当 Seam Remoting 和 Ajax4jsf 在功能上有些重叠时,二者都可以很利于将 Ajax 风格的交互添加到应用程序。此外,您很快就会看到,两个库都为 JSF 应用程序提供了无缝的 Ajax 解决方案。


Seam Remoting 快速入门

如果 Seam Remoting 实现起来不是如此复杂的话,那么它真是一种理想的解决方案。不要担心! Seam Remoting 并不会如您曾领教过的那些远端 EJB 对象那样可怕。使用 Seam Remoting API 启用 JavaScript 代码来与服务器端组件进行交互最好的一面是过程异乎寻常地简单。Seam 真的可以为您完成所有的辛苦工作 —— 您甚至都无需编辑一行 XML,就可以开始使用它!(如果目前您进行 XML 编程要比 Java 编程多,那么这真是一个振奋人心的消息。)

让我们先来快速看看使用 Seam Remoting 来 “Ajax 化” JSF 应用程序所需的步骤。

公开 bean

将服务器端对象方法对远端 Ajax 公开有两个要求:此方法必须是 Seam 组件的公共成员且必须配备 @WebRemote 注释。就这两点!

实际的简单性在清单 1 中可见一斑,其中 Seam 组件 ReasonPotAction为了远端执行而 向 Ajax 客户公开了单一一个方法,即 drawReason()。每次这个方法在此存根上被调用的时候,该调用都会跨 Internet 传递到服务器并会通过在服务器端使用对应的方法随机选择所列的 “在项目中采用 Seam 的十大理由” 之一。服务器随后向客户返回该值(有关这十个理由的更多信息,请参见 参考资料)。

清单 1. 公开远端方法调用
@Name("reasonPot") 
 @Scope(ScopeType.SESSION) 
 public class ReasonPotAction { 
	 private static String[] reasons = new String[] { 
		"It's the quickest way to get \"rich\".", 
		"It's the easiest way to get started with EJB 3.0.", 
		"It's the best way to leverage JSF.", 
		"It's the easiest way to do BPM.", 
		"But CRUD is easy too!", 
		"It makes persistence a breeze.", 
		"Use annotations (instead of XML).", 
		"Get hip to automated integration testing.", 
		"Marry open source with open standards.", 
		"It just works!"
	 }; 
	
	 private Random randomIndexSelector = new Random(); 
	
	 @WebRemote 
	 public String drawReason() { 
		 return reasons[randomIndexSelector.nextInt(reasons.length)]; 
	 } 
 }

服务于资源

服务器端组件设置好后,需要让浏览器准备好来调用 @WebRemote方法。Seam 使用定制 servlet 来处理 HTTP 请求以便执行远端方法并返回其结果。无需担心:您将不必直接与那个 servlet 进行交互。Seam Remoting JavaScript 库负责处理所有的与 Seam servlet 的交互,而处理的方式也与它管理 XMLHttpRequest对象的所有方面相同。

您需要考虑使用这个 servlet 的惟一情况是在应用程序中设置 Seam Remoting 的时候。更好的是您只需配置 Seam 的定制 servlet 一次,而不管有多少 Seam 特性需要定制 servlet 的服务。与为每个特性使用特定的 servlet 相反 —— 该特性可能是将资源(比如一个 JavaScript 文件)提供给浏览器或处理一个非 JSF 请求(像 Ajax remoting 调用),Seam 将所有这些任务捆绑于单一一个控制器 Resource Servlet之下。这个 servlet 使用一个委托链模型,将这些任务传递到注册的处理程序。例如,Remoting对象(我稍后就会介绍)会注册其自身来接收所有由 Seam Remoting JavaScript 库发出的 Ajax 请求。

Resource Servlet 的 XML 定义如清单 2 所示,且必须安装于应用程序的 web.xml 文件中 Faces Servlet 之下:

清单 2. Seam Resource Servlet 的 XML 定义
<servlet> 
  <servlet-name>Seam Resource Servlet</servlet-name> 
  <servlet-class>org.jboss.seam.servlet.ResourceServlet</servlet-class> 
 </servlet> 
 <servlet-mapping> 
  <servlet-name>Seam Resource Servlet</servlet-name> 
  <url-pattern>/seam/resource/*</url-pattern> 
 </servlet-mapping>

自引导 API

Seam Remoting 机制真正的幕后原因是 JavaScript 库。Resource Servlet 将作业指派给 Remoting对象以服务这些库。所发出的两个 JavaScript 库建立适合浏览器的挂钩以便借助 JavaScript 调用远端方法。

必须要将这两个 JavaScript 库都导入页面,如清单 3 所示。第一个库 remote.js 是静态的,将 Seam 的客户端 remoting 框架传递给浏览器。第二个库 interface.js 是在每个请求上动态创建的。它包含远端存根和与服务器端组件进行交互所需的复杂类型。注意,Seam 并不为所有指定一个方法为远端的所有组件都生成存根和复杂类型。相反,它会解析 interface.js URL 中的 ?字符后面的该查询字符串来收集要公开的组件名。每一个组件名都由 &amp;符号分隔。使用这种堆栈式的方法可以使浏览器为外部 JavaScript 文件所进行的请求的数量为最小。Seam 之后会只为这些组件生成这些存根和复杂类型。作为一个例子,清单 3 中的这两个 JavaScript 导入将会加载 Seam Remoting 库并指示 Seam 准备好 reasonPotanotherName 组件:

清单 3. 导入客户端框架和 API
<script type="text/javascript"
 src="seam/resource/remoting/resource/remote.js"></script> 
 <script type="text/javascript"
 src="seam/resource/remoting/interface.js?reasonPot&amp;anotherName"></script>

有了这些,您就可以着手了!您已经完成了 Seam Remoting 设置,现在可以开始进行一些远程调用了。


进行远程调用

所有用于 Seam Remoting 库的客户端框架代码都封装在 SeamJavaScript 对象中。实际上,这个高层对象只是一个名称空间 —— 一个可以将一组功能捆绑在惟一一个名字之下的容器。在 JavaScript 中,名称空间不过是一个可以加载静态属性、静态方法和其他一些嵌套名称空间对象的对象。需要注意的 重要一点是 Seam Remoting 中的 JavaScript 与其他 JavaScript 可以很好地合作。这意味着您可以利用诸如 Prototype 或 Dojo 这样的库来让 JSF UI 与服务器端组件交互。在 JSF UI 中使用第三方 JavaScript 库过去一直是 JSF 开发人员所面临的一个巨大挑战,所以 SeamJavaScript 对象备受欢迎:

Seam Remoting 库的功能在 Seam.ComponentSeam.Remoting名称空间对象间分配。Seam.Component的静态方法提供了对远端 Seam 组件的访问,而静态 Seam.Remoting方法则用来控制 remoting 设置并创建定制数据类型。正如您从本示例中所见的,定制数据类型是任何需要本地创建以调用远端方法的非原语对象。

执行细节

要执行一个远端方法,首先要获得持有此方法的组件的一个实例。如果想要使用现有的 Seam 组件实例(意味着它已经存在于服务器),就可以使用 Seam.Component.getInstance();如果想要在进行调用之前先实例化一个新实例,则可以使用 Seam.Component.newInstance()。这些实例均是由 Seam 动态构建的远端存根。存根代理所有的方法调用,将组件名、方法和参数编排处理成 XML,并通过一个 Ajax 请求将 XML 有效负载传递到服务器端组件。服务器上的 remoting 框架代码获取这个请求,对这个 XML 进行解除编排处理,提取出组件名、方法和实参,然后再在服务器端组件实例上调用此方法。最后,此服务器以 Ajax 调用的响应向客户机发回返回值。同样,所有这些工作都藏于存根的背后,极大地简化了 JavaScript 代码。

至此,有关背景的介绍已经足够了。清单 4 显示了该如何执行 conversation 作用域内的 reasonPot组件上的 drawReason()方法,该组件在 清单 1中被标记为远端服务。一旦获得了组件存根,执行此方法就如同任何其他的 JavaScript 方法调用一样,不是么?

清单 4. 调用远端方法
<script type="text/javascript"> 
 Seam.Component.getInstance("reasonPot").drawReason(displayReason); 

 function displayReason(reason) { 
  alert(reason); 
 } 
 </script>

在清单 4 中,应该注意到存根上的方法和服务器端组件上的 “实际” 方法存在一个重要差异。您觉察到了么?每个在组件存根上的调用都是异步完成的。这意味着远端方法调用的结果并不能立即对执行线程可用。正因为如此,存根上的方法并没有要返回的值,而不管匹配的服务器端方法是否有要返回的值。当远端存根上的方法执行时,它实际上像是在说:“好吧,我稍后再答复您,请留下您的联系电话。”您所提供的码实际上就是 “回调” JavaScript 函数。其任务是捕获实际的返回值。

回调函数是异步 API 的标准结构。当来自 Ajax 请求的响应回到浏览器时,它由 Seam Remoting JavaScript 框架执行。如果服务器端组件上的方法具有非空返回值,该值就会作为其惟一的实参传递进此回调函数。另一方面,如果服务器端组件上的方法具有一个空返回类型,则可以忽略此可选参数。

conversation 作用域内的 bean

对于大多数 remoting 需求,您都可以使用到目前为止所介绍的 API 加以应对。若请求的给出是为了检索服务器端组件或执行其公开的方法中的一个,那么该组件必须在 remoting 生命周期内可见。引用 conversation 作用域内的 bean 不需要任何额外的工作,原因是这些 bean 总是对在同一个浏览器对话中发出的 HTTP 请求可用。

conversation 作用域内的 bean 需要更多技巧,因为它们基于对话令牌的存在而与 HTTP 请求相关联。当处理 conversation 作用域内的 bean 时,您必须要能够在远程调用期间建立正确的对话上下文。此对话通过与远程请求一同发送对话令牌来重新激活。这种交互的细节由 Seam Remoting 框架处理,只要您提供对话令牌即可。

请记住,Ajax 请求完全可以从相同的窗口发出以与此服务器上的两个或更多的对话通信。如果在检索组件实例存根之前指定了对话 ID,那么 Seam 就可以对它们进行区分。使用 Seam.Remoting.getContext().setConversationId("#{conversation.id} ")可以实现这一目的。

Seam 总是在 #{conversation.id}值绑定表达式下公开当前的对话。JavaScript 实际上可以看到解析后的值,通常是一个数值,而不是表达式占位符。您可以与 remoting 内容一起注册该值,它然后会由随后的请求传递。相反,您可以读在之前的请求中分配的这个对话 ID,方法是在远端调用发起后,调用 Seam.Remoting.getContext().getConversationId()。通过这种方式,remoting 调用就可以充分利用 conversation 作用域的益处 参与状态行为。


Google map 和 Open 18 应用程序

清单 1中的 Reason Pot 示例(“使用 Seam 的十大理由”)很有趣,但现在是时候该让 Seam Remoting 库大显身手了!将重点重新放到 “无缝集成 JSF,第 2 部分 : 借助 Seam 进行对话” 中所介绍的 Open 18 应用程序。您可能记得 Open 18 是个高尔夫球场目录。用户可以浏览球场的列表并随后深入查看单个球场的细节。Open 18 还允许用户创建、更新和删除球场。在其 Web 1.0 版本中,用户和这个应用程序间的每个交互都会使页面重载。

借助 Ajax,可以有多种改进 Open 18 应用程序的方法,在我们继续之前,您可以自己尝试其中的一些方法。第一个可以做的事情是在 Open 18 和 Google Map 之间创建一个 mashup。 时下,若不采用 mapping 实现,在 Internet 就走不多远,而如果添加此特性,您的用户当然会非常高兴。将 Seam Remoting API 和 Google Maps Geocoder API 结合起来让您可以定位 Google map 上的球场目录中的每个球场。

使用 Google Maps API

要使用 Google Maps API,您必须注册一个免费的 API key。Google 之所以要求申请这个 key 是为了维护有关 API 使用的统计数据和控制对此服务的滥用以便它能保持免费。申请这个 key 需要 Google 账号,但,根据服务条款,此 key 不能共享。为了遵守这些条款,我在示例中使用了一个假 key 。要在自己的计算机上运行 示例应用程序,需要用您自己的 key 替换字符串 GOOGLE_KEY

借用 Google 的世界视图

地理空间绘制乍听起来需要很多技巧,但若 Google Maps JavaScript API 能代您完成很多工作的话,那就另当别论了。GMap2类可以绘制地图并负责视图端口中的滚动和缩放事件。另一个 Google Maps 类 GClientGeocoder则基于地址字符串解析地理空间的坐标。您的工作只不过就是初始化地图并为每个球场添加标记。

要在地图上放上标记,首先要通过远端方法调用从服务器端组件获取球场集。接下来,用 GClientGeocoder将每个球场的地址翻译成地理空间点(经度和纬度)。最后,使用该点来在地图的相应坐标上放置标记。作为一个额外的小特性,您还可以将编辑图标旁边的罗盘图标装备在目录中的每行。当单击了目录行中的罗盘图标时,地图就会缩放直到所选球场出现在视图内。与此同时,此地图还会在标记上呈现一个气球,显示给定球场的地名、地址、电话和 Web 站点。通过直接单击地图上的一个标记也可以弹出相同的气球。图 2 显示的是完成后的应用程序的预览:

图 2. Google Maps mashup 的一个屏幕截图
Google Maps mashup 的一个屏幕截图

总的来说,地图组件和面向位置数据的集成很有趣,此 mashup 尤其具启迪意义。用户现在可以实际查看地图上每个球场的边界!在 Map 模式,高尔夫球场属性用淡绿色呈现。如果缩放功能足够强大,那么在球场区域还会出现一个标签覆盖图,显示此球场的名称。Satellite 模式则更有趣,它显示了球场的面貌。若缩放的程度足够,甚至还可以看到每个球洞的特征,比如发球台、球道和果岭!高尔夫球场位置的 mashup 以及互动性的地图视图就能让您的努力有所回报。


编织进 Google Maps

Google Maps 很易于集成和嵌入到 Web 应用程序。正如我已经提到的,它负责了所有呈现地图的所有细节并会提供一个 API 来绘制地图上的地理空间位置。GClientGeocoder对象担负所有解析地理空间位置和回送所需的经度和纬度数据这样的艰巨任务。

将地址解析为地理空间点的方法存根与 Seam Remoting 方法存根的工作原理相同。当该方法被调用来获取返回值时,一个回调函数会传递给此方法。在那时,还会向 Google HQ 发送一个 Ajax 请求,而且当响应回至浏览器时,此回调函数会执行。Google Maps API 的智能界面让定位变得非常文字化,因为所基于的只有邮寄地址。您无需再为每个球场维护地理空间的坐标,那样只会增加混乱!这个 API 上的额外的例程之后会使用纬度和经度数据来为地图上的这些位置构建呈现标记。

定制它!

配置 Google map 的显示可用的方法很多。配置此地图本身并不是本文的重点所在,留给您自己尝试练习。有关信息,请参看 参考资料。您可能还会使用高层的 JavaScript 函数,而不是使用面向对象的结构来封装此逻辑。同样地,您尽可以按您自己的意愿自由定制代码。

与地图集成相关的 API 方法有两个:Geocoder.getLatLng()GMap.addOverlay()。首先,Geocoder.getLatLng()方法将地址字符串解析为一个 GLatLng点。数据对象只用来包装经度和纬度值对。此方法的第二个实参是一个回调 JavaScript 函数,一旦与 Google HQ 的通信完成,该函数即会执行。此函数继续执行以通过使用 GMap.addOverlay()来将一个标记覆盖图添加到地图上。默认地,标记以红色的回形针表示。回形针的顶点指向地图上的地址位置。

清单 5 显示了设置 Google map 并向它添加标记的 JavaScript 代码。函数按执行顺序排列。除了新导入的 Google Maps API 脚本之外,您还应该识别出 清单 3中曾经用到的 Seam Remoting 脚本。

清单 5. 映射 Open 18 和 Geocoder
<script type="text/javascript"
    src="http://maps.google.com/maps?file=api&amp;v=2.x&amp;key=GOOGLE_KEY"></script> 
 <script type="text/javascript"
    src="seam/resource/remoting/resource/remote.js"></script> 
 <script type="text/javascript"
    src="seam/resource/remoting/interface.js?courseAction"></script> 
 <script type="text/javascript"> 
 // <![CDATA[ 
 var gmap = null; 
 var geocoder = null; 
 var markers = {}; 
 var mapIsInitialized = false; 

 GEvent.addDomListener(window, 'load', initializeMap); 

 /** 
 * Create a new GMap2 Google map and add markers (pins) for each of the 
 * courses. 
 */ 
 function initializeMap() { 
    if (!GBrowserIsCompatible()) return; 
    gmap = new GMap2(document.getElementById('map')); 
    gmap.addControl(new GLargeMapControl()); 
    gmap.addControl(new GMapTypeControl()); 
    // center on the U.S. (Lebanon, Kansas) 
    gmap.setCenter(new GLatLng(38.2, -95), 4); 
    geocoder = new GClientGeocoder(); 
    GEvent.addDomListener(window, 'unload', GUnload); 
    addCourseMarkers(); 
 } 

 /** 
 * Retrieve the collection of courses from the server and add corresponding 
 * markers to the map. 
 */ 
 function addCourseMarkers() { 
    function onResult(courses) { 
        for (var i = 0, len = courses.length; i < len; i++) { 
            addCourseMarker(courses[i]); 
        } 

        mapIsInitialized = true; 
    } 

    Seam.Remoting.getContext().setConversationId("#{conversation.id}"); 
    Seam.Component.getInstance("courseAction").getCourses(onResult); 
 } 

 /** 
 * Resolve the coordinates of the course to a GLatLng point and adds a marker 
 * at that location. 
 */ 
 function addCourseMarker(course) { 
    var address = course.getAddress(); 
    var addressAsString = [ 
        address.getStreet(), 
        address.getCity(), 
        address.getState(), 
        address.getPostalCode() 
    ].join(" "); 
    geocoder.getLatLng(addressAsString, function(latlng) { 
        createAndPlaceMarker(course, latlng); 
    }); 
 } 

 /** 
 * Instantiate a new GMarker, add it to the map as an overlay, and register 
 * events. 
 */ 
 function createAndPlaceMarker(course, latlng) { 
    // skip adding marker if no address is found 
    if (!latlng) return; 
    var marker = new GMarker(latlng); 
    // hide the course directly on the marker 
    marker.courseBean = course; 
    markers[course.getId()] = marker; 
    gmap.addOverlay(marker); 

    function showDetailBalloon() { 
        showCourseInfoBalloon(this); 
    } 

    GEvent.addListener(marker, 'click', showDetailBalloon); 
 } 

 /** 
 * Display the details of the course in a balloon caption for the specified 
 * marker.  You should definitely escape the data to prevent XSS! 
 */ 
 function showCourseInfoBalloon(marker) { 
    var course = marker.courseBean; 
    var address = course.getAddress(); 
    var content = '<strong>' + course.getName() + '</strong>'; 
    content += '<br />'; 
    content += address.getStreet(); 
    content += '<br />'; 
    content += address.getCity() + ', ' + 
        address.getState() + ' ' + 
        address.getPostalCode(); 
    content += '<br />'; 
    content += course.getPhoneNumber(); 
    if (course.getUri() != null) { 
    content += '<br />'; 
    content += '<a href="' + course.getUri() + '" target="_blank">' + 
        course.getUri().replace('http://', '') + '</a></div>'; 
    } 

    marker.openInfoWindowHtml(content); 
 } 

 // ]]> 
 </script>

清单 5 乍看起来真是有点吓人,但实际上,它所包含的内容并不多。页面加载后,Google map 就会使用 GMap2构造函数进行初始化、插入到目标 DOM 元素并会以美国为中心。成功了!您现在就有了 Google Maps 显示。比您所想的要容易一些,对么?一旦地图初始化,球场标记就会添加进来。代码的难点就是创建这些标记,所以让我们重点看看 addCourseMarkers()函数,其中嵌入了 Seam Remoting API。

定位球场

为了节省服务器资源,您希望远端调用检索相同的球场列表,这些球场是在页面被 JSF 呈现时加载进 conversation 作用域内的。要想确保持有这个集合的对话能够在远端调用的过程中被重新激活,此对话 ID 必须建立在 remoting 内容的基础上,正如本文之前讨论的那样。引用当前对话 ID 的这个值绑定表达式 #{conversation.id}在页面呈现时被解析,而其值随后会通过 setConversationId()方法传递给 Remoting Context。任何借助组件存根到远端方法的后续调用都会传递该值并激活相关的对话。对话激活后,同样的球场列表才会可用。

活动指示器

在运行示例时,您应当会注意到在调用期间,Seam 在页面的右上角放上了一个 “Loading...” 消息。这个消息让用户得以知道后台有动作发生,真是考虑周到的一种做法。如果您更愿意不显示这个消息或如果您更愿意定制它,那么您就能够通过 Seam.RemotingJavaScript 对象实现此目的。

要找到球场,下一步是使用 Seam.Remoting.getInstance()获取到 courseAction组件的引用,然后执行存根上的 getCourses()方法。正如先前所介绍的,存根方法会采用回调函数作为其最后的实参,该实参用来捕获响应中的球场列表。注意到返回值不再是原语类型,而是用户定义的 JavaBean 集。Seam 创建 JavaScript 结构来模仿服务器端组件的方法签名中所用的 JavaBean 类。在本例中,Seam 通过相同的 getters 和 setters 来将响应反编排到代表 Course条目的 JavaScript 对象。清单 5中的很多地方都使用了球场对象上的 getter 方法来读取球场数据。

在把球场放到地图上的最后一个步骤中,每个球场的地址都被 GClientGeocoder转换成 GLatLng点。该值随后被用来创建 GMarker小部件,这个小部件作为覆盖图添加到地图上。

没有罗盘的地图

现在,您的地图已经点缀了所有这些光鲜的标记,但还不可能将球场目录中的行与地图上的标记关联起来。这时就需要罗盘图标发挥作用了。我们将添加多一点 JavaScript,以便在该球场的罗盘图标被单击时,能使用 Google Maps API 来缩放和居中地图。清单 6 显示了罗盘图标的组件标记。该组件被插入到目录中每一行的编辑图标之前。球场编辑功能在 “无缝集成 JSF,第 2 部分 : 借助 Seam 进行对话” 中有详细的介绍。)

清单 6. 能呈现罗盘图标的组件标记
<h:graphicImage value="/images/compass.png" 
  alt="[ Zoom ]" title="Zoom to course on map"
  onclick="focusMarker(#{_course.id}, true);" />

JavaScript 事件处理程序 focusMarker()被注册用来处理罗盘图标上的 onclick 事件。清单 7 中所示的 focusMarker()方法在全局注册表中为该球场查找之前注册的 GMarker,然后将此工作分配给来自 清单 5showCourseInfoBalloon()函数:

清单 7. focusMarker() 事件处理程序着重于选定球场上的地图
/** 
 * Bring the marker for the given course into view and display the 
 * details in a balloon.  This method is registered in an onclick 
 * handler on the compass icons in each row in the course directory. 
 */ 
 function focusMarker(courseId, zoom) { 
    if (!GBrowserIsCompatible()) return; 
    if (!mapIsInitialized) { 
        alert("The map is still being initialized. Please wait a moment and try again."); 
        return; 
    } 
    var marker = markers[courseId]; 
    if (!marker) { 
        alert("There is no corresponding marker for the course selected."); 
        return; 
    } 

    showCourseInfoBalloon(marker); 
    if (zoom) { 
        gmap.setZoom(13); 
    } 
 }

存储球场集

清单 8 给出了服务器端组件上负责公开之前获取的球场的那个方法(为了简便起见,此清单中没有包括为 无缝集成 JSF,第 2 部分 : 借助 Seam 进行对话中的 Open 18 实现所创建的 CRUD 动作处理程序)。

清单 8. CourseAction 组件公开 getCourses 方法
 @Name("courseAction") 
 @Scope(ScopeType.CONVERSATION) 
 public class CourseAction implements Serializable { 
    /** 
     * During a remote call, the FacesContext is <code>null</code>. 
     * Therefore, you cannot resolve this Spring bean using the 
     * delegating variable resolver. Hence, the required flag tells 
     * Seam not to complain. 
     */ 
    @In(value="#{courseManager}", required=false) 
    private GenericManager<Course, Long> courseManager; 

    @DataModel 
    private List<Course> courses; 

    @DataModelSelection 
    @In(value="course", required=false) 
    @Out(value="course", required=false) 
    private Course selectedCourse; 

    @WebRemote 
    public List<Course> getCourses() { 
        return courses; 
    } 

    @Begin(join=true) 
    @Factory("courses") 
    public void findCourses() { 
        System.out.println("Retrieving courses..."); 
        courses = courseManager.getAll(); 
    } 

    // .. additional CRUD action handlers .. 
 }

正如之前所解释的,方法存根可以包括一个可选的回调 JavaScript 函数作为其最后一个实参。这也是为什么客户端存根上的 getCourses()对象只接受单一一个实参,而服务器端组件上的相应方法则不接受任何实参。getCourses()方法返回在呈现页面时所出现的球场集。


没有 FacesContext 的生活

至此,您可能会奇怪所有这些 Seam Remoting 请求是如何与 JSF 生命周期协作的。实际上,从典型意义上讲,它们并非如此。不惊动 JSF 生命周期才是让这些请求如此轻量的原因。从 JavaScript 在组件存根上调用方法时,从 Seam Resource Servlet 传递调用并由 Remoting指派对象进行处理。由于 Resource Servlet 通过 JSF 生命周期接受请求,所以 FacesContext 在远端调用过程中就 可用。相反,Resource Servlet 使用其自身的生命周期来并入 Seam 组件容器。底线是:当方法通过 Seam Remoting API 执行时,任何不解析成由 Seam 管理的对象的值绑定表达式都将是 null 的。此外,由 backing bean 使用的 JSF 组件绑定亦不可用。

在 Open 18 应用程序的初始配置中,CourseActionbean 依赖于 Spring 框架的 JSF 变量解析器通过值绑定表达式 #{courseManager}来将 CourseManager对象注入到 backing bean。其中的问题是,Spring 变量解析器依赖于 FacesContext 定位 Spring 容器。如果 FacesContext 不可用,则变量解析器就不能访问任何的 Spring bean。因而,当通过 Seam Remoting API 访问组件时,值绑定表达式 #{courseManager}将会是 null

如果在远端方法调用过程中需要大量使用 CourseManager对象,那么上述情形就会遇到问题了。因为 Seam 的默认行为是强制依赖项存在,您需要用属性 required=false标注 @In注释,以表明它是可选的。这么做会让 Seam 在调用通过远端存根进行时,不会有什么怨言。

下一节显示了如何使用 Ajax4jsf 来合并应用程序球场目录页和球场细节页。这种增强 的确需要服务层对象来保持对球场的更改。然而,由于 Ajax4jsf 在调用动作处理程序时使用的是完整的生命周期,因而 #{courseManager}表达式会正常解析。


用 Ajax4jsf 进行重呈现

Google Maps mashup 确实让人兴奋不已,但将它添加到 Open 18 应用程序却会引入一个小的设计问题。当用户为了查看有关此球场的详细信息而单击目录中的球场名时,页面就会刷新。结果,地图就会频繁地重载。这些重载会将太多负荷添加给浏览器和服务器,还会将地图恢复到其原始位置。

Ajax4jsf 项目

Ajax4jsf 项目最初由 Exadel 开始并托管于 Java.net。最近,JBoss 已经吸收了 Ajax4jsf 库作为其收购 Exadel 产品的一部分。当 Ajax4jsf 与 JSF 结合后,事件驱动的界面才真正得以实现。事件可以触发服务器上的动作,而且不会带来页面重载的代价,但 UI 还是要更新以反应页面结构上的更改。Ajax4jsf 业已证明非常符合 Seam 的目标,即提供一个构建在 JSF 之上的 “富” Web 平台。

要防止地图重载,必须将所有动作都限制为单一页面加载。不幸的是,提交表单的处理与页面请求紧密相连。这种紧密的耦合是 Web 浏览器的默认行为。您所需要的是数据能在浏览器不再请求页面的前提下被发送到服务器,而这恰恰是 Ajax4jsf 组件库的设计初衷。在目录清单中的 Name 列,用 a4j:commandLink替换 h:commandLink标记让 Ajax4jsf 在任何单击了球场链接的地方都能发挥其作用。当链接的 Ajax4jsf 版本被激活时,它就会执行在动作属性 #{courseAction.selectCourseNoNav}中通过方法绑定表达式表明的方法,然后返回包含要被插入到目录中的球场细节的替代 markup。

selectCourseNoNav()方法执行与 selectCourse()方法相同的逻辑,只不过它没有返回值来确保 JSF 不寻求导航事件。毕竟,Ajax 请求的意义就是防止浏览器再次请求页面。所返回的 XHTML markup 插入到地图下面的区域之内,而且不会破坏地图的状态。清单 9 中所示的组件代替了 “无缝集成 JSF,第 2 部分 : 借助 Seam 进行对话” 中 courses.jspx的原始版本中所用的 h:commandLink

清单 9. 用于选择球场的 Ajax4jsf 链接
 <a4j:commandLink id="selectCourse"
  action="#{courseAction.selectCourseNoNav}" value="#{_course.name}" />

正如之前提到的,有两种方法来指明页面中的哪个区域需要增量更新。一种方法是针对单个组件,Ajax4jsf 动作组件的 reRender属性中指定它们的组件 ID。另一种方法是在 a4j:outputPanel标记中包裹页面中的区域并借助该标记的 ajaxRendered属性将这些区域标识为 “由 Ajax 呈现的”。清单 10 显示了查看模板中能输出所选球场细节的那个区域,方法是采用输出 - 面板方式:

清单 10. 用 Ajax 呈现的细节面板
 <a4j:outputPanel id="detailPanel" ajaxRendered="true"> 
  <h:panelGroup id="detail" rendered="#{course.id gt 0}"> 
  <h3>Course Detail</h3> 
  <!-- table excluded for brevity --> 
  </h:panelGroup> 
 </a4j:outputPanel>

配置 Ajax4jsf

Ajax4jsf 的定制视图处理程序

JSF 规范中的视图处理程序指派机制极为有限,而且不能很好地处理视图处理程序链。Ajax4jsf 使用了一种定制视图处理程序来修饰实际的视图处理程序作为工作区。

在运行所更新的代码之前,需要配置您应用程序中的 Ajax4jsf。首先,向应用程序类路径添加 jboss-ajax4jsf.jar 库及其依赖项 oscache.jar。其次,向 web.xml 添加 Ajax4jsf 过滤器。该过滤器的定义如清单 11 所示。请注意 Ajax4jsf 过滤器必须是在 web.xml 文件内定义的第一个过滤器。与 Seam 的 Resource Servlet 非常类似,这个过滤器处理由 Ajax4jsfs 组件发起的远端调用。与 Seam 的远端调用不同,JSF 生命周期在 Ajax4jsf 异步请求过程中 活动的,因而需要 servlet 过滤器而非常规的 servlet。此外,还需要将 Facelets 视图处理程序的配置从 faces-config.xml 文件移到 web.xml,在后者中,该配置在 servlet 上下文参数中定义。

清单 11. 配置 Ajax4jsf
<context-param> 
    <param-name>org.ajax4jsf.VIEW_HANDLERS</param-name> 
    <param-value>org.jboss.seam.ui.facelet.SeamFaceletViewHandler</param-value> 
 </context-param> 

 <filter> 
    <filter-name>Ajax4jsf Filter</filter-name> 
    <filter-class>org.ajax4jsf.Filter</filter-class> 
 </filter> 

 <filter-mapping> 
    <filter-name>Ajax4jsf Filter</filter-name> 
    <url-pattern>*.action</url-pattern> 
 </filter-mapping>

一旦将清单 11 中的库和安装好的 markup 包括到 web.xml 文件,就可以使用 Ajax4jsf 了!但需要确保将 Ajax4jsf 标记库定义 xmlns:a4j="https://ajax4jsf.dev.java.net/ajax"添加到将要使用 Ajax4jsf 标记的视图模板。当然,还有使用这个库可以实现的许多其他功能,本文未能详述。有关使用 Ajax4jsf 的更多信息和示例,请参看 参考资料


再返回来!

至此,Open 18-Google Maps mashup 就已完全只读。围绕 Ajax 有很多令人兴奋的事情,其核心就是能在不重载页面的情况下更改后端数据。我们不妨为我们的 Google map 添加一些有趣的东西以使其能发送回一些数据。为了演示,假设即使 Google 能将邮寄地址解析为经度和纬度,用户还是想要为 pin 指定一个定制的位置。更准确地说,是用户想让 pin 指向俱乐部会所或给定球场的第一个发球台。不幸的是,Google Map API 让这个对 Open 18 应用程序的最后增强不大容易实现。

首先,让标记能被拖动。要开启此特性,只需在被实例化时向 GMarker添加 draggable标志。接下来,注册一个 JavaScript 函数来侦听当用户结束拖动标记时所激发的 “拖动结束” 事件。在这个回调函数中,为了保存新的点,在 courseAction存根上执行了一个新方法 setCoursePoint()

清单 12 给出了修改后的 createAndPlaceMarker函数。考虑到放置这个标记时在 Course条目上的那个定制点,addCourseMarker()函数也进行了修改。

清单 12. 启用了标记拖动功能的更新后的 JavaScript
function addCourseMarker(course) { 
    var address = course.getAddress(); 
    if (course.getPoint() != null) { 
        var point = course.getPoint(); 
        var latlng = new GLatLng(point.getLatitude(), point.getLongitude()); 
        createAndPlaceMarker(course, latlng); 
    } 
    else { 
        var addressAsString = [ 
            address.getStreet(), 
            address.getCity(), 
            address.getState(), 
            address.getPostalCode() 
        ].join(" "); 
        geocoder.getLatLng(addressAsString, function(latlng) { 
            createAndPlaceMarker(course, latlng); 
        }); 
    } 
 } 

 function createAndPlaceMarker(course, latlng) { 
    // skip adding marker if no address is found 
    if (!latlng) return; 
    var marker = new GMarker(latlng, { draggable: true }); 
    // hide the course directly on the marker 
    marker.courseBean = course; 
    markers[course.getId()] = marker; 
    gmap.addOverlay(marker); 

    function showDetailBalloon() { 
        showCourseInfoBalloon(this); 
    } 

    function assignPoint() { 
        var point = Seam.Remoting.createType("com.ibm.dw.open18.Point"); 
        point.setLatitude(this.getPoint().lat()); 
        point.setLongitude(this.getPoint().lng()); 
		 var courseActionStub = Seam.Component.getInstance("courseAction"); 
		 courseActionStub.setCoursePoint(this.courseBean.getId(), point); 
    } 

    GEvent.addListener(marker, 'click', showDetailBalloon); 
    GEvent.addListener(marker, 'dragstart', closeInfoBalloon); 
    GEvent.addListener(marker, 'dragend', assignPoint); 
 }

虽然还没有大功告成,但已经离之不远了!还需要向服务器端组件添加一个方法以保存这个 GLatLng点。再坚持一会,因为您需要重新访问 Open 18 应用程序的 Spring 集成后才能实现 mashup 的这个最后的特性。(参看 “无缝集成 JSF,第 2 部分 : 借助 Seam 进行对话” 来重新访问 Spring 集成的详细信息。)


Spring 集成回顾

正如之前所讨论的,我最初将 Spring 容器集成到 Seam 中所采用的变量解析器方法有其自身的局限。坦白地讲,该方法己经发展到了尽头,现在该是和它说再见的时候了。大多数的 Seam 特性均涉及到了 JSF,而 Seam 的某些属性却工作于 JSF 生命周期之外。要获得与 Spring 的真正集成,需要比定制变量解析器更好的解决方案。所幸的是,Seam 的开发人员已经开始着手解决这种需求,并添加了针对 Spring 的 Seam 扩展。Spring 集成包利用了 Spring 2.0 中的新特性来创建基于模式的扩展点。这些扩展在 bean 定义文件和名称空间处理程序中启用了定制 XML 名称空间,在 Spring 容器的启动过程中对这些标记进行操作。

用于 Spring 的 Seam 名称空间处理程序(您可能需要大声读几遍才能明白其中的含义)有几种与 Spring 容器进行交互的方式。要去除定制的变量解析器,需要将 Spring bean 作为 Seam 组件公开。seam:component标记可专门用于此目的。将此标记放置于 Spring bean 声明之内会通知 Seam Spring bean 应该被代理或包裹成一个 Seam 组件。在这一点上,Seam 双射机制会将这个 bean 视为仿佛它已经被 @Name注释,只有现在,您才能不需要 FacesContext 来填补 Seam 和 Spring 间的差距!

配置 Seam-Spring 集成异常容易。第一步都完全无需涉及任何代码更改!所有您需要做的只是确保使用了 Spring 2.0 和 Seam 1.2.0 或后续发布。(自本系列的第一篇文章发表以来,Seam 已经有了迅速发展,所以应该准备好进行 一次升级。)IOC 集成打包成一个单独的 JAR,jboss-seam-ioc.jar,所以除了已经提到的 JAR 之外,应该将它也包含到应用程序的类路径中。

第二个步骤也不会涉及到 Seam 配置。这次,您看到的是 Spring 配置。首先,向其中定义了 bean 的 Spring 配置 XML 添加 Seam XML 模式声明。这个文件的标准名称是 applicationContext.xml,但示例应用程序使用了一个更为合适的名字 spring-beans.xml。接下来,在任何您想要公开给 JSF 的 Spring bean 的定义内添加 seam:component标记。在 Open 18 应用程序中,将此标记嵌套在 courseManagerbean 定义内。要查看整个 bean 列表,请参考本文 示例应用程序spring-beans.xml文件。

有了改进后的 Spring 集成,就无需在 courseManager属性顶上的 @In注释中指定值绑定了,亦无需 required=false属性。取而代之的是 create=true属性以便 Seam 能够知道从 Spring 容器中去获取 bean 并将其装饰成 Seam 组件。清单 13 中的代码显示了 CourseAction类中支持球场的地理空间点更新的相关部分:

清单 13. CourseAction 上的新的 setPoint 方法
@WebRemote 
 public void setCoursePoint(Long id, Point point) { 
    System.out.println("Saving new point: " + point + " for course id: " + id); 
    Course course = courseManager.get(id); 
    course.setPoint(point); 
    courseManager.save(course); 
 }

现在您尽可以放松一下。没错,您已经成功地使自己的应用程序 “Ajax 化” 了!


结束语

在本文中,您看到 Open 18 应用程序从一个异常简单的 CRUD 实现扩展成了一个十分复杂的 mashup 并集成了 Ajax 风格的页面以呈现 Google Maps。您也看到了如何利用 Seam Remoting API 和 Ajax4jsf 并学习了集成 Seam 和 Spring 框架的一种更为复杂的方法。

Seam Remoting 和 Ajax4jsf 通过扩展 JFS 组件模型使其包括 Ajax 通信极大地补充了该模型。本文中给出的代码均无需直接与 XMLHttpRequestJavaScript 对象交互。相反,所有工作都藏于较高层 API 背后,让您可以直接查询和处理服务器端组件,也可以调用动作和更新给定页面的某些部分。得益于 Ajax 的魔力,所有的工作均可异步发生。

在本系列的这三篇文章中,您学习了大量有关结合了 Seam 的 JSF 开发的内容,但我还未能尽述 Seam 的所有特性!就让 无缝集成 JSF系列中的讨论和示例作为您学习如何将 Seam 用在 JSF 项目中以使其更容易创建的很好的开始点吧。越多地接触 Seam,您就越能发现它的可爱之处。


下载

描述名字大小
Open 18 示例应用程序 - 阶段 21j-seam3.zip277K

注意:

  1. 样例应用程序被组织成 Maven 2 项目。在执行构建时,会根据需要取出所有依赖项。该应用程序使用若干 Appfuse 项目库来实现服务层和 DAO 层。

参考资料

学习

获得产品和技术

  • 下载 JBoss Seam:获得完整的发行版,包括附带的例子应用程序。
  • Ajax4jsf:将 Ajax 功能添加给现有的 JSF 组件。
  • RichFaces:构建在 Ajax4jsf 之上来为 JSF Web 应用程序添加开箱即用的丰富性。
  • Firebug:该 Firefox 插件是一个在处理 Ajax 应用程序时不可或缺的工具。
  • 下载 Maven 2:源码示例中使用的软件工程管理和综合工具。Maven 会自动在构建过程中下载依赖项。
  • Facelets:用于 Seam 应用程序的首选 JSF 视图处理程序。
  • Appfuse:实际上向您提供了非常全面的基于 Maven 2 的项目框架,可用于根据它构建自己的应用程序。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development, Open source
ArticleID=236340
ArticleTitle=Seam - 无缝集成 JSF,第 3 部分: 用于 JSF 的 Ajax
publish-date=06252007