Seam - 无缝集成 JSF,第 2 部分: 借助 Seam 进行对话

使用 Seam 构建有状态的 CRUD 应用程序

借助 Seam 开发有状态的 CRUD 应用程序是件轻而易举的事情。在 无缝 JSF 系列文章的第二篇中,Dan Allen 向您展示如何使用 Java™Server Faces (JSF) 和 Seam 为基于 Web 的高尔夫课程目录开发创建、读取、更新和删除用例。在此过程中,他突出强调了 Seam 对 JSF 生命周期的两项增强功能 —— 也就是 conversation 作用域和通过自定义 Java 5 注释进行配置 —— 并解释了其能够降低服务器负载和缩减开发时间的原因。

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 月 04 日

在这个分为三部分的系列文章的第一篇中介绍了 Seam,它是既能显著增强 JSF 功能又能实现基于组件的架构的应用程序框架。在这篇文章中,我解释了 Seam 和其他经常与 JSF 结合使用的 Web 框架的不同之处,展示了向现有 JSF 应用程序添加 Seam 是多么轻松,最后概述了 Seam 对 JSF 应用程序生命周期的增强,同时还涉及到有状态的对话、工厂组件以及使用注释进行隐秘配置。

尽管这篇文章可能引发了您对 Seam 的兴趣,但是您可能无法确信它能够改善 JSF 开发体验。集成一组新工具通常比阅读它复杂得多,并且有时候并不值得。在无缝 JSF 系列文章的第二篇文章中,您将亲自发现 Seam 是否能够实现其简化 JSF 开发的承诺。在使用 Seam 构建执行标准 CRUD 操作的简单应用程序之后,我敢肯定您会认为 Seam 是对 JSF 框架的必要扩展。结果,Seam 还能帮助降低数据库层有限资源的负载。

关于本系列

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

Open 18 应用程序

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所示:

清单 1. 配置 spring 变量解析器
<variable-resolver>
  org.springframework.web.jsf.DelegatingVariableResolver
</variable-resolver>

Seam 的上下文组件

为了体现本文的目的,我假设基于 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 类实现的(请参见 参考资料)。

清单 2. 作为 JDF 托管 bean 定义的 CourseAction
<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>

注释简化了 XML!

现在您想起了使用本地 JSF 方法 定义托管 bean 有多麻烦,请忘记曾经看到 managed-bean XML 声明 —— 因为您不再需要它了!在 Seam 构建的 应用程序中,bean 仅仅是使用 Java 5 注释声明的。Seam 将这些 bean 称为上下文组件。尽管您可能觉得该术语 很深奥,但是它只是描述一个组件(或命名实例)与给定的作用域(或称为上下文)有关。

Seam 在为上下文组件分配的作用域的生命期内对该组件进行管理。 Seam 组件更像 Spring bean,而不是 JSF 托管 bean,这是因为它们插入到复杂的、面向方面的框架。 在功能方面,Seam 框架远胜于 JSF 的基本控制反转 (IOC) 容器。 观察清单 3 中 courseAction 的声明。CourseAction 类被重构为利用 Seam 的注释。

清单 3. 作为 Seam 组件定义的 CourseAction
@Name("courseAction")
public class CourseAction {
    @In("#{courseManager}")
    private GenericManager<Course, Long> courseManager;
}

与 Spring 的深入集成

自版本 1.2 起,Seam 开始包括 Spring 的自定义 namespace handler,它允许 将 Spring bean 公开为 Seam 组件。向 Spring bean 定义添加 <seam:component /> 标记, 允许您以基本格式使用 @In 注释(来自 清单 3),而不必 显式指定值绑定表达式。在这个例子中,Seam 会将该属性的名称与 Seam 组件相匹配,现在搜索中包括公开为 Seam 组件的 Spring bean。请参见 参考资料 了解有关 Seam 1.2 新特性的更多信息。

注意所有 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 特有的元素:

清单 4. 初始课程列表视图
<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。

