级别: 中级 Dan Allen ([email protected]), 高级 Java 工程师, CodeRyte, Inc. 2007 年 5 月 21 日 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 实现的应用程序堆栈: 图 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 实现。 图 2. Seam 项目中的 jar 库 接下来的步骤是在 web.xml 文件中安装 servlet 监听器类。该监听器在部署应用程序时初始化 Seam。 清单 1. Seam servlet 监听器配置
接下来,将 JSF phase 监听器添加到 faces-config.xml 文件中,如清单 2 所示。该监听器将 Seam 集成到标准 JSF 生命周期中。(图 3 大致描绘了集成到这个生命周期中的 Seam 增强。) 清单 2. Seam phase 监听器配置
最后,将一个空的 seam.properties 文件放在 类路径的根下,以便指示 Seam 进行加载,如清单 3 所示。这个空白文件被用作一个 JVM 类加载器优化,使 Seam 在类路径下更小的区域内搜索组件,从而大大减少加载时间。 清单 3. Seam 属性文件
当然,在这种最小设置中,Seam 的很多特性是不可用的。以上说明只是为了演示 Seam 很少涉足入门级使用。例如,Seam 包括一个 servlet 过滤器,该过滤器扩展 JSF 生命周期以外的 Seam 特性。 servlet 过滤器的用法包括与非 JSF 请求集成,通过重定向传播 conversation,以及管理文件上传。请参阅 参考资料,看看 Seam 参考文档,其中讨论了用于控制附加功能的配置文件 —— 特别是 EJB3 集成。
与典型的 JSF 配置过程相比,使用 Seam 开发受管 bean 非常容易。为了将 bean 暴露到 JSF 生命周期中,只需在类定义的上面添加一个简单的注释 清单 4 显示了 在 Seam 用语中,这个动作被称作双射(bijection,即 bidirectional injection 的简称)。当注出(outject)属性数据时,视图可以通过名称找到它。在 postback 或者组件初始化时,数据被注入(inject)到 一个组件中。后者是著名的控制反转(inversion of control,IOC)模式的一种实现,可用于连接委托对象。传统 IOC 与 Seam 的双射之间的主要不同点在于,双射使长期作用域中的组件可以引用短期作用域中的组件。可以进行这种连接是因为 Seam 在调用组件时(而不是启动容器时)解析依赖项。双射是有状态组件开发的基础。 显然,清单 4 中的 POJO bean 只是简单地演示了 Seam 的用法。随着本系列讨论的继续,我将探索另外的方法来实现 Seam。 清单 4. 一个典型的 Seam POJO bean
为 了使用由一个已有的 Spring 容器管理的服务层对象中的投资,需要将所有处理相关业务逻辑的 Spring bean 注入到 Seam 组件中。首先需要确保已经配置了 Spring-JSF 集成,它由 Spring 框架附带的一个定制变量解析器进行处理(见 参考资料)。有了这座桥梁,Spring 与 Seam 的集成就很简单,只需使用 清单 5. 注入一个 Spring bean
这个例子设置支持使用以轻量级容器(这里就是 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 扩展点: 图 3. Seam 生命周期增强 让我们来考虑其中一些增强,因为它们适用于 JSF 开发中一些常见的挑战。 Seam 演示了 Java 5 注释的一个非常实用的用法。Seam 的部署扫描程序检查所有包含 seam.properties 文件的归档文件,并为所有标有 在 不使用组件框架的情况下,另一个必须解决的熟悉的问题是预先处理每个请求,就像在基于动作的框架中那样。受此影响的用例是 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 的创立者允许在进入页面时请求参数 您 可能还注意到,当使用页面动作时,地址栏中的 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)的概念,工厂数据提供者由 关 于 JSF 很容易引起困惑的一个地方是它的状态管理功能。JSF 规范解释了在接收一个动作之后页面是如何 “恢复(restored)” 的,在此期间时间事件要进行排队,选择要注册。仔细研究规范中的用词可以发现,虽然在 postback 上恢复了组件树,但是那些组件使用的 backing bean 数据并没有被恢复。组件只是以字符串文字的形式存储值绑定(使用 不将值绑定数据存储在组件树中的一个最值得注意的不利方面是虚幻事件效果(见 参考资料),这是由 虽然丢失的事件看上去像是异常状况,但并不会导致 JSF 生命周期中出现红色标志。因为这些组件依赖底层数据,以保持稳定和适当地被恢复,所以 JSF 难于知道丢失了什么。 不 幸的是,JSF 规范天真地引导开发人员将大多数 backing bean 放入 conversation 作用域中 —— 甚至可以在 “方便的” 作用域内调用它。然后,服务器管理员则必须处理由此导致的 “内存溢出” 错误,集群环境中的服务器相似性,以及服务器重启时的串行化异常。MyFaces Tomahawk 项目通过 “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 容器处理异常的问题在于,这严重限制了错误页面上显示的内容,并且禁止了事务回滚。由于错误页面是在请求分发器转发之后显示的, 这一点上,Seam 再次扮演救世主,它提供了优雅的、说明性方式的异常处理。异常管理可以通过注释指定,或者在配置文件中指定。可以将注释 由于最后发行的 JSF 规范几乎与 Ajax 重合,JSF 框架在异步 JavaScript 和局部页面呈现(partial page rendering)方面帮助不大。在某些时候,甚至这两种类型的编程甚至不一致。 最终的解决办法是建议使用 JSF Seam 为 JavaScript remoting(常常记在 Ajax 名下的一种技术)提供了一种独特的方式,该方式与 Direct Web Remoting (DWR) 库的方式大致相似。Seam 通过允许 JavaScript 直接调用服务器组件上的方法,将客户机与服务器连在一起。Seam remoting 比 DWR 更强大,因为它可以访问丰富的上下文组件模型,而不仅仅是一个孤立的端点。这种交互构建在 JSF 的事件驱动设计的基础上,所以它可以更严格地遵从 Swing 范例。最妙的是,提供这个功能的同时并没有增加开发方面的负担。只需在组件的方法上加一个简单的注释
根据您目前在 无缝集成 JSF 系列 中了解到的内容,可以毫不牵强地说在使用 JSF 的开发中不使用 Seam 是反常的。作为进一步的证明,只需看看 JSR 299, Web Beans 的投票结果(见 参考资料)。 显然,在不久的将来,Seam 会成为一个官方规范,Java EE 栈最终将提供 “显著简化的基于 Web 的应用程序编程模型”。这对 JSF 开发人员和 Seam 来说是一个好消息。但是,即使没有声明要成为一个 Java 标准,Seam 也是 JSF 的一个有价值的补充。 Seam 只需很少的设置就可以开始用于 JSF —— 而正是这一点小小的付出,就能解决 JSF 开发中的一些最麻烦的难题。回报胜于付出 —— 这里讨论的 Seam 的优点还只是一个开始。
注意:
学习
获得产品和技术
讨论
|