标签:
发现 Seam 对 JSF 生命周期特有的增强
JavaServer Faces (JSF) 是用于 Java? Web 应用程序的第一个标准化的用户界面框架。而 Seam 是一个扩展 JSF 的强大的应用程序框架。在这个由三部分组成的新系列中的第一篇文章中,发现这两种框架之间的互补性。Dan Allen 介绍了 Seam 对 JSF 生命周期的增强,包括上下文状态管理、 RESTful URL、Ajax remoting、适当的异常处理和约定优于配置。
--
JSF 正开始凭借其 Java Web 标准的地位主导 Java Web 应用程序市场。随着更多的开发人员受托使用 JSF 作为基础来架构应用程序,他们发现 JSF 的核心规范中清楚地说明: JSF 不是为成为一个完整的 Web 应用程序框架而设计的。相反,它提供一个健壮的、事件驱动的 API 和 UI 组件库,用于构建更复杂的应用程序框架。
我在寻找用于弥补 JSF 的组件驱动架构的扩展时,发现 Shale 和 Struts 2 都有不足之处。我排除了 Struts 2,因为它将 JSF 看作是面向更大范围的设计。而 Shale 似乎更靠近一些,它基本上是基于 JSF,但是 对此我持保留意见。相反,JBoss Seam 是一个全面的应用程序框架,它构建在 JSF 的基础上,但是并没有损害它的核心目标。
这个由三部分组成的系列将介绍 Seam 应用程序框架,演示它的优点,并希望使您相信它与 JSF 是开发 Java 企业应用程序的极好的组合。 在阅读本系列之前,如果您想下载 Seam,那么请阅读 参考资料 一节。
--
刚刚阅读到关于 JBoss Seam 的文章(见 参考资料)的第一页,我就知道 Seam 正是我要找的项目。Seam 的开发人员,尤其是 Gavin King,在经过足够多的、实际的开发之后,知道一个 Web 应用程序框架必须从一开始就攻破难题,包括上下文状态管理、RESTful 和用户友好的 URL、Ajax remoting、适当的异常处理和约定优于配置。令 Java 开发人员欣喜的是,Seam 可以满足所有这些需求,甚至可以满足更多需求。如果您正使用 JSF,并且还没听说过 Seam,那么我强烈建议您看看 Seam 的参考文档(见 参考资料)。Seam 附带的手册就是最好的资料!
尽管 Seam 显然非常适合作为 JSF 的补充,但是在激烈的竞争环境中,它遭到了一定程度的轻视。当今市场中充斥着各种各样的 Web 应用程序框架 —— 包括 Shale 和 Struts 2,新来者往往不受重视,Seam 还没有在主流行列站稳脚跟。 Seam 没有很快流行的另一个原因是关于这种框架的某些流言使 Java 开发人员没能认识到它的直接优点。
我要粉碎的一个流言是:Seam 只有和 EJB 3 一起使用时才有用,或者说在使用 Seam 开发应用程序时需要一个 EJB3 容器。实际上,Seam 的文档清楚地驳斥了这种误解:"Seam 并不要求组件是 EJB,甚至在没有兼容 EJB 3.0 的容器时也能使用。" 如果说只有在使用 EJB 3 的同时才能使用 Seam,那么无异于说只有在使用 Hibernate 的同时才能使用 Spring。虽然这两对都有很强的互补性,但是每一对的两者之间都不是相互依赖的。
正如我将要解释的那样,Seam 通过一些有价值的 hook 和组件管理进程 扩展默认 JSF 生命周期。还可以完全独立于 EJB3 使用 Seam。但是要记住,和 EJB3 一样,Seam 依赖于 JDK 5 注释元数据进行组件声明,因此使用 Seam 时,还需要同时使用兼容 Java 5 的 JVM。图 1 显示了一个 Seam POJO 实现的应用程序堆栈:
实际上,即使完全不引用 EJB 3 jar 或描述符文件,也可以使用 Seam 的很多功能。当和 POJO 一起使用 Seam 时,该框架保留对组件实例化的完全控制,并且不要求任何专门的配置。Seam 负责大多数 Java 5 注释处理,而不需要依赖于 EJB 3 中的任何机制。的确 依赖于 EJB3 容器的一组有限的注释则是专用于那个环境的。在某些情况下,将 Seam 集成到一个没有 EJB 3 耦合的 IT 投资中可以获得更好的成本效益。如何使用 Seam 视个人偏好而定。
如今有那么多种 Java 框架,每天只有有限的那么多小时,显然,如果 Seam 难于集成的话,它就无立足之地。幸运的是,将 Seam 添加到项目中很简单。因为 JSF 生命周期仍然是 Seam 应用程序的中心部分,所以不需要经历一个再训练时期。只需添加 4 个 jar 文件,注册一个 servlet 监听器和一个 JSF phase 监听器,最后再加上一个空白的 Java 属性文件。完成这些设置后,就可以一次性地将本地 JSF 应用程序转移到 Seam 管理的 bean 上。
要开始使用 Seam,首先需要将所需的 jar 文件添加到项目中。如果您当前不是使用 Hibernate,或者还没有升级到最新的版本,那么在设置时需要执行一个额外的步骤。这里需要包含来自 Hibernate 3.2 distribution 的 jar,以及它的众多的依赖项。Seam 还使用 Hibernate 注释用于数据验证,所以除了主 Hibernate jar 之外,还必须包括那个扩展 jar。需要的 Seam 发行版中的库有 jboss-seam.jar 和 jboss-seam-ui.jar,以及两个支持库:Javassist(用于 Java 的加载时反射系统)和 Java Persistence API。图 2 中的项目树说明了一个 Seam 项目中的 jar 集合。该图中显示的大多数附加库支持 JSF 的 MyFaces 实现。
接下来的步骤是在 web.xml 文件中安装 servlet 监听器类。该监听器在部署应用程序时初始化 Seam。
<listener> <listener-class>org.jboss.seam.servlet.SeamListener</listener-class> </listener>
接下来,将 JSF phase 监听器添加到 faces-config.xml 文件中,如清单 2 所示。该监听器将 Seam 集成到标准 JSF 生命周期中。(图 3 大致描绘了集成到这个生命周期中的 Seam 增强。)
<lifecycle> <phase-listener>org.jboss.seam.jsf.SeamPhaseListener</phase-listener> </lifecycle>
最后,将一个空的 seam.properties 文件放在 类路径的根下,以便指示 Seam 进行加载,如清单 3 所示。这个空白文件被用作一个 JVM 类加载器优化,使 Seam 在类路径下更小的区域内搜索组件,从而大大减少加载时间。
# The mere presence of this file triggers Seam to load # It can also be used to tune parameters on configurable Seam components
当然,在这种最小设置中,Seam 的很多特性是不可用的。以上说明只是为了演示 Seam 很少涉足入门级使用。例如,Seam 包括一个 servlet 过滤器,该过滤器扩展 JSF 生命周期以外的 Seam 特性。 servlet 过滤器的用法包括与非 JSF 请求集成,通过重定向传播 conversation,以及管理文件上传。请参阅 参考资料,看看 Seam 参考文档,其中讨论了用于控制附加功能的配置文件 —— 特别是 EJB3 集成。
与典型的 JSF 配置过程相比,使用 Seam 开发受管 bean 非常容易。为了将 bean 暴露到 JSF 生命周期中,只需在类定义的上面添加一个简单的注释 @Name
。然后,Seam
会负责控制组件的可见性和生命周期。最妙的是,不需要在 faces-config.xml 文件中定义这个 bean。
清单 4 显示了 @Name
注释以及 @DataModel
、@DataModelSelection
、@In
、@Out
和 @Factory
。这些注释使变量能够在视图模板和
Seam 组件之间双向流动。
在 Seam 用语中,这个动作被称作双射(bijection,即 bidirectional injection 的简称)。当注出(outject)属性数据时,视图可以通过名称找到它。在 postback 或者组件初始化时,数据被注入(inject)到一个组件中。后者是著名的控制反转(inversion of control,IOC)模式的一种实现,可用于连接委托对象。传统 IOC 与 Seam 的双射之间的主要不同点在于,双射使长期作用域中的组件可以引用短期作用域中的组件。可以进行这种连接是因为 Seam 在调用组件时(而不是启动容器时)解析依赖项。双射是有状态组件开发的基础。
显然,清单 4 中的 POJO bean 只是简单地演示了 Seam 的用法。随着本系列讨论的继续,我将探索另外的方法来实现 Seam。
@Name("addressManager") public class AddressManagerBean { @DataModel private List<Address> addresses; @DataModelSelection @Out( required = false ) private Address selectedAddress; @Factory( value = "addresses" ) public void loadAddress() { // logic to load addresses into this.addresses } public String showDetail() { // no work needs to be done to prepare the selected address return "/address.jspx"; } public String list() { return "/addresses.jspx"; } }
为了使用由一个已有的 Spring 容器管理的服务层对象中的投资,需要将所有处理相关业务逻辑的 Spring bean 注入到 Seam 组件中。首先需要确保已经配置了 Spring-JSF 集成,它由 Spring 框架附带的一个定制变量解析器进行处理(见 参考资料)。有了这座桥梁,Spring
与 Seam 的集成就很简单,只需使用 @In
Java
5 注释和一个值绑定表达式,以表明 Seam 组件的哪些属性应该接收一个 Spring bean 的注入,如清单 5 所示。(将来版本的 Seam 将包括用于 Spring 的一个定制的名称空间,以满足值绑定表达式的需要。)
@Name("addressManager") public class AddressManagerBean { @In("#{addressService}") private AddressService addressService; }
这个例子设置支持使用以轻量级容器(这里就是 Spring)配置的无状态服务和数据访问(DAO)层。因为不需要 EJB3,所以部署的目标可以是任何基本的 servlet 容器。
现在,您对 Seam-JSF 实现有了一个初步的印象,接下来我将更深入地探讨我在使用 JSF 时遇到的挑战,以及 Seam 如何缓解这些挑战。
为了充分理解 Seam 为 JSF 带来了什么,就需要理解 JSF 与其他流行的基于 Web 的编程方法有何不同。JSF 是实现传统的 Model-View-Controller (MVC) 架构的一种 Web 框架。不同之处在于,它采用该模式的一种特别丰富的实现。与 Model 2 或者 Struts、WebWork 和 Spring MVC 之类的框架中使用的 “push-MVC” 方法相比,JSF 中的 MVC 实现更接近于传统的 GUI 应用程序。前面那些框架被归类为基于动作的(action-based),而 JSF 则属于基于组件模型 的新的框架家族中的一员。
如果将基于动作的框架想象为使用 “push” 模型,而将组件框架想象为使用 “pull” 模型,那么这种区别就很容易理解了。组件框架中的控制器不是预先处理页面请求(在基于动作的框架中控制器就是这么做的),而是在请求生命周期中作出让步,在视图中调用数据提供方法。此外,页面上的元素,即组件被绑定到事件,这些事件可以触发服务器端对象(激活后)的方法调用,从而导致重新显示相同的视图,或者转换到另一个页面。因此,组件框架也被归类为事件驱动的。组件框架抽象出用于事件通信的底层请求-响应协议。
事件驱动方法的优点是可以减少单个方法在呈现视图时需要预先做的工作。在组件框架中,UI 事件或解析的值绑定表达式直接导致方法调用。
一个应用程序即使只达到中度成熟,它通常也需要在任何给定页面上使用很多不相关的活动。如果将对所有这些信息的管理全部放入一个动作或者一个动作链中,那么势必给维护带来极大的困扰。因此,开发人员常常发现他们的代码偏离了面向对象模型的轨道,反而陷入了过程编程模型的泥潭。相反,组件框架将这种工作隔离出来,更自然地加强了对象的角色和责任。
对于 JSF 和组件框架的基础已经介绍得差不多了。实际上 —— 很多 Java 开发人员最近发现 —— 转移到 JSF 并非总是一帆风顺。采用组件模型会带来一些全新的问题,首要的一个问题是您通常需要试着使应用程序符合基于动作的 Web。很多时候,JSF 需要具有像基于动作的框架那样的行为,但是在标准 JSF 中这是不可行的,至少不为每个请求使用 phase 监听器就不行。
JSF 的其他主要缺点还包括对 HTTP 会话的依赖过重(尤其是在一序列的页面之间传播数据时),简陋的异常处理,缺少书签支持,以及太多的 XML 配置。 通过与 JSF 自然地集成,同时加入 JSF 规范委员会放弃的或者忽略掉的新功能,Seam 解决了很多这样的问题。Seam 的框架鼓励使用紧凑的、易读的、可重用的代码,并且避免了所有为解决上述问题而常常加入的 “粘连(glue)” 逻辑。图 3 涵盖了 JSF 生命周期中用于简化应用程序代码的大多数 Seam 扩展点:
让我们来考虑其中一些增强,因为它们适用于 JSF 开发中一些常见的挑战。
Seam 演示了 Java 5 注释的一个非常实用的用法。Seam 的部署扫描程序检查所有包含 seam.properties 文件的归档文件,并为所有标有@Name
注释的类创建一个
Seam 组件。由于 Java 语言缺乏用于在代码级添加元数据的一种公共语法,因此需要设计很多 XML 配置。当 Java 5 规范中加入注释后,就获得了一个更好的解决方案。由于大多数 backing bean 是为了在特定应用程序中使用而开发的,因此没有理由将这些 bean 的配置 “抽象” 到类本身以外的任何文件中。附带的好处是,您可以少处理一个文件。Seam 提供了一组完整的注释来帮助将 bean 集成到 JSF 生命周期中。清单
4 显示了其中一些。
在不使用组件框架的情况下,另一个必须解决的熟悉的问题是预先处理每个请求,就像在基于动作的框架中那样。受此影响的用例是 RESTful URL、书签支持、通过 URL 模式获得的安全性以及页面流验证等。这也是学习使用 JSF 的开发人员容易感到困惑的主要原因之一。有些 JSF 供应商通过用开发人员工具提供 onPageLoad 功能来绕过这个问题(见 参考资料),但这不是核心规范的一部分。
当用户直接从书签(比如)请求一个商品详细信息屏幕时,通常会发生什么事情呢?由于 JSF 控制器采取被动方式,当页面开始呈现时,即使明显没有目标数据,也不能将用户重新带到逻辑流的开始处。相反,这种情况下只能显示一个空页面,其中只有一些空值或其他可能存在的假信号。
首先,您可能会本能地想要在页面的主 backing bean 上实现一个 “prerender” 方法。然而,在组件框架中,backing bean 与页面之间的关系并不一定都是一对一的。每个页面可能依赖于多个 backing bean,每个那样的 bean 也可能在多个不同的页面上使用。必须用某种方式将一个视图 ID(例如 /user/detail.jspx)与一个或多个方法关联起来,当选择呈现相应的视图模板时就调用这个(些)方法。您可以使用 phase-listener 方法,但是这仍然需要定制的逻辑来确定对于当前视图和阶段是否应该执行该功能。这种解决方案不但会导致很多冗余逻辑,而且会将视图 ID(很可能是应用程序中最不确定的部分)硬编码到编译后的 Java 代码中。
Seam 的页面动作可以帮助您预先拦截呈现的假信号。页面动作是使用方法绑定指定的,方法绑定在进入页面时、Render Response 阶段之前执行。对于 /WEB-INF/pages.xml 配置文件中一个给定的视图 ID,可以配置任意数量的方法绑定。(或者,可以通过将它们放在视图模板邻近的一个文件中,复制它的名称,但是将文件扩展名换为 *.page.xml,从而分解每个页面的定义)。对于页面动作,XML 是有必要的,因为视图 ID 非常容易变化。就像 JSF 通过 Apply Request Values 阶段的值绑定将 post 数据映射到模型对象一样, Seam 可以通过执行页面动作之前的值绑定将任意请求参数映射到模型对象。这些请求参数注入的配置嵌套在页面动作 XML 声明中。如果页面动作方法调用返回一个非空字符串值,则 Seam 将其当作一个导航事件。因此,不必迁移到一个完整的基于动作的框架中,仍然可以比得上最特别的特性。Seam 包括很多内置的页面动作,它们通常跨应用程序使用。其中包括用于验证 conversation 是否建立的一个动作;可以启动、嵌套和结束 conversation 的动作;处理预期异常的动作;以及确保适当的凭证的动作。
页面动作是启用对 JSF 的书签支持的关键。Seam 的创立者允许在进入页面时请求参数 actionMethod
触发一个方法调用,从而利用了这一特性。更妙的是,您不需要做任何额外的工作就能为书签创建链接。
Seam 提供了两个组件标记:s:link
和 s:button
,用以处理细节。这两个标记分别对应于
JSF 中的 h:commandLink
和 h:commandButton
。不同之处在于,Seam
组件标记组装的链接使用一个 HTTP
GET
操作发出请求,而不是使用 JSF 的 HTTP
POST
表单提交模型表示。因此,Seam 创建的链接对书签更 “友好”,对于开发人员来说更方便。
您可能还注意到,当使用页面动作时,地址栏中的 URL 对应于正在显示的页面,而不总是背后的一个页面。(后一种情况之所以会发生,是因为 JSF 将表单配置为 post 回生成它们的 URL。地址栏没有更新,以反映执行动作后的新视图,因为 JSF 通过一个服务器端重定向使之前进。)如果您想演示页面动作的灵活性,那么可以使用它们来创建 RESTful URL(例如 /faces/product/show/10)。为此,将页面动作方法映射到视图 ID“/product/show/*”,其中 /faces 前缀是 JSF servlet 映射部分。然后,该页面动作方法利用请求 URL,以判断数据类型和数据标识符,加载数据,然后导航到适当的模板。这个例子很明显地演示了 JSF 请求 URL 与视图模板之间并不一定存在一对一的关系。
JSF 最大的一个失败是没有在用户触发的动作或动作监听器方法以外的其他地方提供可靠的机会来为视图准备数据。将逻辑放在一个动作方法中并不能保证该逻辑在视图呈现之前得到执行,因为页面视图并不总是在用户触发的事件之后。
例如,当一个 JSF 应用程序的 URL 第一次被请求时,会发生什么情况?如果需要在该页面上显示从服务层获得的一组数据,那么在 JSF 生命周期中始终没有好的机会来取数据。您可能会认为,可以将逻辑放在映射到视图中值绑定表达式的 backing bean 的 getter 方法中。但是,每当 JSF 解析那个表达式时,就会触发另一个调用,新的调用又会访问服务层。即使页面上只有少数几个组件,getter 方法也可能被推后数次执行。显然,就性能而言这不是最优的。即使通过使用受管 bean 上的私有属性维护状态,每当面对那样的情况时,仍然必须增加额外的管道。一种解决方案是使用 Seam 的页面动作。但是由于这种任务是如此常见,Seam 提供了一个更加容易的解决方案。
Seam 引入了工厂数据提供者(factory data provider)的概念,工厂数据提供者由 @Factory
Java
5 注释指定。虽然有两种方法配置工厂,但是最终结果是同样的数据只需在第一次被请求时准备一次。 Seam 确保随后对相同数据的请求将直接返回之前创建的结果集,而不必再一次触发对查找方法的调用。通过与 conversation 相结合,工厂数据提供者成为实现数据短期缓存的非常强大的特性,否则,取这些数据的代价可能较高。在 JSF 不负责减少它解析一个值绑定表达式的次数的情况下,Seam 的工厂特性常常变得非常方便。
关于 JSF 很容易引起困惑的一个地方是它的状态管理功能。JSF 规范解释了在接收一个动作之后页面是如何 “恢复(restored)” 的,在此期间时间事件要进行排队,选择要注册。仔细研究规范中的用词可以发现,虽然在 postback 上恢复了组件树,但是那些组件使用的 backing bean 数据并没有被恢复。组件只是以字符串文字的形式存储值绑定(使用 #{value}
语法的
EL 表达式),只在运行时解析底层数据。这意味着如果一个值是短期作用域存储的,例如页面或请求作用域,那么当 JSF 生命周期到达 Restore View 阶段时,这个值将消失。
不将值绑定数据存储在组件树中的一个最值得注意的不利方面是虚幻事件效果(见 参考资料),这是由 UIData
家族中的临时父组件导致的。如果一个值绑定表达式引用的模型数据不再可用,或者在组件树被恢复之前发生了更改,那么组件树的一些部分将被撤销。如果在这些被撤销的分支中,有一个组件中触发了一个事件,那么它将不能被发现,而且这种事件丢失情况是难于觉察的。(只是队列开发人员可能会惊呼
“为什么我的动作没有被调用?”)
虽然丢失的事件看上去像是异常状况,但并不会导致 JSF 生命周期中出现红色标志。因为这些组件依赖底层数据,以保持稳定和适当地被恢复,所以 JSF 难于知道丢失了什么。
不幸的是,JSF 规范天真地引导开发人员将大多数 backing bean 放入 conversation 作用域中 —— 甚至可以在 “方便的” 作用域内调用它。然后,服务器管理员则必须处理由此导致的 “内存溢出” 错误,集群环境中的服务器相似性,以及服务器重启时的串行化异常。MyFaces Tomahawk 项目通过 t:saveState
标记的形式提供了对虚幻事件的一个解决方案。MyFaces
标记允许将数据(包括整个 backing bean)存储为组件树的一部分,而仅仅是值绑定。然而,这种解决方案有些简陋,很像使用隐藏的表单字段在请求之间传递值。 它还造成视图与控制器之间紧密耦合。Seam 的创立者意识到,Java Servlet 规范中三个内置的上下文(请求、会话和应用程序)不足以构成数据驱动的 Web 应用程序的全部作用域。在 Seam 中,他们引入了 conversation 作用域,这是一个有状态作用域,由页面流的起止点界定。
“Seam 强调使用有状态组件。” Seam 参考文档中的这句话体现了 Seam 的核心思想。很长一段时间内,关于 Web 应用程序的看法是,它们是无状态的 —— 这种思想一定程度上要归因于 HTTP 协议的无状态性质。大多数框架为了迎合这一点,在结束页面呈现之前提供 one-shot-processing。这种方法导致很大的阻力,因为任何大的应用程序都需要长时间运行的 conversation 来满足某些用例。需要有状态上下文的应用程序的例子有很多,例如存储检查过程、产品定制、多页表单向导和很多其他基于线形交互的应用程序。虽然其中有些例子可以通过使用 URL 参数(aka RESTful URL)和隐藏字段在页面之间迁移数据,但是这样做对于开发人员来说有些繁杂。而且,如今这种做法已经过时了。因为大多数 Web 框架仍然在无状态模型下操作,所以您常常发现自己走出了这种框架,而 “开辟” 出定制解决方案。
JSF 大量依赖于 HTTP 会话,试图引入有状态上下文。实际上,当和会话作用域的 backing bean 一起使用时,JSF 组件的行为要好得多。如果不小心设计,过度使用 HTTP 会话会导致严重的内存泄漏、性能瓶颈和安全问题。此外,在多标签浏览器环境中,使用 HTTP 会话可能导致非常奇怪的行为,破坏用户神圣的 Back 按钮。值得注意的是,JSF 只是与您互作让步:它是一个有状态 UI,处理保存和恢复组件树的所有细节,但是它在保存和恢复数据方面没有提供帮助。因此,JSF 带来有状态 UI,而您则带来有状态数据。不幸的是,需要由您来负责确保它们是相符的。
在 Seam 之前,使用有状态数据的惟一方便的方式是依赖于 HTTP 会话。Seam 纠正了这个问题,它通过建立一个全新的 conversation 作用域,完善了 JSF 的状态管理。随着将 Seam 添加到 JSF 生命周期中,conversation 上下文与一个浏览器窗口(或标签页)联系在一起,这个浏览器窗口(或标签页)由随每个请求提交的一个标志来标识。conversation 作用域使用 HTTP 会话的一个单独的区段在页面之间迁移数据。记住,Seam 使用 HTTP 会话用于 conversation 持久性这一点是完全透明的。 Seam 并不是不负责任地将组件推卸到 HTTP 会话中,使其茫然地呆在那里。相反,Seam 小心地管理那个区段的会话数据的生命周期,并且当 conversation 终止时,自动清除它们。Seam 聪明地使用双射来允许以一种新的说明性方式使数据流入和流出一个 “Web conversation” 的每个页面。 Seam 的 conversation 作用域同时还克服了 HTTP 会话的限制,帮助开发人员放弃使用 HTTP 会话。
Seam 的创立者曾说过:“在异常处理方面,JSF 非常有限”。这一点显然毫无争议。 JSF 规范完全忽视异常管理,将责任完全放在 servlet 容器上。允许 servlet 容器处理异常的问题在于,这严重限制了错误页面上显示的内容,并且禁止了事务回滚。由于错误页面是在请求分发器转发之后显示的,FacesContext
不再在作用域中,因此这时执行业务逻辑就太迟了。您的最后一线希望是使用
Servlet API,并抓住javax.servlet.error.*
请求属性,以搜索能表明出错处的信息。
这一点上,Seam 再次扮演救世主,它提供了优雅的、说明性方式的异常处理。异常管理可以通过注释指定,或者在配置文件中指定。可以将注释 @HttpError
、@Redirect
和 @ApplicationException
放在异常类的上面,表明当异常被抛出时应该采取的动作。对于那些不能修改的异常类,可以使用
XML 配置选项。Seam 中的异常管理器负责发送 HTTP 状态码、执行重定向、促使页面呈现、终止 conversation 和定制出现异常时的用户消息。由于在开始呈现响应之后,JSF 不能改变动作的过程,一些固有的限制规定了何时才能处理这些异常。通过适当使用其他 Seam 特性,例如页面动作,可以确保大多数异常情况在呈现响应之前得到解决。
由于最后发行的 JSF 规范几乎与 Ajax 重合,JSF 框架在异步 JavaScript 和局部页面呈现(partial page rendering)方面帮助不大。在某些时候,甚至这两种类型的编程甚至不一致。 最终的解决办法是建议使用 JSF PhaseListener
或组件 Renderer
来处理局部页面更新。即使如此,这一点仍然很明显:JSF
使得 Ajax 比过去更难于采用。有些项目,例如 ICEfaces,甚至用一个更好的、专为页面-服务器通信设计的技术(即 direct-to-DOM rendering)来替代 JSF 生命周期。
Seam 为 JavaScript remoting(常常记在 Ajax 名下的一种技术)提供了一种独特的方式,该方式与 Direct Web Remoting (DWR) 库的方式大致相似。Seam 通过允许 JavaScript 直接调用服务器组件上的方法,将客户机与服务器连在一起。Seam remoting 比 DWR 更强大,因为它可以访问丰富的上下文组件模型,而不仅仅是一个孤立的端点。这种交互构建在 JSF 的事件驱动设计的基础上,所以它可以更严格地遵从 Swing 范例。最妙的是,提供这个功能的同时并没有增加开发方面的负担。只需在组件的方法上加一个简单的注释 @WebRemote
,JavaScript
就可以访问该方法。当服务器组件的状态被修改之后,Ajax4JSF 组件库就可以处理局部呈现。简言之:Seam remoting 库使 JSF 可以实现它的创立者一向期望的交互设计。
根据您目前在无缝集成 JSF 系列 中了解到的内容,可以毫不牵强地说在使用 JSF 的开发中不使用 Seam 是反常的。作为进一步的证明,只需看看 JSR 299, Web Beans 的投票结果(见 参考资料)。显然,在不久的将来,Seam 会成为一个官方规范,Java EE 栈最终将提供 “显著简化的基于 Web 的应用程序编程模型”。这对 JSF 开发人员和 Seam 来说是一个好消息。但是,即使没有声明要成为一个 Java 标准,Seam 也是 JSF 的一个有价值的补充。
Seam 只需很少的设置就可以开始用于 JSF —— 而正是这一点小小的付出,就能解决 JSF 开发中的一些最麻烦的难题。回报胜于付出 —— 这里讨论的 Seam 的优点还只是一个开始。
使用 Seam 构建有状态的 CRUD 应用程序
借助 Seam 开发有状态的 CRUD 应用程序是件轻而易举的事情。在 无缝 JSF 系列文章的第二篇中,Dan Allen 向您展示如何使用 Java?Server Faces (JSF) 和 Seam 为基于 Web 的高尔夫课程目录开发创建、读取、更新和删除用例。在此过程中,他突出强调了 Seam 对 JSF 生命周期的两项增强功能 —— 也就是 conversation 作用域和通过自定义 Java 5 注释进行配置 —— 并解释了其能够降低服务器负载和缩减开发时间的原因。
在这个分为三部分的系列文章的第一篇中介绍了 Seam,它是既能显著增强 JSF 功能又能实现基于组件的架构的应用程序框架。在这篇文章中,我解释了 Seam 和其他经常与 JSF 结合使用的 Web 框架的不同之处,展示了向现有 JSF 应用程序添加 Seam 是多么轻松,最后概述了 Seam 对 JSF 应用程序生命周期的增强,同时还涉及到有状态的对话、工厂组件以及使用注释进行隐秘配置。
尽管这篇文章可能引发了您对 Seam 的兴趣,但是您可能无法确信它能够改善 JSF 开发体验。集成一组新工具通常比阅读它复杂得多,并且有时候并不值得。在无缝 JSF 系列文章的第二篇文章中,您将亲自发现 Seam 是否能够实现其简化 JSF 开发的承诺。在使用 Seam 构建执行标准 CRUD 操作的简单应用程序之后,我敢肯定您会认为 Seam 是对 JSF 框架的必要扩展。结果,Seam 还能帮助降低数据库层有限资源的负载。
Open 18 是基于 Web 的应用程序,允许用户管理一列曾经体验过的高尔夫课程,并记录每个场次的分数。为了体现本讨论的目的,该应用程序的范围仅限于管理高尔夫课程目录。第一个屏幕展现了已经输入的课程列表,并列出各个课程的一些相关字段,如课程名称、地点和专卖店的电话号码。用户可以从该处查看完整的课程详细内容、添加新课程、编辑现有课程,最终还可以删除课程。
在讲述如何使用 Seam 为 Open 18 应用程序开发用例时,我重点讲述它如何简化代码,自动管理一系列请求期间的状态,并对输入数据 执行数据模型验证。
该系列文章的目标之一是证明 Seam 可以集成到现有的任何 JSF 应用程序,并且不需要转换到 Enterprise JavaBeans (EJB) 3。因此,Open 18 应用程序并不依靠 Seam 的 JPA EntityManager
集成进行事务型数据库访问,也不依靠
EBJ3 有状态会话 bean 进行状态管理。(Seam
附带的示例 大多都使用了这两项技术。)Open 18 设计为使用无状态的分层架构。服务层和数据访问 (DAO) 层使用 Spring 框架绑定到一起。我相信 由于 Spring 在 Web 应用程序领域的普遍性,该设计是切实可行的选择。该应用程序展示了如何通过使用 conversation 作用域将有状态的行为引入到 JSF 托管的 bean。记住这些 bean 是简单的 POJO。
您可以 下载 Open 18 源文件 以及 Maven 2,以编译并运行样例代码。为了使您快速入门,我已经将该应用程序配置为使用 Seam 和 Spring-JSF 集成。如果想要在自己的项目中设置 Seam,可以 在 本系列第一篇文章 中找到完整的操作指导。请参见 参考资料 了解关于集成 JSF 和 Spring 的更多信息。
构建利用 Spring 框架的 JSF 应用程序的第一个步骤 是配置 JSF,使其可以访问 Spring 容器中的 bean。spring-web 包是 Spring 发布的一部分,附带有自定义 JSF 变量解析器,可构建此桥梁。 首先,Spring 解析器委托给 JSF 实现附带的本地解析器。本地解析器尝试将值绑定引用(如 #{courseManager}
)与
JSF 容器中的托管 bean 相匹配。 该 bean 名称是由 #{}
表达式分隔符之间的字符组成的,
在这个例子中为courseManager。如果该查找未能发现匹配,自定义解析器就会检查 Spring 的 WebApplicationContext
,以查找带有匹配 id
属性的
Spring bean。请记住 Seam 是 JSF 框架的扩展, 因此 JSF 可以访问的任何变量也可以被 Seam 访问。
Spring 变量解析器是使用变量解析器节点在 faces-config.xml 文件中 配置的,如清单1所示:
<variable-resolver> org.springframework.web.jsf.DelegatingVariableResolver </variable-resolver>
为了体现本文的目的,我假设基于 Spring 的服务层是不证自明的。除了 JSF-Spring 集成层之外 —— 该层负责向 JSF 公开 Spring bean (因此也向 Seam 公开该 bean),并没有深入地使用 Spring。服务层对象将作为无状态的接口对待,CRUD 操作可以委托给该接口。解决这些应用程序细节之后,就可以自由地重点研究 Seam 如何将托管 bean 转换成有状态的组件,这些组件明确其在促进用户与应用程序 交互方面的角色。
通过创建名为 courseAction
的
支持 bean 来支持管理高尔夫课程目录的视图,就开始开发 Open 18 应用程序。 该托管 bean 公开一个高尔夫课程对象集合,然后对管理这些实例的操作做出响应。 这些数据的持久化委托给基于 Spring 的 服务层。
在典型的 JSF 应用程序中,使用托管 bean 工具来 注册 CourseAction
bean,然后借助其委托对象(或
“依赖项”)注入该 bean。为此,必须打开 faces-config.xml 文件,然后使用该 bean 的名称和类添加新的 managed-bean
节点,如清单
2 所示。通过使用值绑定表达式 添加引用其他托管 bean 的子 managed-property
节点,指定要向该类的属性中注入的依赖项。
在这个例子中,惟一的 依赖项是无状态的服务对象courseManager,它是使用来自 Appfuse 项目的 GenericManager
类实现的(请参见 参考资料)。
<managed-bean> <managed-bean-name>courseAction</managed-bean-name> <managed-bean-class>com.ibm.dw.open18.CourseAction</managed-bean-class> <managed-property> <property-name>courseManager</property-name> <value>#{courseManager}</value> </managed-property> </managed-bean>
现在您想起了使用本地 JSF 方法 定义托管 bean 有多麻烦,请忘记曾经看到 managed-bean XML 声明 —— 因为您不再需要它了!在 Seam 构建的 应用程序中,bean 仅仅是使用 Java 5 注释声明的。Seam 将这些 bean 称为上下文组件。尽管您可能觉得该术语 很深奥,但是它只是描述一个组件(或命名实例)与给定的作用域(或称为上下文)有关。
Seam 在为上下文组件分配的作用域的生命期内对该组件进行管理。 Seam 组件更像 Spring bean,而不是 JSF 托管 bean,这是因为它们插入到复杂的、面向方面的框架。 在功能方面,Seam 框架远胜于 JSF 的基本控制反转 (IOC) 容器。 观察清单 3 中 courseAction
的声明。CourseAction
类被重构为利用
Seam 的注释。
@Name("courseAction") public class CourseAction { @In("#{courseManager}") private GenericManager<Course, Long> courseManager; }
注意所有 XML 语句都被去掉了!总之,这就是 Seam 注释的美妙之处。类的 @Name 注释指导 Seam 的变量解析器处理名称与 注释中的值相匹配的变量请求。然后 Seam 实例化这个类的实例,注入 @In
注释指派的任何依赖项,然后假借该变量名公开
该实例。使用清单 3 作为示例,Seam 创建了 CourseAction
类实例,将 courseManager
Spring
bean 注入courseManager
属性,然后在收到
对变量 courseAction
的请求时返回该实例。额外
的好处是,该 bean 的配置接近于代码,因此对继承代码库的新开发人员来 说更加透明(甚至对于您这样只学了 6 个月的人来说也是如此)。
@In
注释告知
Seam 将绑定表达式 #{courseManager}
的值注入到定义它的属性。安装
JSF-Spring 集成之后, 该表达式解析成 Spring bean 配置中定义的名为 courseManager
的
bean。
既然已经准备就绪,就可以继续研究第一个用例。 在 Open 18 应用程序的开始屏幕中,向用户提供了 当前存储在数据库中的所有课程列表。 借助 h:dataTable
组件标记,
清单 4 中的页面定义相当直观,并且不允许任何 Seam 特有的元素:
<h2>Courses</h2> <h:panelGroup rendered="#{courseAction.courses.rowCount eq 0}"> No courses found. </h:panelGroup> <h:dataTable id="courses" var="_course" value="#{courseAction.courses}" rendered="#{courseAction.courses.rowCount gt 0}"> <!-- column definitions go here --> </h:dataTable>
Java 代码可能有点难懂。清单 5 展示了如何使用本地 JSF 在作用域为该请求的支持 bean 中准备一个课程集合。为了简便起见, 去掉了注入的 Spring bean。
public class CourseAction { // ... private DataModel coursesModel = new ListDataModel(); public DataModel getCourses() { System.out.println("Retrieving courses..."); coursesModel.setWrappedData(courseManager.getAll()); return coursesModel; } public void setCourses(DataModel coursesModel) { this.coursesModel = coursesModel; } }
清单 5 中的 Java 代码看起来相当直观,不是吗?下面研究 JSF 使用支持 bean 时带来的性能问题。 提供实体列表时,您可能使用两种方法之一。 第一种是应用条件逻辑呈现至少包含一项的集合所支持的 h:dataTable
,第二种是显示一条信息型消息,声明找不到任何实体。
要做出决定,可能需要咨询 #{courseAction.courses}
,然后再对支持
bean 调用 相关的 getter 方法。
如果加载截至目前所开发的页面,然后查看最终 的服务器日志输出,就会看到:
Retrieving courses... Retrieving courses... Retrieving courses...
那么兄弟!如果您将这些代码投入生产,最好能 找到一个 DBA 找不到的安全隐藏点!这类代码执行对于数据库来说是个负累。 更糟的是,回发时情况会恶化,此时可能发生额外的冗余数据库调用。
如果曾经使用 JSF 开发过应用程序,就会了解盲目地 在 getter 方法中获取数据非常不妥。为什么? 因为在典型的 JSF 执行生命周期中,会多次调用 getter 方法。 工作区尝试通过委托对象使数据检索过程与后续的数据访问过程相 隔离。其目的是避免每次咨询支持 bean 的访问函数时 带来运行查询的计算成本。解决方案包括 在构造函数中初始化 DataModel
(
静态块),或 “init” 托管属性;在该 bean 的 私有属性中缓存结果;使用 HttpSession
或作用域为会话的支持
bean;并依赖 另一层 O/R 缓存机制。
清单 6 显示了另一种选择:使用作用域为该请求的 bean 的私有属性 临时缓存查找结果。您会发现, 这至少能够在页面呈现阶段消除冗余获取, 但是当该 bean 在后续页面超出作用域时,仍然会丢弃该缓存。
public class CourseAction { // ... private DataModel coursesModel = null; public DataModel getCourses() { if (coursesModel == null) { System.out.println("Retrieving courses..."); coursesModel = new ListDataModel(courseManager.getAll()); } return coursesModel; } public void setCourses(DataModel coursesModel) { this.coursesModel = coursesModel; } }
清单 6 中的方法只是切断数据检索和数据访问的尝试之一。 无论您制定什么样的解决方案,保持数据 的可用性直到不再需要是避免冗余数据获取的关键。 幸运的是,这类上下文状态管理正是 Seam 所擅长的!
Seam 使用工厂模式初始化非组件对象和 集合。一旦初始化数据之后,Seam 就可以将生成的对象放到 一个可用的作用域中,然后就可以在其中反复读取,而不再需要借助工厂方法 。这个特殊的上下文 就是 conversation 作用域。conversation 作用域提供了 在一组明确定义的请求期间临时维护状态的方法。
直到最近,也很少有 Web 应用程序架构提供 任何类型的能够表现对话的构造。 现有的任何上下文都没有提供 合适的粒度水平,用于处理多请求操作。 您会发现,对话提供了一种方式,可以防止短期存储丢失, 而短期存储丢失在 Web 应用程序中很常见,并且还是滥用数据库的根本原因。结合工厂组件模式使用对话使得在合适时咨询数据库成为可能,而不是为了重新获取应用程序未能跟踪的数据。
要完成一项任务,应用程序常常必须指导用户浏览一系列屏幕。 该过程通常需要多次向服务器发出 post, 或者是由用户直接提交表单,或者通过 Ajax 请求。 在任何一种情况下,都应该能够在用例期间通过维护服务器端对象的状态 跟踪该应用程序。对话相当于逻辑工作单元。它 允许您借助确定的起始点和结束点在单个浏览器窗口中为单个用户创建单 独的上下文。用户与该应用程序的交互状态是针对整个对话维护的。
Seam 提供了两类对话:临时对话和长时间运行的对话。临时对话 存在于整个请求过程,包括重定向。 这项功能解决了 JSF 开发过程中的一项难题, 即重定向将无意中丢弃存储在FacesContext
(如 FacesMessage
实例)中的信息。临时对话是
Seam 中的标准操作模式:您可以免费获得这些模式。这意味着 在经过重定向之后取出的任何值仍然能够存在,而不需要您执行额外的工作。 这项功能是安全网,允许 Seam 自由地在任意适当的时候使用重定向。
相比之下,长期运行的对话 能够在一系列明确定义的 请求期间保持作用域中的变量。您可以在配置文件中定义对话边界,借助 注释进行声明,也可以借助 Seam API,通过编程对其进行控制。 长期运行的对话有点像小会话,隔离在自己的浏览器选项卡中(或窗口), 能够在对话结束或超时时自动清除。与对应的会话相比,conversation 作用域的 要点之一是:conversation 作用域将发生在同一应用程序屏幕上位于多个浏览器选项卡中 的活动分离开。简单地讲,使用对话消除了并发冲突的危险。(请参见 参考资料 阅读关于 Seam 如何隔离并发对话的详细讨论。)
Seam 对话是对 ad-hoc 会话管理方法的重大改进,后者是现场临时 准备的,或者是其他框架鼓励使用的。conversation 作用域的引入还解决了很多开发人员指出的 问题,即 JSF 使用对象打乱了 HttpSession
,没有提供任何自动垃
圾回收 (GC) 机制。对话允许您创建有状态的组件,而不必使用 HttpSession
。借助
conversation 作用域,几乎不再 需要使用会话作用域,并且您可以更为随意地使用。
回到课程列表示例,这时该重构代码,以利用工厂模式。 目的是允许 Seam 管理课程集合,以便其在请求(包括重定向)期间保持可用。 如果希望 Seam 管理该集合,则必须使用合适的注释将创建过程交给 Seam。
Seam 使用构建函数实例化和装配组件。这些构建函数是在 bean 类中 通过注释声明的。实际上,您已经见到过其中一个例子: @Name
注释。@Name
注释告知
Seam 使用默认的 类构造函数创建新实例。要构建自己的课程列表,您不希望使用组件实例,而是 使用对象集合。为此,您希望使用 @Factory
注释。@Factory
注释向已提取变量的创建过程
附加了一个方法,这是在注释的值中指定的,当该变量没有绑定任何值时就会使 用该方法。
在清单 7 中,工厂方法 findCourses()
(位于 CourseAction
类)用于初始化 courses 属性的值,该值是作为 DataModel
提取到视图中的。该工厂方法通过将这项工作委托给服务层来实例化课程对象集合。
@Name("courseAction") public class CourseAction { // ... @DataModel private List<Course> courses; @Factory("courses") public void findCourses() { System.out.println("Retrieving courses..."); courses = courseManager.getAll(); } }
请注意,这里不存在 getCourses()
和 setCourses()
方法!借助
Seam,使用 标记着 @DataModel
注释的私有属性的名称和值将数据提取到视图中。
因此不需要属性访问函数。在这个方案中,@DataModel
注释执行两项功能。首先,它提取或公开 该属性,以便
JSF 变量解析器可以通过值绑定表达式 #{courses}
对它进行访问。
其次,它提供了手动在 DataModel
类型中包装课程列表的备选方式(如 清单
4 中所示)。作为替代,Seam 自动在 DataModel
实例中嵌入课程列表,以便其可以方便地
与 UIData
组件(如 h:dataTable
)一起使用。因此,支持
bean(CourseAction
)成为简单的
POJO。然后由该框架处理 JSF 特有的细节。
清单 8 显示了该视图中发生的相应重构。 与 清单 5 惟一的不同之处在于值绑定表达式。
利用 Seam 的提取机制时,使用缩写的值绑定表达式#{courses}
,而不是通过 #{courseAction.courses}
咨询支持
bean 的访问方法。提取的变量直接放到该变量上下文中,不受其支持 bean 的约束。
<h2>Courses</h2> <h:panelGroup rendered="#{courses.rowCount eq 0}"> No courses found. </h:panelGroup> <h:dataTable id="courses" var="_course" value="#{courses}" rendered="#{courses.rowCount gt 0}"> <!-- column definitions goes here --> </h:dataTable>
现在再次访问该页面时,以下消息在控制台中只出现一次:
Retrieving courses...
使用工厂构建函数以及临时 conversation 作用域能够在请求期间保持这些数据, 并确保变量 courses 仅实例化一次,而不管在视图中它被访问了多少次。
您可能想知道 @Factory
注释什么时候起作用。
为了防止注释变得太神秘,我们将逐步分析刚刚描述的创建方案。可以按照图 1 中的序列图进行研究:
视图组件(如 h:dataTable
)依靠
值绑定表达式 #{courses}
提供课程集合。本地
JSF 变量解析器首先查找与名称 courses 相匹配的 JSF 托管 bean。如果找不到任何匹配,Seam 就会收到解析该变量的请求。Seam 搜索其组件,然后发现在 CourseAction
类中,@DataModel
注释被指派给具有等价名称(courses
)的属性。
然后如果不存在 CourseAction
类实例,则创建之。
如果 courses
属性的值为 null
,Seam
就会再次使用该属性的名称作为键查找 @Factory
注释。
借助 findCourses()
方法找到匹配之后,Seam
调用它来初始化 该变量。最后作为 courses 提取该属性的值,将其包装到 DataModel
实例。现在
JSF 变量解析器和视图就可以使用包装的值。任何 针对此上下文变量的后续请求都会返回已经准备好的课程集合。
既然已经清楚检索课程列表以及在 Seam 托管的上下文变量中维护该值的方法, 下面研究课程列表以外的内容。您已经准备好与课程目录进行交互。 在以下几节中,将使用显示单门课程详细内容(以及添加、编辑和删除课程)的功能,扩展 Open 18 应用程序。
遇到的第一项 CRUD 操作是显示从课程列表中选出的单门课程的详细内容。 JSF 规范实际上为您处理了一些数据选择收集工作。当从 UIData
组件(如h:dataTable
)的
某行触发 h:commandLink
之类的操作时,在调用事件监听程序之前,
组件的当前行设置为与该事件相关的行。可以将当前行想象成一个指针, 在这个例子中,该指针固定在接受该操作的行。实际上,JSF 了解行操作与该行的底层数据有关。处理该操作时,JSF 帮助将这些数据放到上下文中。
JSF 本身允许您以两种方式访问支持被激活行的数据。 一种方式是使用 DataModel#getRowData()
方法检索该数据。另一种方法是
从对应于临时循环变量的值绑定中读取该数据, 该变量定义在组件标记的 var
属性中。在第二种情况下,在事件处理期间将再次向变量解析器公开
临时循环变量(_course)。这两种访问形式最终都需要与 JSF API 进行交互。
如果选择 DataModel
API
作为行 数据入口点,那么必须将 DataModel
包装器对象公开为支持
bean 的属性,如 清单 4 所示。另一方面,如果
选择通过值绑定访问行数据,则必须咨询 JSF 变量解析器。后一种方法还会将您与视图中使用的临时循环变量名称 _course 联系起来。
现在考虑 Seam 更抽象的获得所选数据的方法。Seam 允许您将针对 Seam 组件定义的 @DataModel
注释与 @DataModelSelection
补充注释配对。在回发期间,Seam
自动检测该配对。然后将 UIData
组件的当前行数据注入指派了 @DataModelSelection
注释的属性。该方法使支持
bean 与 JSF API 分离,因此使其返回 POJO 状态。
要确保回发时该课程列表仍然可用,并且 不必重新从数据库中获取该列表,就能呈现下一个响应, 则必须将当前的临时对话转变成长期运行的对话。
说服 Seam 将临时对话提升到长期运行对话的一种方式是 设置一个方法,使其在执行过程中驻留 @Begin
注释。还
必须将组件本身放到该 conversation 作用域中。通过在CourseAction
类定义
顶部添加 @Scope(ScopeType.CONVERSATION)
注释,就可以实现。使用长期运行的对话,允许变量保持作用域直至对话结束,
而不仅仅是单个请求。对于 UIData
组件来说,这种跨多个请求的稳定性尤其重要。
(请参阅 本系列第一篇文章 中关于有状态组件的讨论,了解
数据不稳定可能对 UIData
组件的列队执行事件所造成的问题。)
您希望允许用户从课程目录中选择单个课程。要实现这项功能, 在 h:commandLink
中包装各个课程的名称,h:commandLink
将方法绑定 #{courseAction.selectCourse}
指派成操作,
如清单 9 所示。当用户单击其中一个链接时,就会触发对支持 bean 的 selectCourse()
方法的调用过程。由于
Seam 控制着注入过程,所以与该行有关的课程数据 将自动分配给带有 @DataModelSelection
注释的属性。因此,不必执行任何查找,就能使用该属性,详细信息
如清单 10 所示。
<h2>Courses</h2> <h:panelGroup rendered="#{courses.rowCount eq 0}"> No courses found. </h:panelGroup> <h:dataTable id="courses" var="_course" value="#{courses}" rendered="#{courses.rowCount gt 0}"> <h:column> <f:facet name="header">Course Name</f:facet> <h:commandLink id="select" action="#{courseAction.selectCourse}" value="#{_course.name}" /> </h:column> <!-- additional properties --> </h:dataTable>
向提供数据选择的支持 bean 添加的内容主要是注释; 放到 conversation 作用域时,必须将该类序列化。
@Name("courseAction") @Scope(ScopeType.CONVERSATION) public class CourseAction implements Serializable { // ... @DataModel private List<Course> courses; @DataModelSelection private Course selectedCourse; @Begin(join=true) @Factory("courses") public void findCourses() { System.out.println("Retrieving courses..."); courses = courseManager.getAll(); } public String selectCourse() { System.out.println("Selected course: " + selectedCourse.getName()); System.out.println("Redirecting to /courses.jspx"); return "/courses.jspx"; } }
在 清单 10 中可以看出,所有变量作用域是由
Seam 处理的。 当执行工厂方法来初始化课程集合时,Seam 遇到 @Begin
注释,因此将该临时对话提升为长期运行的对话。@DataModel
注释提取的变量采用其所有者组件的作用域。因此,
在对话期间,该课程集合保持可用。当遇到标记着 @End
注释的方法时,
对话结束。
单击某一行的课程名称时,Seam 使用支持该行的课程数据值 填充带有@DataModelSelection
注释的属性。
然后触发操作方法 selectCourse()
,导致在控制台上显示
所选课程的名称。最后,重新显示课程列表。 随后就会在控制台中看到:
Retrieving courses... Selected course: Sample Course Redirecting to /courses.jspx
借助 Seam,就不必在 faces-config.xml 中定义 导航规则,即映射每个操作的返回值。取而代之,Seam 检查 操作的返回值是不是有效的视图模板(技术上称之为视图 id), 并对其执行动态导航。这项功能能够使简单的应用程序保持简单, 还允许对更高级的用例使用声明式导航。请记住, 在这个例子中,Seam 在执行导航时发出了重定向命令。
如果需要通过声明结束对话,则可以使用 @End(beforeRedirect=true)
注释操作方法 selectCourse()
,在这种情况下,
对话会在每次调用该方法后结束。beforeRedirect
属性确保在呈现下一
个页面之前清除对话上下文中的变量,这样能使临时对话的工作短路, 而在重定向时临时对话通常会填充这些值。 在这个方案中,在每次选中课程时开始数据准备过程。 执行完以上描述的同一事件序列之后,现在控制台将显示:
Retrieving courses... Selected course: Sample Course Redirecting to /courses.jspx Retrieving courses...
您尚未详细了解显示课程的用例。@DataModelSelection
注释负责
将当前行数据注入支持 bean 的实例变量,但是它不是在执 行该操作方法之后填充数据,使其可用于随后的视图。为此, 必须提取所选的值。
您已经看到一种注入形式,即 @DataModel
注释向要呈现的视图
公开一个对象集合。@DataModel
注释对单个对象实例的补充是 @Out
注释。@Out
注释仅仅获取该属性,并使用该属性自己的名称向变量解析器公开其值。
默认情况下,每次激活时,@Out
注释都需
要非 null 值。因为并非总是存在课程选择,如第一次显示课程列表时, 所以必须将所需的注释标记设置为 false,以表明该提取是有 条件的。
默认情况下,@Out
注释反映了
用于确定上下文变量名称的属性名称。如果您认为更合适的话, 可以选择为提取的变量使用不同名称。因为课程数据将被提取 到 conversation 作用域,并且可能在后续的一些请求中使用, 所以该名称的 “所选” 特征失去了原来的意义。在这种情况下, 最好使用实体本身的名称。因此,selectedCourse
属性的推荐注释为@Out(value="course",
required=false)
。
可以在新页面上显示课程详细内容,也可以 显示在同一页面的表格下面。为了演示的目的,在同一页面显示详细内容,同时限制要构造的视图数目。要在另一个页面中 访问提取的变量,不需要额外的工作或特殊技巧。
与 该支持 bean 的上一个版本 的差别不大,因此,清单
11 仅突出显示了两者的不同之处。selectedCourse
属性现在有两个注释。selectCourse()
方法也被稍加整理。
现在它在继续呈现视图之前重新提取该课程对象。在无状态的设计中, 必须确保完全由数据层填充对象,并且正确地初始化任何与显示其详细 内容有关的延迟加载。
// ... @DataModelSelection @Out(value="course", required=false) private Course selectedCourse; public String selectCourse() { System.out.println("Selected course: " + selectedCourse.getName()); // refetch the course, loading all lazy associations selectedCourse = courseManager.get(selectedCourse.getId()); System.out.println("Redirecting to /courses.jspx"); return "/courses.jspx"; } // ...
其中大多数有趣的变化都发生在视图中,但是这些变化并不新奇。清单 12 显示了在选中某个课程时,呈现在 h:dataTable
下面的详细内容面板:
<h:panelGroup rendered="#{course.id gt 0}"> <h3>Course Detail</h3> <table class="detail"> <tr> <th>Course Name</th> <td>#{course.name}</td> </tr> <!-- additional properties --> </h:panelGroup>
Open 18 应用程序最复杂的用例是创建和更新操作。 但是借助 Seam,实现起来并不困难。要完成这两项需求, 必须使用一个额外的注释:@In
。将课程提取到呈现课程编辑器表单的视图之后,
必须在回发时捕获已更新的对象。 就像使用 @Out
将变量推送到视图中一样,
可以使用 @In
在回发时重新捕获它们。
当用户处理加载到表单中的课程信息时, 该课程实体耐心地在 conversation 作用域中等待。 因为应用程序使用无状态的服务接口,所以此时的课程实例 看作已经与持久化上下文 “分离”。提交该表单时, 最终到达 JSF 的更新模型值(Update Model Value)阶段。 此时,与表单中字段有关的课程对象将收到用户的更新。 当调用该操作方法时,必须重新使已更新的对象与持久化 上下文建立联系。通过使用 save()
方法将该对象传递回服务层,就可以实现。
但是等等 —— 验证在哪里?您肯定不希望无效数据损坏您的数据库! 另一方面,您可能不希望验证标记打乱您的视图模板。 您甚至可能同意验证代码不属于视图层的说法。 幸运的是,Seam 负责完成 JSF 验证的琐碎工作!
如果您将整个表单包装到一个 s:validateAll
组件标记中,
Seam 允许您在 JSF 的流程验证(Process Validation)阶段执行对数据模型 定义的验证。这种验证方法比以下方法更有吸引力: 在视图中到处设置 JSF 验证器标记,或者维护一个配置文件, 写满针对第三方验证框架的验证定义。取而代之, 可以使用 Hibernate Validator 注释向实体类属性指派验证标准, 如清单 13 所示。然后 Hibernate 在持久化对象时,对验证进行两次检查,为您提供双重保护。这个双重保障方法意味着
视图中不小心出现的 bug 没有任何机会危害您的数据质量。(请参阅 参考资料了解关于
Hibernate Validator 的更多内容。)
@Entity @Table(name = "course") public class Course implements Serializable { private long id; private String name; private CourseType type = CourseType.PUBLIC; private Address address; private String uri; private String phoneNumber; private String description; public Course() {} @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") @NotNull public long getId() { return this.id; } public void setId(long id) { this.id = id; } @Column(name = "name") @NotNull @Length(min = 1, max = 50) public String getName() { return this.name; } public void setName(String name) { this.name = name; } @Column(name = "type") @Enumerated(EnumType.STRING) @NotNull public CourseType getType() { return type; } public void setType(CourseType type) { this.type = type; } @Embedded public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } @Column(name = "uri") @Length(max = 255) @Pattern(regex = "^https?://.+$", message = "validator.custom.url") public String getUri() { return this.uri; } public void setUri(String uri) { this.uri = uri; } @Column(name = "phone") @Length(min = 10, max = 10) @Pattern(regex = "^\\d*$", message = "validator.custom.digits") public String getPhoneNumber() { return this.phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } @Column(name = "description") public String getDescription() { return this.description; } public void setDescription(String description) { this.description = description; } // equals and hashCode not shown }
课程对象仅在回发时注入,而回发是用户提交课程编辑器表单 触发的,不是由每个涉及 courseAction
组件的请求触发的。
要想有条件地使用@In
注释,必须在定义它时将
其 required
标志设置为 false。这样做
可以确保 Seam 在找不到要注入的课程对象时不会发出警报。
当提交课程编辑器表单时,就可以注入以前提取的课程对象。 要确保将该实例重新注入回同一属性,则向 @In
注释提供的名称必须等价于@Out
注释所使用的名称。
作为添加这些内容的结果,selectedCourse 属性现在拥有三个注释。 (情况变得复杂起来!)
还必须向支持 bean 提供三个额外的操作方法,以处理 讲述到的新 CRUD 操作。新注释以及 addCourse()
、editCourse()
和saveCourse()
操作方法如清单
14 所示:
// ... @DataModelSelection @In(value="course", required=false) @Out(value="course", required=false) private Course selectedCourse; public String addCourse() { selectedCourse = new Course(); selectedCourse.setAddress(new Address()); return "/courseEditor.jspx"; } public String editCourse() { selectedCourse = courseManager.get(selectedCourse.getId()); return "/courseEditor.jspx"; } public String saveCourse() { // remove course from cached collection // optionally, the collection could be nullified, forcing a refetch if (selectedCourse.getId() > 0) { courses.remove(selectedCourse); } courseManager.save(selectedCourse); // add course to the cached collection // optionally, the collection could be nullified, forcing a refetch courses.add(selectedCourse); FacesMessages.instance().add("#{course.name} has been saved."); return "/courses.jspx"; } // ...
课程编辑器页面负责创建和更新。Seam 之所以这么酷,是因为它能够暗中指挥通信,在这个例子中, 是通过在您浏览页面时将所选课程保存在上下文中实现的。 不需要使用 HttpSession
请求参数,
也不需要想方设法存储所选课程。而仅仅是提取想要公开的内容, 并注入期望接收的内容。
从编辑器页面(如清单 15 所示)观察表单组件。该页使用了以下两个 Seam 组件标记,使得开发视图的工作变得更加简单:
s:decorate
结合 afterInvalidField
facet
在每个输入组件之后插入 s:message
组件,
输入组件使您不必在页面中重复标记。s:validateAll
指导
Seam 将 Hibernate Validator 注释结合到 JSF 验证过程, 以便在回发时验证表单中的每个字段。您不会在课程编辑器视图页面上发现任何本地 JSF 验证器, 因为 Seam 在利用 Hibernate Validator 时,完全不需使用本地验证器。 该页面还显示了 Seam 附带的枚举转换器 组件,以防您碰巧使 用 Java 5 枚举类型。
<h2><h:outputText value="#{course.id gt 0 ? ‘Edit‘ : ‘Create‘} Course" /></h2> <h:form id="course"> <s:validateAll> <f:facet name="afterInvalidField"> <s:span styleClass="error"> <s:message showDetail="true" showSummary="false"/> </s:span> </f:facet> <ul> <li> <h:outputLabel for="name" value="Course Name"/> <s:decorate> <h:inputText id="name" value="#{course.name}" required="true"/> </s:decorate> </li> <li> <h:outputLabel for="type" value="Type"/> <s:decorate> <h:selectOneMenu id="type" value="#{course.type}"> <s:convertEnum /> <s:enumItem enumValue="PUBLIC" label="Public" /> <s:enumItem enumValue="PRIVATE" label="Private" /> <s:enumItem enumValue="SEMI_PRIVATE" label="Semi-Private" /> <s:enumItem enumValue="RESORT" label="Resort" /> <s:enumItem enumValue="MILITARY" label="Military" /> </h:selectOneMenu> </s:decorate> </li> <li> <h:outputLabel for="uri" value="Website" /> <s:decorate> <h:inputText id="uri" value="#{course.uri}"/> </s:decorate> </li> <li> <h:outputLabel for="phone" value="Phone Number" /> <s:decorate> <h:inputText id="phone" value="#{course.phoneNumber}"/> </s:decorate> </li> <li> <h:outputLabel for="city" value="City" /> <s:decorate> <h:inputText id="city" value="#{course.address.city}"/> </s:decorate> </li> <li> <h:outputLabel for="state" value="State" /> <s:decorate> <h:selectOneMenu id="state" value="#{course.address.state}" required="true"> <s:selectItems var="state" value="#{states}" label="#{state}" /> </h:selectOneMenu> </s:decorate> </li> <li> <h:outputLabel for="zip" value="ZIP Code" /> <s:decorate> <h:inputText id="zip" value="#{course.address.city}"/> </s:decorate> </li> <li> <h:outputLabel for="description" value="Description" /> <s:decorate> <h:inputTextarea id="description" value="#{course.description}"/> </s:decorate> </li> <ul> </s:validateAll> <p class="commands"> <h:commandButton id="save" action="#{courseAction.saveCourse}" value="Save"/> <s:button id="cancel" view="/courses.jspx" value="Cancel"/> </p> </h:form>
回顾代码片段,可以发现到目前为止重点内容大多涉及 消除代码、选择,而不是通过注释描述功能,并由框架负责处理细节。 这种简单性允许您集中精力处理更复杂的问题,并添加深受大家喜欢的 奇特 Ajaxian 效果。您可能尚未认识到只需再做少量工作,就 可以实现所有 CRUD 操作 —— 实际上即将到达最后阶段!
在应用程序中实现删除功能是一项简单的事情。 只需向每行添加另一个 h:commandLink
,该命令链接
能激活支持 bean 的删除方法(deleteCourse()
)。我们已经
实现了公开所选课程的工作,仅仅需要将绑定到课程属性的课程对象传递给 CourseManager
以终止该课程,如
清单 16 中所示:
<h:dataTable id="courses" var="_course" value="#{courses}" rendered="#{courses.rowCount gt 0}"> <h:column> <f:facet name="header">Course Name</f:facet> <h:commandLink id="select" action="#{courseAction.selectCourse}" value="#{_course.name}" /> </h:column> <h:column> <f:facet name="header">Actions</f:facet> <h:commandLink id="delete" action="#{courseAction.deleteCourse}" value="Delete" /> </h:column> <!-- additional properties --> </h:dataTable>
在 deleteCourse()
方法中,如清单
17中所示,利用 Seam 的 FacesMessages
组件警告用户正
在发生的操作。该消息是以典型的途径在视图中使用 h:messages
JSF
组件显示的。但是首先请注意, 创建消息是多么简单!您可以彻底抛弃以前令人头疼的 JSF 工具类;Seam 可靠地消除了 JSF 以前的阴影。
// ... public String deleteCourse() { courseManager.remove(selectedCourse.getId()); courses.remove(selectedCourse); FacesMessages.instance().add(selectedCourse.getName() + " has been removed."); // clear selection so that it won‘t be shown in the detail pane selectedCourse = null; return "/courses.jspx"; } // ...
处理完所有 CRUD 操作,就即将完工了!剩下的惟一的一个步骤 是将整个课程列表组装到一起,如清单 18 所示:
<h2>Courses</h2> <h:messages id="messages" globalOnly="true" /> <h:panelGroup rendered="#{courses.rowCount eq 0}"> No courses found. </h:panelGroup> <h:dataTable id="courses" var="_course" value="#{courses}" rendered="#{courses.rowCount gt 0}"> <h:column> <f:facet name="header">Course Name</f:facet> <h:commandLink id="select" action="#{courseAction.selectCourse}" value="#{_course.name}" /> </h:column> <h:column> <f:facet name="header">Location</f:facet> <h:outputText value="#{course.address.city}, #{course.address.state}" /> </h:column> <h:column> <f:facet name="header">Phone Number</f:facet> <h:outputText value="#{course.phoneNumber} /> </h:column> <h:column> <f:facet name="header">Actions</f:facet> <h:panelGroup> <h:commandLink id="edit" action="#{courseAction.editCourse}" value="Edit" /> <h:commandLink id="delete" action="#{courseAction.deleteCourse}" value="Delete" /> </h:panelGroup> </h:column> </h:dataTable> <h:commandButton id="add" action="#{courseAction.addCourse}" value="Add Course" /> <h:panelGroup rendered="#{course.id gt 0}"> <h3>Course Detail</h3> <table class="detail"> <col width="20%" /> <col width="80%" /> <tr> <th>Course Name</th> <td>#{course.name} <span class="notation">(#{course.type})</span></td> </tr> <tr> <th>Website</th> <td><h:outputLink value="#{course.uri}" rendered="#{not empty course.uri}">#{course.uri}</h:outputLink></td> </tr> <tr> <th>Phone</th> <td>#{course.phoneNumber}</td> </tr> <tr> <th>State</th> <td>#{course.address.state}</td> </tr> <tr> <th>City</th> <td>#{course.address.city}</td> </tr> <tr> <th>ZIP Code</th> <td>#{course.address.postalCode}</td> </tr> </table> <h:panelGroup rendered="#{not empty course.description}"> <p><q>...#{course.description}</q></p> </h:panelGroup> </h:panelGroup>
恭喜!您完成了第一个基于 Seam 的 CRUD 应用程序。
在 无缝 JSF 系列第二篇文章中,您亲自发现了 Seam 的 Java 5 注释如何简化代码,conversation 作用域如何自动在一系列请求期间管理状态, 以及如何同时使用 Seam 和 Hibernate Validator 对输入数据执行数据模 型验证。
实际上可以使用 seam-gen 自动完成大多数 CRUD 工作(请参见 参考资料), seam-gen 是 Ruby-on-Rails 样式的 Seam 应用程序生成器。但是我希望您从本文的练习 中了解到 Seam 不仅仅是另一个 Web 框架。采用 Seam 并不强制您抛弃 JSF 经 验。相反,Seam 是对 JSF 非常强大的扩展,实际上它增强了 JSF 的生命周期。Seam 和 JSF 结合起来可以顺利地和任何无状态的服务层或 EJB3 模型进行集成。
既然已经了解 Seam 减轻 JSF 开发的一些方式, 您可能想知道它对 第 1 部分 中 讨论的更高级 Web 2.0 技术的支持程度。在本系列的最后 一个部分中,将讲述如何使用 Ajax remoting 通过在课程目录 和 Google Maps 之间创建 mashup, 进一步开发 Open 18 应用程序, 在这个过程中,您将了解 Seam 的 Java 5 注释和捆绑的 JavaScript 库如何指导浏览器和服务器端组件之间的通信。
再见,同时祝您玩高尔夫愉快!
用 Seam Remoting 和 Ajax4jsf 无缝熔接客户机和服务器
JSF 基于组件的方法论促进了抽象,但大多数 Ajax 实现由于公开了底层的 HTTP 交换而使之大受干扰。在 无缝集成 JSF 系列最后的这篇文章中,Dan Allen 展示了如何使用 Seam Remoting API 和 Ajax4jsf 组件与服务器上的受管 bean 通信,就好像这些 bean 与浏览器同在本地一样。您将了解利用 Ajax 作为 JSF 事件驱动架构的一种自然改进是多么地容易,以及如何在不影响 JSF 组件模型的前提下实现这一目的。
时下,大多数 Java?开发人员都很看好 mashup,所以您可能会困惑:Seam 与号称 Web 2.0 的技术,尤其是 Ajax,如何能集成。若能使用 Seam 启动 JSF 中的部分页面更新或者用 Google Map 协助 JSF 应用程序 mashup,那将非常酷,不是么?您不仅能这么做,而且还非常容易。
在 无缝集成
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 让用户可以定位地图中的高尔夫球场目录中的位置。将此球场目录和球场细节页合并起来(并将低层代码 Ajax 化)可以让您显示球场的细节信息而无需加载新页。将 Spring bean 和 Seam Remoting 相集成让您可以捕获 Google Maps 位置标记的重定位并能将相关球场的经度和纬度存储到数据库中。如您所见,结果就是会产生所有高尔夫球员都喜欢使用的 Web 2.0 风格的应用程序,这真是让人印象深刻!
如果您曾经深受涉及到大量 JavaScript 的过于复杂的 Ajax 编程之苦,如果到目前为止,您都由于不想面对其复杂性而一直尽量避免使用 Ajax,那么本文所要教授的内容将会帮助您免除这种担心。在重构应用程序时,您需要进行一些 JavaScript 编码,但与大多数 Ajax 实现不同,JavaScript 并不会占据您代码中的大部分;相反,它只扩展了服务器端的 Java 对象。
正如在应用程序中希望避免显式的内存管理一样,您亦 不希望必须要处理低层的 Ajax 请求协议。这么做只会带来更大的麻烦(更确切地说,是更多的麻烦),比如多浏览器支持、数据封送处理、并发冲突、服务器负载以及定制 servlet 和 servlet 过滤器。其中您想要避免的最大的麻烦是无意间公开的无状态的请求 - 响应范例,但该范例是基于组件的框架,比如 JSF,所想要隐藏的。
JSF 生命周期通过对底层的 servlet 模型屏蔽应用程序代码促进了面向组件的设计。为了保持处理 Ajax 的这种抽象性,您可以将低层的这些琐碎工作交由 Seam Remoting 或 Ajax4jsf 处理。这两个库均可负责通过 Ajax 交互将 JSF 组件熔合到浏览器时所需的管道处理。图 1 显示了实际应用中的 Seam Remoting。当事件触发时,比如用户单击了一个按钮,消息就会异步发送给服务器上的组件。一旦收到响应,它就会用来对此页进行增量更新。用来处理浏览器和服务器端组件间的交互的低层通信协议都藏于 API 之后。
在图 1 所示的用例中,用户能看到单击按钮后所发生的方法调用的结果。在研究此用例时,有两个要点需要注意: (1) 该页永远无法刷新;(2) 客户端代码与组件上的方法进行透明通信,而不是显式地构建然后再请求 URL。标准的 HTTP 请求在后台使用,但客户端代码永远无需直接与 HTTP 协议交互。
Seam Remoting 和 Ajax4jsf 是两个独特的库,可分别服务于 JSF 的 “Ajax 化” 的目的。两个库均使用 Ajax 来引入交互模型,其中浏览器和服务器间的通信可以在后台异步发生,并对用户不可见。没有必要为了执行服务器上的方法而浪费用户页面重载的时间。在这些库所发出的 Ajax 请求中由服务器检索到的信息可用来增量地 “实时” 更新页面的状态。两个库均可配备生命周期,此生命周期可以在浏览器需要的时候恢复(restore)组件的状态。这种 Ajax 交互并不是真的请求而是一种 “恢复并执行”。浏览器像是 “敲敲” 服务器的 “肩膀”,请它在服务器端的一个受管 bean 上执行一个方法并返回结果。
虽然这两个库工作起来有些差别,但它们并不是相互排斥的。由于它们都采用的是 JSF 组件模型,所以二者可以很容易地相互结合,这将在本文后面的部分详细介绍。目前,我们只需分别考虑二者各自将 Ajax 风格的交互引入 JSF 应用程序的方式:
我将深入探究这些方式,但我们还是先来看看 Ajax 的基础知识吧。
要想让应用程序成为 Ajax/Web 2.0 意义上的 “富” 应用程序,Web 浏览器(也即客户机)必须能直接访问服务器上的组件。由于在客户机和服务器间存在着巨大差异,让这个目标成为现实还是很有挑战性的。差异的一个方面(即网络)存在于客户机浏览器,另一方面存在于服务器和其组件。Ajax 应用程序的一个目标是让它们可以相互 “通话”。
实际上,在大多数传统的 Web 应用程序中,客户机和服务器可以正常通信,但交互性就完全是另一回事了。服务器发起对话,浏览器收听对话。您如果以前曾陷于 这类对话之中,实在不足为怪。在没有 Ajax 通信的世界里,浏览器可以发送对任何 URL 的同步请求,但它必须要呈现服务器发回的 HTML。这类交互性的另一个不足之处是等待时间很多。
若只有 HTTP 这种没有什么发展的语言,浏览器客户机将对服务器如何生成 HTML 完全束手无策,进而也就无从知道其组件的内容。从浏览器的立场上看,页面生成过程完全是个黑盒子。浏览器可以以 URL 形式询问服务器不同的问题并将打包成请求参数和 POST 数据的提示一并传递,但它其实并不 “讲” 服务器的语言。如果想要给浏览器提供一个到应用程序服务器端动作的视图,则需要建立更复杂的通信手段。这种面向页面的确定性方法对此无能为力。
Seam Remoting 和 Ajax4jsf 对打破客户和浏览器组件的隔阂所采用的方式有所不同,所以很有必要知道如何利用好这二者。Seam Remoting 提供了浏览器本地语言 JavaScript 形式的 API,通过这些 API 服务器端组件上的方法就可以被访问到。若要将访问权赋予这些方法,它们必须通过 @Remote
注释被显式地标记为
“remote”。
Seam Remoting 中的调用机制与 Java RMI 类似,二者都使用本地代理对象或 “存根” 来允许 JavaScript 调用位于远程服务器上的组件。就客户而言,该存根对象 就是远端对象。该存根负责实现实际远端对象上的方法执行。当远端方法被调用时,响应就会将此方法调用的返回值封装起来。返回值被编制处理成一个 JavaScript 对象。因此,Seam Remoting 就使浏览器可以 “讲” 服务器的本地语言。Java 代码和 JavaScript 现已合二为一,这对于那些认为这二者原本就是一种语言的人多少有点出乎意料。
另一方面,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 并不会如您曾领教过的那些远端 EJB 对象那样可怕。使用 Seam Remoting API 启用 JavaScript 代码来与服务器端组件进行交互最好的一面是过程异乎寻常地简单。Seam 真的可以为您完成所有的辛苦工作 —— 您甚至都无需编辑一行 XML,就可以开始使用它!(如果目前您进行 XML 编程要比 Java 编程多,那么这真是一个振奋人心的消息。)
让我们先来快速看看使用 Seam Remoting 来 “Ajax 化” JSF 应用程序所需的步骤。
将服务器端对象方法对远端 Ajax 公开有两个要求:此方法必须是 Seam 组件的公共成员且必须配备 @WebRemote
注释。就这两点!
实际的简单性在清单 1 中可见一斑,其中 Seam 组件 ReasonPotAction
为了远端执行而
向 Ajax 客户公开了单一一个方法,即drawReason()
。每次这个方法在此存根上被调用的时候,该调用都会跨
Internet 传递到服务器并会通过在服务器端使用对应的方法随机选择所列的 “在项目中采用 Seam 的十大理由” 之一。服务器随后向客户返回该值(有关这十个理由的更多信息,请参见 参考资料)。
@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 之下:
<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>
Seam Remoting 机制真正的幕后原因是 JavaScript 库。Resource Servlet 将作业指派给 Remoting
对象以服务这些库。所发出的两个
JavaScript 库建立适合浏览器的挂钩以便借助 JavaScript 调用远端方法。
必须要将这两个 JavaScript 库都导入页面,如清单 3 所示。第一个库 remote.js 是静态的,将 Seam 的客户端 remoting 框架传递给浏览器。第二个库 interface.js 是在每个请求上动态创建的。它包含远端存根和与服务器端组件进行交互所需的复杂类型。注意,Seam 并不为所有指定一个方法为远端的所有组件都生成存根和复杂类型。相反,它会解析 interface.js URL 中的 ?
字符后面的该查询字符串来收集要公开的组件名。每一个组件名都由 &
符号分隔。使用这种堆栈式的方法可以使浏览器为外部
JavaScript 文件所进行的请求的数量为最小。Seam 之后会只为这些组件生成这些存根和复杂类型。作为一个例子,清单 3 中的这两个 JavaScript 导入将会加载 Seam Remoting 库并指示 Seam 准备好reasonPot
和 anotherName
组件:
<script type="text/javascript" src="seam/resource/remoting/resource/remote.js"></script> <script type="text/javascript" src="seam/resource/remoting/interface.js?reasonPot&anotherName"></script>
有了这些,您就可以着手了!您已经完成了 Seam Remoting 设置,现在可以开始进行一些远程调用了。
所有用于 Seam Remoting 库的客户端框架代码都封装在 Seam
JavaScript
对象中。实际上,这个高层对象只是一个名称空间 —— 一个可以将一组功能捆绑在惟一一个名字之下的容器。在 JavaScript 中,名称空间不过是一个可以加载静态属性、静态方法和其他一些嵌套名称空间对象的对象。需要注意的 重要一点是 Seam Remoting 中的 JavaScript
与其他 JavaScript 可以很好地合作。这意味着您可以利用诸如 Prototype 或 Dojo 这样的库来让 JSF UI 与服务器端组件交互。在 JSF UI 中使用第三方 JavaScript 库过去一直是 JSF 开发人员所面临的一个巨大挑战,所以 Seam
JavaScript
对象备受欢迎:
Seam Remoting 库的功能在 Seam.Component
和 Seam.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 方法调用一样,不是么?
<script type="text/javascript"> Seam.Component.getInstance("reasonPot").drawReason(displayReason); function displayReason(reason) { alert(reason); } </script>
在清单 4 中,应该注意到存根上的方法和服务器端组件上的 “实际” 方法存在一个重要差异。您觉察到了么?每个在组件存根上的调用都是异步完成的。这意味着远端方法调用的结果并不能立即对执行线程可用。正因为如此,存根上的方法并没有要返回的值,而不管匹配的服务器端方法是否有要返回的值。当远端存根上的方法执行时,它实际上像是在说:“好吧,我稍后再答复您,请留下您的联系电话。”您所提供的码实际上就是 “回调” JavaScript 函数。其任务是捕获实际的返回值。
回调函数是异步 API 的标准结构。当来自 Ajax 请求的响应回到浏览器时,它由 Seam Remoting JavaScript 框架执行。如果服务器端组件上的方法具有非空返回值,该值就会作为其惟一的实参传递进此回调函数。另一方面,如果服务器端组件上的方法具有一个空返回类型,则可以忽略此可选参数。
对于大多数 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 作用域的益处 并参与状态行为。
清单 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 JavaScript API 能代您完成很多工作的话,那就另当别论了。GMap2
类可以绘制地图并负责视图端口中的滚动和缩放事件。另一个
Google Maps 类 GClientGeocoder
则基于地址字符串解析地理空间的坐标。您的工作只不过就是初始化地图并为每个球场添加标记。
要在地图上放上标记,首先要通过远端方法调用从服务器端组件获取球场集。接下来,用GClientGeocoder
将每个球场的地址翻译成地理空间点(经度和纬度)。最后,使用该点来在地图的相应坐标上放置标记。作为一个额外的小特性,您还可以将编辑图标旁边的罗盘图标装备在目录中的每行。当单击了目录行中的罗盘图标时,地图就会缩放直到所选球场出现在视图内。与此同时,此地图还会在标记上呈现一个气球,显示给定球场的地名、地址、电话和
Web 站点。通过直接单击地图上的一个标记也可以弹出相同的气球。图 2 显示的是完成后的应用程序的预览:
总的来说,地图组件和面向位置数据的集成很有趣,此 mashup 尤其具启迪意义。用户现在可以实际查看地图上每个球场的边界!在 Map 模式,高尔夫球场属性用淡绿色呈现。如果缩放功能足够强大,那么在球场区域还会出现一个标签覆盖图,显示此球场的名称。Satellite 模式则更有趣,它显示了球场的面貌。若缩放的程度足够,甚至还可以看到每个球洞的特征,比如发球台、球道和果岭!高尔夫球场位置的 mashup 以及互动性的地图视图就能让您的努力有所回报。
Google Maps 很易于集成和嵌入到 Web 应用程序。正如我已经提到的,它负责了所有呈现地图的所有细节并会提供一个 API 来绘制地图上的地理空间位置。GClientGeocoder
对象担负所有解析地理空间位置和回送所需的经度和纬度数据这样的艰巨任务。
将地址解析为地理空间点的方法存根与 Seam Remoting 方法存根的工作原理相同。当该方法被调用来获取返回值时,一个回调函数会传递给此方法。在那时,还会向 Google HQ 发送一个 Ajax 请求,而且当响应回至浏览器时,此回调函数会执行。Google Maps API 的智能界面让定位变得非常文字化,因为所基于的只有邮寄地址。您无需再为每个球场维护地理空间的坐标,那样只会增加混乱!这个 API 上的额外的例程之后会使用纬度和经度数据来为地图上的这些位置构建呈现标记。
与地图集成相关的 API 方法有两个:Geocoder.getLatLng()
和 GMap.addOverlay()
。首先,Geocoder.getLatLng()
方法将地址字符串解析为一个 GLatLng
点。数据对象只用来包装经度和纬度值对。此方法的第二个实参是一个回调
JavaScript 函数,一旦与 Google HQ 的通信完成,该函数即会执行。此函数继续执行以通过使用 GMap.addOverlay()
来将一个标记覆盖图添加到地图上。默认地,标记以红色的回形针表示。回形针的顶点指向地图上的地址位置。
清单 5 显示了设置 Google map 并向它添加标记的 JavaScript 代码。函数按执行顺序排列。除了新导入的 Google Maps API 脚本之外,您还应该识别出 清单 3中曾经用到的 Seam Remoting 脚本。
<script type="text/javascript" src="http://maps.google.com/maps?file=api&v=2.x&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.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 进行对话” 中有详细的介绍。)
<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
,然后将此工作分配给来自 清单
5的 showCourseInfoBalloon()
函数:
/** * 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 动作处理程序)。
@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()
方法返回在呈现页面时所出现的球场集。
至此,您可能会奇怪所有这些 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 应用程序的初始配置中,CourseAction
bean
依赖于 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}
表达式会正常解析。
Google Maps mashup 确实让人兴奋不已,但将它添加到 Open 18 应用程序却会引入一个小的设计问题。当用户为了查看有关此球场的详细信息而单击目录中的球场名时,页面就会刷新。结果,地图就会频繁地重载。这些重载会将太多负荷添加给浏览器和服务器,还会将地图恢复到其原始位置。
要防止地图重载,必须将所有动作都限制为单一页面加载。不幸的是,提交表单的处理与页面请求紧密相连。这种紧密的耦合是 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
:
<a4j:commandLink id="selectCourse" action="#{courseAction.selectCourseNoNav}" value="#{_course.name}" />
正如之前提到的,有两种方法来指明页面中的哪个区域需要增量更新。一种方法是针对单个组件,Ajax4jsf 动作组件的 reRender
属性中指定它们的组件
ID。另一种方法是在 a4j:outputPanel
标记中包裹页面中的区域并借助该标记的 ajaxRendered属性将这些区域标识为
“由 Ajax 呈现的”。清单 10 显示了查看模板中能输出所选球场细节的那个区域,方法是采用输出 - 面板方式:
<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。首先,向应用程序类路径添加 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 上下文参数中定义。
<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()
函数也进行了修改。
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 容器集成到 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
类中支持球场的地理空间点更新的相关部分:
@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 通信极大地补充了该模型。本文中给出的代码均无需直接与XMLHttpRequest
JavaScript
对象交互。相反,所有工作都藏于较高层 API 背后,让您可以直接查询和处理服务器端组件,也可以调用动作和更新给定页面的某些部分。得益于 Ajax 的魔力,所有的工作均可异步发生。
在本系列的这三篇文章中,您学习了大量有关结合了 Seam 的 JSF 开发的内容,但我还未能尽述 Seam 的所有特性!就让 无缝集成 JSF系列中的讨论和示例作为您学习如何将 Seam 用在 JSF 项目中以使其更容易创建的很好的开始点吧。越多地接触 Seam,您就越能发现它的可爱之处。
相关资料下载:
标签:
原文地址:http://blog.csdn.net/zajin/article/details/51612189