清单 5. 作为 DataModel 公开课程
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 在后续页面超出作用域时,仍然会丢弃该缓存。

清单 6. 作为 DataModel 公开课程,仅获取一次
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 应用程序中很常见,并且还是滥用数据库的根本原因。结合工厂组件模式使用对话使得在合适时咨询数据库成为可能,而不是为了重新获取应用程序未能跟踪的数据。

双射

双射是 Seam 对依赖项注入概念的扩展。 除了接收上下文变量来设置组件属性值之外, 双射允许将组件属性值推出目标上下文, 该操作称为 outjecting。双射与依赖项注入的不同之处 在于它是动态的、上下文相关的并且是双向的。 也就是说,双射是动态上一致的,即调用组件时进行导入和导出值操作。因此, 双射更适合有状态的组件,如 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。

Seam 使用构建函数实例化和装配组件。这些构建函数是在 bean 类中 通过注释声明的。实际上,您已经见到过其中一个例子: @Name 注释。@Name 注释告知 Seam 使用默认的 类构造函数创建新实例。要构建自己的课程列表,您不希望使用组件实例,而是 使用对象集合。为此,您希望使用 @Factory 注释。@Factory 注释向已提取变量的创建过程 附加了一个方法,这是在注释的值中指定的,当该变量没有绑定任何值时就会使 用该方法。

在清单 7 中,工厂方法 findCourses()(位于 CourseAction 类)用于初始化 courses 属性的值,该值是作为 DataModel 提取到视图中的。该工厂方法通过将这项工作委托给服务层来实例化课程对象集合。

清单 7. 使用 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 的约束。

清单 8. 使用提取的 DataModel 的课程列表视图
<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 中的序列图进行研究:

图 1. Seam 提取使用工厂方法初始化的 DataModel
Seam 工厂序列图

视图组件(如 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 的巧妙方式

遇到的第一项 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 状态。

组件ID

为 JSF 组件标记的 id 属性指定值,总是不错的主意,命名容器的属性更是如此。 如果没有为组件指派 ID,JSF 实现就会生成隐秘的 ID。 拥有有意义的 ID 有助于调试或编写访问该 DOM 的 JavaScript。

长期运行的对话

要确保回发时该课程列表仍然可用,并且 不必重新从数据库中获取该列表,就能呈现下一个响应, 则必须将当前的临时对话转变成长期运行的对话。

说服 Seam 将临时对话提升到长期运行对话的一种方式是 设置一个方法,使其在执行过程中驻留 @Begin 注释。还 必须将组件本身放到该 conversation 作用域中。通过在 CourseAction 类定义 顶部添加 @Scope(ScopeType.CONVERSATION) 注释,就可以实现。使用长期运行的对话,允许变量保持作用域直至对话结束, 而不仅仅是单个请求。对于 UIData 组件来说,这种跨多个请求的稳定性尤其重要。 (请参阅 本系列第一篇文章 中关于有状态组件的讨论,了解 数据不稳定可能对 UIData 组件的列队执行事件所造成的问题。)

您希望允许用户从课程目录中选择单个课程。要实现这项功能, 在 h:commandLink 中包装各个课程的名称,h:commandLink 将方法绑定 #{courseAction.selectCourse} 指派成操作, 如清单 9 所示。当用户单击其中一个链接时,就会触发对支持 bean 的 selectCourse() 方法的调用过程。由于 Seam 控制着注入过程,所以与该行有关的课程数据 将自动分配给带有 @DataModelSelection 注释的属性。因此,不必执行任何查找,就能使用该属性,详细信息 如清单 10 所示。

清单 9. 添加命令链接以选择课程
<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 作用域时,必须将该类序列化。

清单 10. 用于捕获所选课程的 DataModelSelection 注释
@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";
    }
}

持久化上下文

持久化上下文 是 Seam 可以管理的上下文之一。 持久化上下文是借助 Hibernate 或 JPA 从数据库加载的所有对象的标识作用域和 内存缓存。与 Spring 提倡的无状态架构相比,Seam 的创建者推荐使用作用域为对话的组件, 并使该组件跨多个请求保留持久化上下文。无状态模型引入的问题是: 关闭持久化上下文时,所有加载的对象进入 “隔离” 状态, 无法再担保这些对象的标识是否正确。 结果导致数据库和开发人员不得不解决跨持久化上下文会话的对象是否相等。 本文中没有利用托管的持久化上下文。请参阅 参考资料 了解更多内容。

对话的优点

清单 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,以表明该提取是有 条件的。

命名循环变量

h:dataTable 选择临时循环变量名时, 必须谨慎,不能使其与其他提取变量发生冲突。在所有示例中, 我指定循环变量名称时使用了下划线前缀。该前缀不仅 可以防止与提取的课程发生冲突,还有助于说明该变量限定了作用域。 上下文变量存储在给定作用域的共享映射中,因此为其选择名称时 一定要谨慎!

默认情况下,@Out 注释反映了 用于确定上下文变量名称的属性名称。如果您认为更合适的话, 可以选择为提取的变量使用不同名称。因为课程数据将被提取 到 conversation 作用域,并且可能在后续的一些请求中使用, 所以该名称的 “所选” 特征失去了原来的意义。在这种情况下, 最好使用实体本身的名称。因此,selectedCourse 属性的推荐注释为 @Out(value="course", required=false)

可以在新页面上显示课程详细内容,也可以 显示在同一页面的表格下面。为了演示的目的,在同一页面显示详细内容,同时限制要构造的视图数目。要在另一个页面中 访问提取的变量,不需要额外的工作或特殊技巧。

修订过的支持 bean

该支持 bean 的上一个版本 的差别不大,因此,清单 11 仅突出显示了两者的不同之处。selectedCourse 属性现在有两个注释。selectCourse() 方法也被稍加整理。 现在它在继续呈现视图之前重新提取该课程对象。在无状态的设计中, 必须确保完全由数据层填充对象,并且正确地初始化任何与显示其详细 内容有关的延迟加载。

清单 11. 将所选课程提取到视图
    // ...

    @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 下面的详细内容面板:

清单 12. 有条件地为所选课程显示课程详细内容
<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 验证的琐碎工作!

借助 Seam 和 Hibernate 进行验证

如果您将整个表单包装到一个 s:validateAll 组件标记中, Seam 允许您在 JSF 的流程验证(Process Validation)阶段执行对数据模型 定义的验证。这种验证方法比以下方法更有吸引力: 在视图中到处设置 JSF 验证器标记,或者维护一个配置文件, 写满针对第三方验证框架的验证定义。取而代之, 可以使用 Hibernate Validator 注释向实体类属性指派验证标准, 如清单 13 所示。然后 Hibernate 在持久化对象时,对验证进行两次检查,为您提供双重保护。这个双重保障方法意味着 视图中不小心出现的 bug 没有任何机会危害您的数据质量。(请参阅 参考资料 了解关于 Hibernate Validator 的更多内容。)

清单 13. 带有 Hibernate 验证注释的课程实体
@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 所示:

清单 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 枚举类型。

清单 15. 课程编辑器视图
<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 中所示:

清单 16. 向 deleteCourse 添加命令链接
<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 以前的阴影。

清单 17. 向 deleteCourse 添加操作方法
    // ...

    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 所示:

清单 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 库如何指导浏览器和服务器端组件之间的通信。

再见,同时祝您玩高尔夫愉快!


下载

描述名字大小
open 18 样例应用程序 —— 第 1 阶段1j-seam2.zip248KB

注意:

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

参考资料

学习

获得产品和技术

  • JBoss Seam:下载完整的发行版,包括附带的例子应用程序。
  • Hibernate:初步了解 Hibernate Validator。
  • Apache Geronimo:下载 Java EE 5 版本,然后利用 Seam 的 EJB3 集成功能。
  • 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=228248
ArticleTitle=Seam - 无缝集成 JSF,第 2 部分: 借助 Seam 进行对话
publish-date=06042007