Seam - 语境相关的组件 [满江红20080327]

Java EE框架

2.0GA


Table of Contents

JBoss Seam简介
1. Seam 入门
1.1. 试试看
1.1.1. 在JBoss AS上运行示例
1.1.2. 在Tomcat服务器上运行示例
1.1.3. 运行测试
1.2. 第一个例子:注册示例
1.2.1. 了解代码
1.2.1.1. 实体Bean:User.java
1.2.1.2. 无状态会话Bean:RegisterAction.java
1.2.1.3. 会话Bean的本地接口:Register.java
1.2.1.4. Seam组件部署描述文件:components.xml
1.2.1.5. Web部署描述文件:web.xml
1.2.1.6. JSF配置:faces-config.xml
1.2.1.7. EJB部署描述文件:ejb-jar.xml
1.2.1.8. EJB持久化部署描述文件:persistence.xml
1.2.1.9. 视图:register.xhtml 和 registered.xhtml
1.2.1.10. EAR部署描述文件:application.xml
1.2.2. 工作原理
1.3. Seam中的可点击列表:消息示例
1.3.1. 理解代码
1.3.1.1. 实体Bean:Message.java
1.3.1.2. 有状态的会话Bean:MessageManagerBean.java
1.3.1.3. 会话Bean的本地接口:MessageManager.java
1.3.1.4. 视图:messages.jsp
1.3.2. 工作原理
1.4. Seam和jBPM:待办事项列表(todo list)示例
1.4.1. 理解代码
1.4.2. 工作原理
1.5. Seam页面流:猜数字范例
1.5.1. 理解代码
1.5.2. 工作原理
1.6. 一个完整的Seam应用程序:宾馆预订范例
1.6.1. 介绍
1.6.2. 预订系统概况
1.6.3. 理解Seam业务对话(Conversation)
1.6.4. Seam的UI控制库
1.6.5. Seam调试页面
1.7. 一个使用Seam和jBPM的完整范例:DVD商店
1.8. 结合Seam和Hibernate的范例:Hibernate预订系统
1.9. 一个RESTful的Seam应用程序:Blog范例
1.9.1. 使用“拉”风格的MVC
1.9.2. 可收藏的搜索结果页面
1.9.3. 在RESTful应用程序中使用“推”风格("push"-style)的MVC
2. 用Seam-gen起步
2.1. 准备活动
2.2. 建立一个新的Eclipse项目
2.3. 创建新动作
2.4. 创建有动作的表单(form)
2.5. 从现有数据库生成应用程序
2.6. 将应用部署为EAR
2.7. Seam与增量热部署
2.8. 在Jboss 4.0下使用Seam
2.8.1. 安装JBoss 4.0
2.8.2. 安装JSF 1.2 RI
3. 上下文相关的组件模型
3.1. Seam上下文
3.1.1. Stateless context(无状态上下文)
3.1.2. Event context(事件上下文)
3.1.3. Page context(页面上下文)
3.1.4. Conversation context(业务会话上下文)
3.1.5. Session context(Session上下文)
3.1.6. Business process context (业务流程上下文)
3.1.7. Application context(应用上下文)
3.1.8. Context variables(上下文变量)
3.1.9. Context搜索优先级
3.1.10. 并发模型
3.2. Seam 组件
3.2.1. 无状态Session Bean
3.2.2. 有状态Session Bean
3.2.3. 实体Bean
3.2.4. JavaBeans
3.2.5. 消息驱动Bean
3.2.6. 拦截
3.2.7. 组件名字
3.2.8. 定义组件范围(Defining the Component Scope)
3.2.9. 具有多个角色的组件(Components with multiple roles)
3.2.10. 内置组件
3.3. 双向注入
3.4. Lifecycle methods(生命周期方法)
3.5. 条件装载(Conditional installation)
3.6. 日志
3.7. Mutable接口和@ReadOnly
3.8. Factory和Manager组件
4. 配置Seam组件
4.1. 通过属性设置来配置组件
4.2. 通过 components.xml 来配置组件
4.3. 细粒度的配置文件
4.4. 可配置的属性类型
4.5. 使用XML命名空间
5. 事件、拦截器和异常处理
5.1. Seam事件
5.1.1. 页面动作
5.1.1.1. 页面参数
5.1.1.2. 导航
5.1.1.3. 导航的定义、页面动作和参数的细粒度文件
5.1.2. 组件驱动的事件
5.1.3. 上下文事件
5.2. Seam 拦截器
5.3. 管理异常
5.3.1. 异常和事务
5.3.2. 激活Seam异常处理
5.3.3. 使用注解处理异常
5.3.4. 用XML处理异常
5.3.5. 一些常见的异常
6. 对话以及工作区管理
6.1. Seam的对话模型
6.2. 嵌套对话
6.3. 使用GET请求来开始一个对话
6.4. 利用<s:link>以及<s:button>
6.5. 成功信息
6.6. 使用“显式”的对话id
6.7. 工作区管理
6.7.1. 工作区管理及JSF导航
6.7.2. 工作区管理和jPDL页面流
6.7.3. 对话转换器
6.7.4. 对话列表
6.7.5. 导航控件
6.8. 对话组件和JSF组件绑定
6.9. 对话组件的并发调用
6.9.1. RichFaces Ajax
7. 页面流和业务流程
7.1. Seam中的页面流
7.1.1. 两种导航模型
7.1.2. Seam和后退按钮
7.2. 使用jPDL页面流
7.2.1. 安装页面流
7.2.2. 开始页面流
7.2.3. 页面节点和跳转
7.2.4. 流程控制
7.2.5. 流程的结束
7.2.6. 页面流组合
7.3. Seam中的业务流程管理
7.4. 使用jPDL业务流程定义
7.4.1. 安装流程定义
7.4.2. 初始化Actor id
7.4.3. 启动一个业务流程
7.4.4. 任务分配
7.4.5. 任务列表
7.4.6. 执行一个任务
8. Seam和对象/关系映射
8.1. 简介
8.2. Seam管理的事务
8.2.1. 关闭Seam管理的事务
8.2.2. 配置Seam事务管理器
8.2.3. 事务同步
8.3. Seam管理的持久化上下文
8.3.1. 在Seam管理的持久化上下文中使用JPA
8.3.2. 使用Seam管理的Hibernate会话
8.3.3. Seam管理的持久化上下文和原子会话
8.4. 使用JPA “代理(delegate)”
8.5. 在EJB-QL/HQL中使用EL
8.6. 使用Hibernate过滤器
9. Seam中的JSF表单验证
10. Groovy集成
10.1. Groovy简介
10.2. 用Groovy编写Seam应用
10.2.1. 编写Groovy组件
10.2.1.1. 实体
10.2.1.2. Seam组件
10.2.2. seam-gen
10.3. 部署
10.3.1. 部署Groovy代码
10.3.2. 开发时部署本地.groovy文件
10.3.3. seam-gen
11. Seam应用程序框架
11.1. 简介
11.2. Home对象
11.3. Query对象
11.4. Controller对象
12. Seam和JBoss规则
12.1. 安装规则
12.2. 在Seam组件中使用规则
12.3. 在jBPM流程定义中使用规则
13. 安全
13.1. 概述
13.1.1. 哪种模式更适合我的应用程序呢?
13.2. 需求
13.3. 取消安全
13.4. 验证
13.4.1. 配置
13.4.2. 编写验证方法
13.4.2.1. Identity.addRole()
13.4.3. 编写登录表单
13.4.4. 简化配置 - 概述
13.4.5. 处理安全异常
13.4.6. 登录重定向
13.4.7. HTTP验证
13.4.7.1. 编写Digest验证者
13.4.8. 高级验证特性
13.4.8.1. 使用容器的JAAS配置
13.5. 错误消息
13.6. 授权
13.6.1. 核心概念
13.6.2. 保护组件
13.6.2.1. @Restrict注解
13.6.2.2. 行内限制
13.6.3. 用户界面中的安全
13.6.4. 保护页面
13.6.5. 保护实体
13.6.5.1. 使用JPA的实体安全
13.6.5.2. 使用Hibernate的实体安全
13.7. 编写安全规则
13.7.1. 许可概述
13.7.2. 配置规则文件
13.7.3. 创建安全规则文件
13.7.3.1. 通配符许可检查
13.8. SSL安全
13.9. 实现Captcha测试
13.9.1. 配置Captcha Servlet
13.9.2. 添加Captcha到页面
13.9.3. 定制Captcha图片
14. 国际化和主题
14.1. 本地化
14.2. 标签
14.2.1. 定义标签
14.2.2. 标签显示
14.2.3. Faces Messages
14.3. 时区
14.4. 主题
14.5. 使用cookie保存locale和主题设置
15. Seam Text
15.1. 基本格式化
15.2. 输入代码和有特殊字符的文本
15.3. 链接
15.4. 输入HTML
16. iText PDF生成
16.1. 使用PDF支持
16.1.1. 创建一个文档
16.1.2. 基本的文本元素
16.1.3. 页眉和页脚
16.1.4. 章节
16.1.5. 列表
16.1.6. 表格
16.1.7. 文档常量
16.1.7.1. 颜色值
16.1.7.2. 对齐方式值
16.1.8. iText配置
16.2. 图表
16.3. 柱状图编码
16.4. 更详细的文档
17. 电子邮件
17.1. 创建一条消息
17.1.1. 附件
17.1.2. HTML/Text 交替部分
17.1.3. 多个收件人
17.1.4. 多条信息
17.1.5. 模板
17.1.6. 国际化
17.1.7. 其它的标识头
17.2. 接收邮件
17.3. 配置
17.3.1. mailSession
17.3.1.1. 在JBoss AS中查找JNDI
17.3.1.2. Seam配置会话
17.4. Meldware
17.5. 标签
18. 异步和消息
18.1. 异步
18.1.1. 异步方法
18.1.2. 包含Quartz Dispatcher的异步方法
18.1.3. 异步事件
18.2. Seam中的消息
18.2.1. 配置
18.2.2. 发送消息
18.2.3. 利用消息驱动Bean接收消息
18.2.4. 在客户端接收消息
19. 缓存
19.1. 在Seam中使用JBossCache
19.2. 页片段缓存
20. Web Services
20.1. 配置和打包
20.2. 对话的Web Services
20.2.1. 建议策略
20.3. Web Servic范例
21. Remoting
21.1. 配置
21.2. Seam对象
21.2.1. Hello World示例
21.2.2. Seam.Component
21.2.2.1. Seam.Component.newInstance()
21.2.2.2. Seam.Component.getInstance()
21.2.2.3. Seam.Component.getComponentName()
21.2.3. Seam.Remoting
21.2.3.1. Seam.Remoting.createType()
21.2.3.2. Seam.Remoting.getTypeName()
21.3. EL表达式求值
21.4. 客户端接口
21.5. 上下文
21.5.1. 设置和读取对话ID
21.5.2. 当前对话范围内的远程调用
21.6. 批量请求
21.7. 使用数据类型
21.7.1. 原生 / 基本 类型
21.7.1.1. String
21.7.1.2. Number
21.7.1.3. Boolean
21.7.2. JavaBeans
21.7.3. Date和Time
21.7.4. Enums 枚举类型
21.7.5. Collections 集合
21.7.5.1. Bags
21.7.5.2. Maps
21.8. 调试
21.9. 加载消息
21.9.1. 修改信息
21.9.2. 隐藏加载信息
21.9.3. 自定义加载指示器
21.10. 控制返回数据
21.10.1. 一般字段的约束
21.10.2. 集合和映射的约束
21.10.3. 特定类型对象的约束
21.10.4. 组合约束
21.11. JMS消息
21.11.1. 配置
21.11.2. 订阅JMS主题
21.11.3. 退订主题
21.11.4. 调整轮询过程
22. Seam和Google的Web工具包(GWT)
22.1. 配置
22.2. 准备你的组件
22.3. 将GWT小组件接到Seam组件
22.4. GWT Ant Targets
23. Spring Framework集成
23.1. 把Seam组件注入Spring Bean中
23.2. 将Spring Bean注入到Seam组件中
23.3. 将Spring Bean转换为Seam组件
23.4. Seam作用域的Spring Bean
23.5. 使用Spring PlatformTransactionManagement
23.6. 在Spring中使用Seam管理的持久化上下文
23.7. 在Spring中使用Seam管理的Hibernate会话
23.8. 作为Seam组件的Spring应用上下文
23.9. 使用Spring TaskExecutor的@Asynchronous
24. Hibernate Search
24.1. 简介
24.2. 配置
24.3. 用法
25. Seam配置和Seam应用程序打包
25.1. Seam基本配置
25.1.1. 将Seam与JSF和servlet容器集成
25.1.2. 使用Facelets
25.1.3. Seam Resource Servlet
25.1.4. Seam Servlet过滤器
25.1.4.1. 异常处理
25.1.4.2. 通过重定向传播对话
25.1.4.3. 多重表单提交
25.1.4.4. 字符编码
25.1.4.5. RichFaces
25.1.4.6. Identity Logging
25.1.4.7. 定制Servlet的上下文管理
25.1.4.8. 增加定制的过滤器
25.1.5. 将Seam与你的EJB容器集成
25.1.6. 切记!
25.2. 在Java EE 5中配置Seam
25.2.1. 打包
25.3. 在J2EE中配置Seam
25.3.1. 在Seam中引导Hibernate
25.3.2. 在Seam中引导JPA
25.3.3. 打包
25.4. 在Java SE中配置Seam,没有内嵌JBoss
25.5. 用嵌入式的JBoss在Java SE中配置Seam
25.5.1. 安装嵌入式的JBoss
25.5.2. 打包
25.6. 在Seam中配置jBPM
25.6.1. 打包
25.7. 在Portal中配置Seam
25.8. 在JBoss AS中配置SFSB和会话超时
26. Seam on OC4J
26.1. jee5/booking 实例
26.1.1. 预订酒店实例的依赖包
26.1.2. OC4J需要的额外依赖包
26.1.3. 配置文件的改变
26.1.4. 构建 jee5/booking 实例
26.2. 部署Seam应用程序到OC4J中
26.3. 将一个使用 seam-gen 创建的应用程序部署到OC4J中。
26.3.1. seam-gen之类的应用程序的OC4J部署描述符
27. Seam注解
27.1. 用于定义组件的注解
27.2. 用于双向注入的注解
27.3. 关于组件生命周期方法的注解
27.4. 用于声明上下文的注解
27.5. 用于在J2EE环境中使用Seam JavaBean组件的注解
27.6. 用于异常的注解
27.7. 用于Seam Remoting 的注解
27.8. 用于Seam拦截器(interceptor)的注解
27.9. 用于异步(asynchronicity)的注解
27.10. 用于JSF的注解
27.10.1. 和 dataTable 一起使用的注解
27.11. 用于数据绑定的元数据注解
27.12. 用于打包(packing)的注解
27.13. 用于和Servlet容器集成的注解
28. 内置Seam组件
28.1. 上下文注入组件
28.2. 工具组件
28.3. 组件的国际化和主题
28.4. 控制对话组件
28.5. 与jBPM相关的组件
28.6. 与安全相关的组件
28.7. 与JMS相关的组件
28.8. 与邮件相关的组件
28.9. 基础组件
28.10. 杂项组件
28.11. 特殊组件
29. Seam的JSF控件
29.1. 标签
29.2. 注解
30. 表达式语言增强
30.1. 参数方法绑定
30.1.1. 用法
30.1.2. 限制
30.1.2.1. 与JSP 2.1不兼容
30.1.2.2. 从Java代码中调用 MethodExpression
30.2. 参数值绑定
30.3. 映射
31. 测试Seam应用程序
31.1. Seam组件的单元测试
31.2. Seam组件的集成测试
31.2.1. 在集成测试中使用Mock对象
31.3. 集成测试Seam应用程序中的用户交互
31.3.1. 利用Mock数据进行集成测试
31.3.2. Seam Mail集成测试
32. Seam工具
32.1. jBPM设计器和查看器
32.1.1. 业务流程设计器
32.1.2. Pageflow查看器
33. 依赖包
33.1. 项目依赖包
33.1.1. Core
33.1.2. Ajax4JSF / RichFaces
33.1.3. Seam Mail
33.1.4. Seam PDF
33.1.5. JBoss Rules
33.1.6. JBPM
33.1.7. GWT
33.1.8. Spring
33.1.9. Groovy
33.2. 使用Maven依赖管理
A. Seam 2.0 开发手册中文翻译项目
A.1. 声明
A.2. 项目历程
A.2.1. Seam 1.2.1 开发手册翻译项目
A.2.2. Seam 2.0 Beta 1 开发手册翻译项目
A.2.3. Seam 2.0 正式版开发手册翻译项目

JBoss Seam简介

Seam是一种企业级Java的应用程序框架。它的灵感源自下列原则:

只有一种“工具”

Seam为你的应用程序中所有的业务逻辑定义了一种统一的组件模型。 Seam组件可能是有状态的,包含与几个定义良好的上下文中任何一个相关联的状态, 包括长时间运行上下文、持久化上下文、业务流程上下文, 以及用户交互中能够跨多个Web请求保存的对话上下文

Seam中的表现层组件和业务逻辑组件之间并没有区别。 你可以根据你设计的任何架构给应用程序进行分层,而不是强制将你的应用程序逻辑硬塞进一个由你目前在使用的任何框架组合所强加给你的不适当的分层配置中。

与简单的Java EE或者J2EE组件不同,Seam组件可以同时访问与Web请求相关的状态,以及保存在事务资源中的状态(而不必通过方法参数手工传播Web请求状态)。 你可能反对说由旧式的J2EE平台强加给你的应用程序分层是件好东西,没有什么可以阻止你利用Seam创建一个相当的分层架构— 区别在于,要自己架构应用程序,并决定有哪些层,以及它们是如何合作的。

将JSF与EJB 3.0整合

JSF和EJB 3.0是Java EE5的两个最好的新特性。EJB3是服务器端业务和持久逻辑的全新组件模型。 同时,JSF也是表现层的一个优秀组件模型。不幸的是,这二者都无法独自解决所有的计算问题。 实际上,JSF和EJB3结合使用后运作得最好。 但是Java EE5规范并没有提供如何整合这两个组件模型的标准方法。 所幸,这两种模型的创建者都前瞻到了这种状况,并且提供了标准的扩展点,允许对各自进行扩展,或者与其他解决方案集成。

Seam将JSF和EJB3的组件模型合二为一,消除了胶合代码,使得开发者专注于业务问题。

编写“一切”都是EJB的Seam应用程序是有可能的。如果你习惯把EJB当作是细粒度的所谓“重量化”的对象,这可能会令你很吃惊。 然而,从开发人员的角度来看,3.0版本已经完全改变了EJB的本质。 EJB是一个细粒度的对象—没有什么东西会比注解的JavaBean更复杂了。Seam甚至鼓励你使用会话Bean作为JSF动作监听者!

另一方面,如果你宁可不在这个时候采用EJB 3.0,不用勉强。 事实上,任何Java类都可以是一个Seam组件,并且Seam提供了你期待从“轻量化”的容器,甚至任何组件、EJB或者其他东西中获得的所有功能。

集成AJAX

Seam支持两个最好的、开源的基于JSF的AJAX解决方案:JBoss RichFaces和ICEfaces。 这两个解决方案让你无需编写任何JavaSctipt代码就可以为你的界面添加AJAX功能。

Seam也提供了内置的JavaSctipt远程访问层,它让你异步地从客户端JavaScript中调用组件,而不需要中间的action层。 你还可以订阅服务器端的JMS主题,并通过AJAX的push方法接收信息。

假若不是有Seam内置的并发和状态管理能力,以上这些方法将都无法很好地运作。 这两种方法确保服务器端能够安全而高效地处理多个并发细粒度的异步AJAX请求。

将业务流程作为首要的基础建筑

Seam可以选择通过jBPM提供透明的业务流程管理。使用jBPM和Seam共同实现复杂的工作流、合作和任务管理,简单到了让人难以置信的程度。

Seam甚至允许你利用与jBPM给业务流程定义所使用的相同语言(jPDL)来定义表现层页面流。

JSF为表示层提供了非常丰富的事件模型。 通过以完全相同的事件处理机制暴露与jBPM业务流程相关的事件,Seam强化了这一模型,这就为Seam的统一组件模型提供了统一的事件模型。

声明式状态管理

从EJB早期开始,我们已经习惯于声明式事务管理和J2EE声明式安全的概念。EJB 3.0还引入了声明式持久上下文管理。 一个更广泛的管理状态问题--管理与某个特殊context关联的状态,有三个特例,它确保这个上下文结束时进行所有必要的清理。 Seam把声明式状态管理的概念推进的远得多,并把它应用于应用程序状态(application state)。 J2EE应用程序一般通过手工实现状态管理,通过获取和设置Servlet Session和Request属性。 假若程序没能清除Session属性,或者在多窗口的应用程序中,与不同的工作流关联的Session数据发生冲突,这种状态管理的方法就会成为很多Bug和内存泄漏的根源。 Seam有可能几乎完全消除这类Bug。

声明式应用程序状态管理通过Seam定义的丰富的context model(上下文模型)而成为可能。 Seam扩展了Servlet规范—定义的上下文模型——请求、会话、应用程序—增加了两个新的上下文— 对话和业务流程—,从业务逻辑的角度来看它们更具意义。

一旦你开始使用对话,将会惊讶于许多事情变得更加容易了。你曾经在像Hibernate或者JPA这样的ORM解决方案中痛苦地处理过延迟的关联抓取吗? Seam对话范围的持久化上下文意味着你将几乎看不到 LazyInitializationException。 你曾经遇到过刷新(Refresh)按钮或者后退(Back)按钮的问题吗?或者有过重复提交表单的问题吗? 有通过post-then-redirect传播信息的问题吗?Seam的对话管理解决了这些问题,甚至无需你真正去关注它们。 它们都是自Web最早期以来普遍的不良状态管理架构的征兆。

Bijection(双向注入)

Inversion of Control(控制反转) 或者 dependency injection(依赖注入) 的概念出现在JSF和EJB3以及很多所谓的“轻型容器”中。 这类容器大多注重于实现 stateless services(无状态服务) 的组件注射。 即便在支持对有状态的组件进行注射的情况下(例如JSF),事实上也难以用于处理应用程序状态, 因为有状态组件的范围难以有效而灵活地定义,并且属于更广范围的组件不能被注入到属于更窄范围的组件中。

Bijection(双向注入)和IoC的不同之处在于它是动态的、语境相关的以及双向的。 你可以把这一机制理解成将语境相关的变量(与当前线程绑定的各种上下文中的名称)对应到组件的属性中。 双向注入允许由容器对有状态的组件进行自动组装。它甚至允许组件可以安全而简单地处理上下文变量的值,只需要把它赋给组件的属性。

工作区管理(Workspace Management)和多窗口浏览

Seam应用程序让用户自由地在多个浏览器窗口中切换,每个窗口都与一个不同的、安全隔离的对话关联。 应用程序甚至可以利用 workspace management,允许用户在一个浏览器窗口的多个对话(工作区)之间进行切换。 Seam不仅提供正确的多窗口操作,还提供在一个窗口中模拟多个窗口的操作。

更喜欢XML注解

传统上,关于到底哪些元信息可以算作配置,Java社区一直处于一种极为混乱的状态。 J2EE和流行的“轻型”容器为真正可以在不同的系统部署之间配置的东西,以及任何不容易用Java表达的其他声明都提供了基于XML的部署描述符。 Java 5 注解改变了所有这一切。

EJB3.0 接受注解和“对例外配置”,这成了以声明的形式为容器提供信息的最简易方法。不幸的是,JSF仍然在十分依赖笨重的XML配置文件。 Seam扩展了EJB 3.0 提供的注解,以用于声明式状态管理和声明式上下文划分。 这让你摆脱了对繁琐的JSF managed bean(JSF受管bean)的配置,减少了所需的XML,只剩下那些真正属于XML的信息(JSF导航规则)。

集成测试轻而易举

Seam组件作为POJO,天生就是可以进行单元测试的。但是对于复杂的应用程序,只有单元测试则还不够。 对于Java Web应用程序来说,集成测试一般是一项笨拙且困难的任务。因此,Seam为Seam应用程序提供了可测试性作为该框架的一项核心功能。 你可以轻易地编写重现与用户完整交互的JUnit或TestNG测试,来演习除了视图View(JSP或者Facelets页面)之外的所有系统组件。 你可以直接从你的IDE中运行这些测试,Seam会在那里自动地利用JBoss Embeddable部署EJB组件。

规范也非尽善尽美

我们认为最新的Java EE规范很不错。但是我们知道它还远不够完美。 在规范中有许多漏洞(例如,GET请求的JSF生命周期中的局限性),Seam修正了这些漏洞。 Seam的创建者们正与JCP专家组一道,确保这些修正恢复到标准的下一次修订中。

Web应用程序不只是服务HTML页面

当今的Web框架认为太小了。它们让你叫用户输入表单,并进入到你的Java对象。然后它们就让你悬着。 真正完整的Web应用程序框架应该解决像持久化、并发、异步、状态管理、安全、电子邮件、信息、PDF和图表生成、工作流、wikitext渲染、Web Services、缓存等等更多的问题。 一旦你尝到了Seam的甜头,就会惊讶地发现许多问题都变得更加简单了......

Seam为持久化集成了JPA和Hibernate 3,为轻量化的异步性集成了EJB Timer Service和Quartz,为工作流集成了jBPM,为业务规则集成了JBoss规则,为电子邮件集成了Meldware Mail,为完整的文本搜索集成了Hibernate Search和Lucene,为消息集成了JMS,以及为页面片断捕捉集成了JBoss Cache。 Seam在JAAS和JBoss规则之上,创建了一个新的基于规则的安全框架。甚至有用来渲染PDF、在线电子邮件和图表及wikitext的JSF标签库。 Seam组件可以同时作为一个Web Service进行调用,异步地从客户端JavaScript或者Google Web Toolkit,或者当然也可以直接从JSF调用。

立刻开始吧!

Seam在任何Java EE应用程序服务器中都可以运行,甚至在Tomcat中也可以。如果你的环境支持EJB 3.0,好极了! 如果不支持,也没关系,你可以使用Seam为持久化内置的包含JPA或者Hibernate3的事务管理。 或者,你可以在Tomcat中部署JBoss Embedded,同时享有对EJB 3.0 的完整支持。

最终你会发现,Seam、JSF和EJB3的组合就是用Java编写复杂Web应用程序的 简单办法。你不会相信所需的代码是多么地少!

本文档翻译由俞黎敏作为Leader组织。翻译及一审、二审名单见下。王琳、马越对全书进行了三审。俞黎敏进行了全书统稿及发布的build工作。

Table 1. 翻译及审核人员列表

章节KB页数翻译一审二审
master.xml 17K CaoXiaogangEchoYuLimin
1. tutorial.xml (1.1-1.4)130K20PseanchanJackyDigitalSonic
       (1.5-1.11)  24P DigitalSonic Jacky ronghao
2. gettingstarted.xml 21K 6P seanchan zaya DigitalSonic
3. concepts.xml 56K 16P CaoXiaogang kuuyee ronghao
4. xml.xml 25K 7P downpour Echo YuLimin
5. events.xml 39K 11P mochow xihuyu2000 Echo
6. conversations.xml 34K 10P magice Echo YuLimin
7. jbpm.xml 32K 10P 差沙 ronghao Echo
8. persistence.xml 23K 6P pesome caoer DigitalSonic
9. validation.xml 9K 4P pesome DigitalSonic YuLimin
10. groovy.xml 11K 4P kuuyee DigitalSonic YuLimin
11. framework.xml 20K 7P alexchang CaoXiaogang YuLimin
12. drools.xml 7K 3P DigitalSonic shaozhou Echo
13. security.xml 51K 14P YuLimin xihuyu2000 Echo
14. i18n.xml 14K 4P YY DigitalSonic Echo
15. text.xml 7K 3P DigitalSonic yeshucheng(万国辉) Echo
16. itext.xml 51K 11P lyfcdy Echo YuLimin
17. mail.xml 26K 7P chentianyi yeshucheng(万国辉) Echo
18. jms.xml 11K 5P YuLimin caoer Echo
19. cache.xml 11K 3P crazycy CaoXiaogang DigitalSonic
20. webservices.xml 9K 3P Echo YuLimin Echo
21. remoting.xml 37K 13P crazycy agile_boy Echo
22. gwt.xml 10K 4P yeshucheng(万国辉) Echo YuLimin
23. spring.xml 13K 4P YY caoer DigitalSonic
24. hsearch.xml 7K 3P yeshucheng(万国辉) agile_boy Echo
25. configuration.xml 48K 15P yeby kuuyee Echo
26. annotations.xml 64K 14P caoer CaoXiaogang YuLimin
27. components.xml 68K 11P jiaochar zaya HuYan
28. controls.xml 47K 13P Echo YuLimin Catherine
29. elenhancements.xml 5K 2P CaoXiaogang yeshucheng(万国辉) Echo
30. testing.xml 10K 6P agile_boy CaoXiaogang YuLimin
31. tools.xml 23K 9P junjzheng CaoXiaogang Echo
32. oc4j.xml 31K 8P yeshucheng(万国辉) YuLimin Echo
33. dependencies.xml 26K 5P yeshucheng(万国辉) DigitalSonic YuLimin

Chapter 1. Seam 入门

1.1. 试试看

本教程假定你已下载JBoss AS 4.0.5并安装了EJB 3.0 profile(请使用JBoss AS安装器)。你也得下载一份Seam并解压到工作目录上。

各示例的目录结构仿效以下形式:

  • 网页、图片及样式表可在 examples/registration/view 目录中找到。

  • 诸如部署描述文件及数据导入脚本之类的资源可在目录 examples/registration/resources 中找到。

  • Java源代码保存在 examples/registration/src 中。

  • Ant构建脚本放在 examples/registration/build.xml 文件中。

1.1.1. 在JBoss AS上运行示例

第一步,确保已安装Ant,并正确设定了 $ANT_HOME$JAVA_HOME 的环境变量。接着在Seam的根目录下的 build.properties 文件中正确设定JBoss AS 4.0.5的安装路径。 若一切就绪,就可在JBoss的安装根目录下敲入 bin/run.shbin/run.bat 命令来启动JBoss AS。(译注:此外,请安装JDK1.5以上以便能直接运行示例代码)

现在只要在Seam安装目录 examples/registration 下输入 ant deploy 就可构建和部署示例了。

试着在浏览器中访问此链接:http://localhost:8080/seam-registration/

1.1.2. 在Tomcat服务器上运行示例

首先,确保已安装Ant,并正确设定了 $ANT_HOME$JAVA_HOME 的环境变量。接着在Seam的根目录下的 build.properties 文件中正确设定Tomcat 6.0的安装路径。你需要按照25.5.1章节“安装嵌入式的Jboss”中的指导配置 (当然, SEAM也可以脱离Jboss在TOMCAT上直接运行)。

至此,就可在Seam安装目录 examples/registration 中输入 ant deploy.tomcat 构建和部署示例了。

最后启动Tomcat。

试着在浏览器中访问此链接:http://localhost:8080/jboss-seam-registration/

当你部署示例到Tomcat时,任何的EJB3组件将在JBoss的可嵌入式的容器,也就是完全独立的EJB3容器环境中运行。

1.1.3. 运行测试

几乎所有的示例都有相应的TestNG的集成测试代码。最简便的运行测试代码的方法是在 examples/registration目录中运行 ant testexample。当然也可在IDE开发工具中使用TestNG插件来运行测试。

1.2. 第一个例子:注册示例

注册示例是个极其普通的应用,它可让新用户在数据库中保存自己的用户名,真实的姓名及密码。 此示例并不想一下子就把Seam的所有的酷功能全部秀出。然而, 它演示了EJB3 会话Bean作为JSF动作监听器及Seam的基本配置的使用方法。

或许你对EJB 3.0还不太熟悉,因此我们会对示例的慢慢深入说明。

此示例的首页显示了一个非常简单的表单,它有三个输入字段。试着在表单上填写内容并提交,一旦输入数据被提交后就会在数据库中保存一个user对象。

1.2.1. 了解代码

本示例由两个JSP页面,一个实体Bean及无状态的会话Bean来实现。

让我们看一下代码,就从最“底层”的实体Bean开始吧。

1.2.1.1. 实体Bean:User.java

我们需要EJB 实体Bean来保存用户数据。这个类通过注解声明性地定义了 persistencevalidation 属性。它也需要一些额外的注解来将这个类定义为Seam的组件。

Example 1.1. 

@Entity                                                                                  (1)
@Name("user")                                                                            (2)
@Scope(SESSION)                                                                          (3)
@Table(name="users")                                                                     (4)
public class User implements Serializable
{
   private static final long serialVersionUID = 1881413500711441951L;

   private String username;                                                              (5)
   private String password;
   private String name;

   public User(String name, String password, String username)
   {
      this.name = name;
      this.password = password;
      this.username = username;
   }

   public User() {}                                                                      (6)

   @NotNull @Length(min=5, max=15)                                                       (7)
   public String getPassword()
   {
      return password;
   }

   public void setPassword(String password)
   {
      this.password = password;
   }

   @NotNull
   public String getName()
   {
      return name;
   }

   public void setName(String name)
   {
      this.name = name;
   }

   @Id @NotNull @Length(min=5, max=15)                                                   (8)
   public String getUsername()
   {
      return username;
   }

   public void setUsername(String username)
   {
      this.username = username;
   }

}
(1)

EJB3标准注解 @Entity 表明了 User 类是个实体Bean.

(2)

Seam组件需要一个 组件名称,此名称由注解 @Name来指定。此名称必须在Seam应用内唯一。当JSF用一个与组件同名的名称去请求Seam来解析上下文变量, 且该上下文变量尚未定义(null)时,Seam就将实例化那个组件,并将新实例绑定给上下文变量。 在此例中,Seam将在JSF第一次遇到名为 user 的变量时实例化 User

(3)

每当Seam实例化一个组件时,它就将始化后的实例绑定给组件中 默认上下文 的上下文变量。默认的上下文由 @Scope注解指定。 User Bean是个会话作用域的组件。

(4)

EJB标准注解@Table 表明了将 User 类映射到 users 表上。

(5)

namepasswordusername 都是实体Bean的持久化属性。所有的持久化属性都定义了访问方法。当JSF渲染输出及更新模型值阶段时需要调用该组件的这些方法。

(6)

EJB和Seam都要求有空的构造器。

(7)

@NotNull@Length 注解是Hibernate Validator框架的组成部份, Seam集成了Hibernate Validator并让你用它来作为数据校验(尽管你可能并不使用Hibernate作为持久化层)。

(8)

标准EJB注解 @Id 表明了实体Bean的主键属性。

这个例子中最值得注意的是 @Name@Scope 注解,它们确立了这个类是Seam的组件。

接下来我们将看到 User 类字段在更新模型值阶段时直接被绑定给JSF组件并由JSF操作, 在此并不需要冗余的胶水代码来在JSP页面与实体Bean域模型间来回拷贝数据。

然而,实体Bean不应该进行事务管理或数据库访问。故此,我们无法将此组件作为JSF动作监听器,因而需要会话Bean。

1.2.1.2. 无状态会话Bean:RegisterAction.java

在Seam应用中大都采用会话Bean来作为JSF动作监听器(当然我们也可选择JavaBean)。

在我们的应用程序中确实存在一个JSF动作和一个会话Bean方法。在此示例中,只有一个JSF动作,并且我们使用会话Bean方法与之相关联并使用无状态Bean,这是由于所有与动作相关的状态都保存在 User Bean中。

这是示例中比较有趣的代码部份:

Example 1.2. 

@Stateless                                                                               (1)
@Name("register")
public class RegisterAction implements Register
{

   @In                                                                                   (2)
   private User user;

   @PersistenceContext                                                                   (3)
   private EntityManager em;

   @Logger                                                                               (4)
   private Log log;

   public String register()                                                              (5)
   {
      List existing = em.createQuery(
         "select username from User where username=#{user.username}")                    (6)
         .getResultList();

      if (existing.size()==0)
      {
         em.persist(user);
         log.info("Registered new user #{user.username}");                               (7)
         return "/registered.jsp";                                                       (8)
      }
      else
      {
         FacesMessages.instance().add("User #{user.username} already exists");           (9)
         return null;
      }
   }

}
(1)

EJB标准注解 @Stateless 将这个类标记为无状态的会话Bean。

(2)

注解 @In将Bean的一个属性标记为由Seam来注入。 在此例中,此属性由名为 user 的上下文变量注入(实例的变量名)。

(3)

EJB标准注解 @PersistenceContext 用来注入EJB实体管理器。

(4)

Seam的 @Logger 注解用来注入组件的 Log 实例。

(5)

动作监听器方法使用标准的EJB3 EntityManager API来与数据库交互,并返回JSF的输出结果。 请注意,由于这是个会话Bean,因此当 register() 方法被调用时事务就会自动开始,并在结束时提交(commit)。

(6)

请注意Seam让你在EJB-QL中使用JSF EL表达式。因此可在标准JPA Query 对象上调用普通的JPA setParameter() 方法,这样岂不妙哉?

(7)

Log API为显示模板化的日志消息提供了便利。

(8)

多个JSF动作监听器方法返回一个字符串值的输出,它决定了接下来应显示的页面内容。 空输出(或返回值为空的动作监听器方法)重新显示上一页的内容。 在普通的JSF中,用JSF的导航规则(navigation rule) 来决定输出结果的JSF视图id是很常用的。 这种间接性对于复杂的应用是非常有用的,值得去实践。但是,对于象示例这样简单的的应用,Seam让你使用JSF视图id作为输出结果,以减少对导航规则的需求。请注意,当你用视图id作为输出结果时,Seam总会执行一次浏览器的重定向。

(9)

Seam提供了大量的 内置组件(built-in components) 来协助解决那些经常遇到的问题。 用 FacesMessages 组件就可很容易地来显示模板化的错误或成功的消息。 内置的Seam组件还可由注入或通过调用 instance() 方法来获取。

这次我们并没有显式指定 @Scope,若没有显式指定时,每个Seam 组件类型就使用其默认的作用域。对于无状态的会话Bean, 其默认的作用域就是无状态的上下文。实际上 所有的 无状态的会话Bean都属于无状态的上下文。

会话Bean的动作监听器在此小应用中履行了业务和持久化逻辑。在更复杂的应用中,我们可能要将代码分层并重构持久化逻辑层成 专用数据存取组件,这很容易做到。但请注意Sean并不强制你在应用分层时使用某种特定的分层策略。

此外,也请注意我们的SessionBean会同步访问与web请求相关联的上下文(比如在 User 对象中的表单的值),状态会被保持在事务型的资源里(EntityManager 对象)。 这是对传统J2EE的体系结构的突破。再次说明,如果你习惯于传统J2EE的分层,也可以在你的Seam应用实行。但是对于许多的应用,这是明显的没有必要 。

1.2.1.3. 会话Bean的本地接口:Register.java

很自然,我们的会话Bean需要一个本地接口。

Example 1.3. 

@Local
public interface Register
{
   public String register();
}

所有的Java代码就这些了,现在去看一下部署描述文件。

1.2.1.4. Seam组件部署描述文件:components.xml

如果你此前曾接触过许多的Java框架,你就会习惯于将所有的组件类放在某种XML文件中来声明,那些文件就会随着项目的不断成熟而不断加大到最终到不可收拾的地步。 对于Seam应用,你尽可放心,因为它并不要求应用组件都要有相应的XML。大部份的Seam应用要求非常少量的XML即可,且XML文件大小不会随着项目的增大而快速增长。

无论如何,若能为 某些 组件(特别是Seam内置组件)提供某些 外部配置往往是有用的。这样一来,我们就有几个选择, 但最灵活的选择还是使用位于 WEB-INF 目录下的 components.xml 配置文件。 我们将用 components.xml 文件来演示Seam怎样在JNDI中找到EJB组件:

Example 1.4. 

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core">
     <core:init jndi-pattern="@jndiPattern@"/>
</components>

此代码配置了Seam内置组件 org.jboss.seam.core.initjndiPattern 属性。这里需要奇怪的@符号是因为ANT脚本会在部署应用时将正确的JNDI语法在标记处自动填补

1.2.1.5. Web部署描述文件:web.xml

我们将以WAR的形式来部署此小应用的表示层,因此需要web部署描述文件。

Example 1.5. 

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <!-- Seam -->

    <listener>
        <listener-class>org.jboss.seam.servlet.SeamListener</listener-class>
    </listener>

    <!-- MyFaces -->

    <listener>
        <listener-class>
            org.apache.myfaces.webapp.StartupServletContextListener
        </listener-class>
    </listener>

    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <param-value>client</param-value>
    </context-param>

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Faces Servlet Mapping -->
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.seam</url-pattern>
    </servlet-mapping>

</web-app>

web.xml 文件配置了Seam和JSF。所有Seam应用中的配置与此处的配置基本相同。

1.2.1.6. JSF配置:faces-config.xml

绝大多数的Seam应用将JSF来作为表示层。因而我们通常需要 faces-config.xml。SEAM将用Facelet定义视图表现层,所以我们需要告诉JSF用Facelet作为它的模板引擎。

Example 1.6. 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE faces-config
PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
                            "http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
<faces-config>

    <!-- A phase listener is needed by all Seam applications -->

    <lifecycle>
        <phase-listener>org.jboss.seam.jsf.SeamPhaseListener</phase-listener>
    </lifecycle>

</faces-config>

注意我们不需要申明任何JSF managed Bean!因为我们所有的managed Bean都是通过经过注释的Seam组件。所以在Seam的应用中,faces-config.xml比原始的JSF更少用到。

实际上,一旦你把所有的基本描述文件配置完毕,你所需写的 唯一类型的 XML文件就是导航规则及可能的jBPM流程定义。对于Seam而言, 流程(process flow)配置数据 是唯一真正属于需要XML定义的。

在此简单的示例中,因为我们将视图页面的ID嵌入到Action代码中,所以我们甚至都不需要定义导航规则。

1.2.1.7. EJB部署描述文件:ejb-jar.xml

ejb-jar.xml 文件将 SeamInterceptor 绑定到压缩包中所有的会话Bean上,以此实现了Seam与EJB3的整合。

<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
         version="3.0">

   <interceptors>
     <interceptor>
       <interceptor-class>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
     </interceptor>
   </interceptors>

   <assembly-descriptor>
      <interceptor-binding>
         <ejb-name>*</ejb-name>
         <interceptor-class>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
      </interceptor-binding>
   </assembly-descriptor>

</ejb-jar>

1.2.1.8. EJB持久化部署描述文件:persistence.xml

persistence.xml 文件告诉EJB的持久化层在哪找到数据源,该文件也含有一些厂商特定的设定。此例在程序启动时自动创建数据库Schema。

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
             version="1.0">
    <persistence-unit name="userDatabase">
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <jta-data-source>java:/DefaultDS</jta-data-source>
      <properties>
         <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      </properties>
    </persistence-unit>
</persistence>

1.2.1.9. 视图:register.xhtmlregistered.xhtml

对于Seam应用的视图可由任意支持JSF的技术来实现。在此例中,我们使用了JSP,因为大多数的开发人员都很熟悉, 且这里并没有其它太多的要求。(我们建议你在实际开发中使用Facelets)。

Example 1.7. 

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://jboss.com/products/seam/taglib" prefix="s" %>
<html>
 <head>
  <title>Register New User</title>
 </head>
 <body>
  <f:view>
   <h:form>
     <table border="0">
       <s:validateAll>
         <tr>
           <td>Username</td>
           <td><h:inputText value="#{user.username}"/></td>
         </tr>
         <tr>
           <td>Real Name</td>
           <td><h:inputText value="#{user.name}"/></td>
         </tr>
         <tr>
           <td>Password</td>
           <td><h:inputSecret value="#{user.password}"/></td>
         </tr>
       </s:validateAll>
     </table>
     <h:messages/>
     <h:commandButton type="submit" value="Register" action="#{register.register}"/>
   </h:form>
  </f:view>
 </body>
</html>

这里的 <s:validateAll>标签是Seam特有的。 该JSF组件告诉JSF让它用实体Bean中所指定的Hibernat验证器注解来验证所有包含输入的字段。

Example 1.8. 

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
 <head>
  <title>Successfully Registered New User</title>
 </head>
 <body>
  <f:view>
    Welcome, <h:outputText value="#{user.name}"/>,
    you are successfully registered as <h:outputText value="#{user.username}"/>.
  </f:view>
 </body>
</html>

这是个极其普通的使用JSF组件的JSP页面,与Seam毫无相干。

1.2.1.10. EAR部署描述文件:application.xml

最后,因为我们的应用是要部署成EAR的,因此我们也需要部署描述文件。

Example 1.9. 

<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application_5.xsd"
             version="5">

    <display-name>Seam Registration</display-name>

    <module>
        <web>
            <web-uri>jboss-seam-registration.war</web-uri>
            <context-root>/seam-registration</context-root>
        </web>
    </module>
    <module>
        <ejb>jboss-seam-registration.jar</ejb>
    </module>
    <module>
        <java>jboss-seam.jar</java>
    </module>
    <module>
        <java>el-api.jar</java>
    </module>
    <module>
        <java>el-ri.jar</java>
    </module>

</application>

此部署描述文件联接了EAR中的所有模块,并把Web应用绑定到此应用的首页 /seam-registration

至此,我们了解了整个应用中 所有的 部署描述文件!

1.2.2. 工作原理

当提交表单时,JSF请求Seam来解析名为 user 的变量。由于还没有值绑定到 user 上(在任意的Seam上下文中), Seam就会实例化 user组件,接着把它保存在Seam会话上下文后,然后将 User 实体Bean实例返回给JSF。

表单输入的值将由在 User 实体中所指定的Hibernate验证器来验证。 若有非法输入,JSF就重新显示当前页面。否则,JSF就将输入值绑定到 User 实体Bean的字段上。

接着,JSF请求Seam来解析变量 register。 Seam在无状态上下文中找到 RegisterAction 无状态的会话Bean并把它返回。JSF随之调用 register() 动作监听器方法。

Seam拦截方法调用并在继续调用之前从Seam会话上下文注入 User 实体。

register() 方法检查所输入用户名的用户是否已存在。 若存在该用户名,则错误消息进入 facesmessages 组件队列,返回无效结果并触发浏览器重显页面。facesmessages 组件嵌在消息字符串的JSF表达式,并将JSF facesmessage 添加到视图中。

若输入的用户不存在,"/registered.jsp" 输出就会将浏览器重定向到 registered.jsp 页。 当JSF来渲染页面时,它请求Seam来解析名为 user 的变量,并使用从Seam会话作用域返回的 User 实体的属性值。

1.3. Seam中的可点击列表:消息示例

在几乎所有的在线应用中都免不了将搜索结果显示成可点击的列表。 因此Sean在JSF层之上提供了特殊的功能,使得我们很容易用EJB-QL或HQL来查询数据并用JSF <h:dataTable> 将查询结果显示成可点击的列表。我们将在接下的例子中演示这一功能。

1.3.1. 理解代码

此消息示例中有一个实体Bean,Message,一个会话Bean MessageListBean 及一个JSP页面。

1.3.1.1. 实体Bean:Message.java

Message 实体定义了消息的title,text,date和time以及该消息是否已读的标志:

Example 1.10. 

@Entity
@Name("message")
@Scope(EVENT)
public class Message implements Serializable
{
   private Long id;
   private String title;
   private String text;
   private boolean read;
   private Date datetime;

   @Id @GeneratedValue
   public Long getId() {
      return id;
   }
   public void setId(Long id) {
      this.id = id;
   }

   @NotNull @Length(max=100)
   public String getTitle() {
      return title;
   }
   public void setTitle(String title) {
      this.title = title;
   }

   @NotNull @Lob
   public String getText() {
      return text;
   }
   public void setText(String text) {
      this.text = text;
   }

   @NotNull
   public boolean isRead() {
      return read;
   }
   public void setRead(boolean read) {
      this.read = read;
   }

   @NotNull
   @Basic @Temporal(TemporalType.TIMESTAMP)
   public Date getDatetime() {
      return datetime;
   }
   public void setDatetime(Date datetime) {
      this.datetime = datetime;
   }

}

1.3.1.2. 有状态的会话Bean:MessageManagerBean.java

如此前的例子,会话Bean MessageManagerBean 用来给表单中的两个按钮定义个动作监听器方法, 其中的一个按钮用来从列表中选择消息,并显示该消息。而另一个按钮则用来删除一条消息,除此之外,就没什么特别之处了。

在用户第一次浏览消息页面时,MessageManagerBean 会话Bean也负责抓取消息列表,考虑到用户可能以多种方式来浏览该页面,他们也有可能不是由JSF动作来完成,比如用户可能将该页加入收藏夹。 因此抓取消息列表发生在Seam的工厂方法中,而不是在动作监听器方法中。

之所以将此会话Bean设为有状态的,是因为我们想在不同的服务器请求间缓存此消息列表。

Example 1.11. 

@Stateful
@Scope(SESSION)
@Name("messageManager")
public class MessageManagerBean implements Serializable, MessageManager
{

   @DataModel                                                                            (1)
   private List<Message> messageList;

   @DataModelSelection                                                                   (2)
   @Out(required=false)                                                                  (3)
   private Message message;

   @PersistenceContext(type=EXTENDED)                                                    (4)
   private EntityManager em;

   @Factory("messageList")                                                               (5)
   public void findMessages()
   {
      messageList = em.createQuery("from Message msg order by msg.datetime desc").getResultList();
   }

   public void select()                                                                  (6)
   {
      message.setRead(true);
   }

   public void delete()                                                                  (7)
   {
      messageList.remove(message);
      em.remove(message);
      message=null;
   }

   @Remove @Destroy                                                                      (8)
   public void destroy() {}

}
(1)

注解 @DataModel 暴露了 java.util.List 类型的属性给JSF页面来作为 javax.faces.model.DataModel 的实例。 这允许我们在JSF <h:dataTable>的每一行中能使用可点击列表。在此例中,DataModel 可在变量名为 messageList 的会话上下文中被使用。

(2)

@DataModelSelection 注解告诉了Seam来注入 List 元素到相应的被点击链接。

(3)

注解 @Out 直接暴露了被选中的值给页面。 这样一来,每次可点击列表一旦被选中,Message 就被会注入给有状态Bean的属性,紧接着 向外注入(outjected)给变量名为 message 的事件上下文的属性。

(4)

此有状态Bean有个EJB3的 扩展持久化上下文(extended persistence context)。只要Bean存在,查询中获取的消息就会保留在受管理的状态中。 这样一来,此后对有状态Bean的所有方法调用勿需显式调用 EntityManager 就可更新这些消息了。

(5)

当我们第一次浏览JSP页面时,messageList 上下文变量尚未被初始化,@Factory 注解告诉Seam来创建 MessageManagerBean 的实例并调用 findMessages() 方法来初始化上下文变量。 我们把 findMessages() 当作 messages 工厂方法

(6)

select() 将选中的 Message 标为已读,并同时更新数据库。

(7)

delete() 动作监听器方法将选中的 Message 从数据库中删除。

(8)

对于每个有状态的会话Bean,Seam组件的所有方法中 必须 有一不带参数的方法被标为 @Remove @Destroy 以确保在Seam的上下文结束时删除有状态Bean,并同时清除所有服务器端的状态。

请注意,这是个会话作用域的Seam组件。它与用户登入会话相关联,并且登入会话的所有请求共享同一个组件的实例。 (在Seam的应用中,我们通常使用会话作用域的组件。)

1.3.1.3. 会话Bean的本地接口:MessageManager.java

当然,每个会话Bean都有个业务接口。

@Local
public interface MessageManager
{
   public void findMessages();
   public void select();
   public void delete();
   public void destroy();
}

从现在起,我们在示例代码中将不再对本地接口作特别的说明。

由于XML文件与此前的示例几乎都一样,因此我们略过了 components.xmlpersistence.xmlweb.xmlejb-jar.xmlfaces-config.xmlapplication.xml 的细节,直接来看一下JSP。

1.3.1.4. 视图:messages.jsp

JSP页面就是直接使用JSF <h:dataTable> 的组件,并没有与Seam有什么关系。

Example 1.12. 

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
 <head>
  <title>Messages</title>
 </head>
 <body>
  <f:view>
   <h:form>
     <h2>Message List</h2>
     <h:outputText value="No messages to display" rendered="#{messageList.rowCount==0}"/>
     <h:dataTable var="msg" value="#{messageList}" rendered="#{messageList.rowCount>0}">
        <h:column>
           <f:facet name="header">
              <h:outputText value="Read"/>
           </f:facet>
           <h:selectBooleanCheckbox value="#{msg.read}" disabled="true"/>
        </h:column>
        <h:column>
           <f:facet name="header">
              <h:outputText value="Title"/>
           </f:facet>
           <h:commandLink value="#{msg.title}" action="#{messageManager.select}"/>
        </h:column>
        <h:column>
           <f:facet name="header">
              <h:outputText value="Date/Time"/>
           </f:facet>
           <h:outputText value="#{msg.datetime}">
              <f:convertDateTime type="both" dateStyle="medium" timeStyle="short"/>
           </h:outputText>
        </h:column>
        <h:column>
           <h:commandButton value="Delete" action="#{messageManager.delete}"/>
        </h:column>
     </h:dataTable>
     <h3><h:outputText value="#{message.title}"/></h3>
     <div><h:outputText value="#{message.text2}"/></div>
   </h:form>
  </f:view>
 </body>
</html>

1.3.2. 工作原理

当我们首次浏览 messages.jsp 页面时,无论是否由回传(postback)的JSF(页面请求)或浏览器直接的GET请求(非页面请求),此JSP页面将设法解析 messagelist 上下文变量。 由于上下文变量尚未被初始化,因此Seam将调用工厂方法 findmessages(),该方法执行了一次数据库查询并导致 DataModel 被向外注入。 DataModel 提供了渲染 <h:dataTable> 所需的行数据。

当用户点击 <h:commandLink> 时,JSF就调用 Select() 动作监听器。 Seam拦截此调用并将所选行的数据注入给 messageManager 组件的 message 属性。 而动作监听器将所选定的 Message 标为已读。在此调用结束时,Seam向外注入所选定的 Message 给名为 message 的变量。 接着,EJB容器提交事务,将 Message 的已读标记写入数据库。 最后,该网页重新渲染,再次显示消息列表,并在列表下方显示所选消息的内容。

如果用户点击了 <h:commandButton>,JSF就调用 delete() 动作监听器。 Seam拦截此调用并将所选行的数据注入给 messageManager 组件的 message 属性。 触发动作监听器,将选定的 Message 从列表中删除并同时在 EntityManager 中调用 remove() 方法。在此调用的最后,Seam刷新 messageList 上下文变量并清除名为 message 的上下文变量。 接着,EJB容器提交事务,将 Message 从数据库中删除。最后,该网页重新渲染,再次显示消息列表。

1.4. Seam和jBPM:待办事项列表(todo list)示例

jBPM提供了先进的工作流程和任务管理的功能。为了体验一下jBPM是如何与Seam集成在一起工作的,在此将给你一个简单的管理“待办事项列表”的应用。由于管理任务列表等功能是jBPM的核心功能,所以在此例中只用了很少的Java代码。

1.4.1. 理解代码

这个例子的核心是jBPM的流程定义(process definition)。此外,还有两个JSP页面和两个简单的JavaBeans(由于他们不用访问数据库,或有其它事务相关的行为,因此并没有用会话Bean)。让我们先从流程定义开始:

Example 1.13. 

<process-definition name="todo">

   <start-state name="start">                                                            (1)
      <transition to="todo"/>
   </start-state>

   <task-node name="todo">                                                               (2)
      <task name="todo" description="#{todoList.description}">                           (3)
         <assignment actor-id="#{actor.id}"/>                                            (4)
      </task>
      <transition to="done"/>
   </task-node>

   <end-state name="done"/>                                                              (5)

</process-definition>
(1)

节点 <start-state> 代表流程的逻辑开始。一旦流程开始时,它就立即转入 todo节点。

(2)

<task-node> 节点代表 等待状态,就是在执行业务流程暂停时,等待一个或多个未完成的任务。

(3)

<task> 元素定义了用户需要完成的任务。 由于在这个节点只有定义了一个任务,当它完成,或恢复执行时我们就转入结束状态。 此任务从Seam中名为 todolist 的组件(JavaBeans之一)获得任务description。

(4)

任务在创建时就会被分配给一个用户或一组用户时。在此示例中,任务是分配给当前用户,该用户从一个内置的名为 actor 的Seam组件中获得。任何Seam组件都可用来执行任务指派。

(5)

<end-state>节点定义业务流程的逻辑结束。当执行到达这个节点时,流程实例就要被销毁。

如果我们用jBossIDE所提供的流程定义编辑器来查看此流程定义,那它就会是这样:

这个文档将我们的 业务流程 定义成节点图。 这可能是最常见的业务流程:只有一个 任务 被执行,当这项任务完成之后,业务流程就结束了。

第一个JavaBean处理登入界面 login.jsp。 它的工作就是用 actor 组件初始化jBPM用户id(在实际的应用中,它也需要验证用户。)

Example 1.14. 

@Name("login")
public class Login {

   @In
   private Actor actor;

   private String user;

   public String getUser() {
      return user;
   }

   public void setUser(String user) {
      this.user = user;
   }

   public String login()
   {
      actor.setId(user);
      return "/todo.jsp";
   }
}

在此我们使用了 @In 来将actor属性值注入到Seam内置的 Actor 组件。

JSP页面本身并没有什么特别之处:

Example 1.15. 

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<f:view>
    <h:form>
      <div>
        <h:inputText value="#{login.user}"/>
        <h:commandButton value="Login" action="#{login.login}"/>
      </div>
    </h:form>
</f:view>
</body>
</html>

第二个JavaBean负责启动业务流程实例及结束任务。

Example 1.16. 

@Name("todoList")
public class TodoList {

   private String description;

   public String getDescription()                                                        (1)
   {
      return description;
   }

   public void setDescription(String description) {
      this.description = description;
   }

   @CreateProcess(definition="todo")                                                     (2)
   public void createTodo() {}

   @StartTask @EndTask                                                                   (3)
   public void done() {}

}
(1)

description属性从JSP页接受用户输入,并将它暴露给流程定义,这样就可让Seam来设定任务的descrption。

(2)

Seam的 @CreateProcess 注解为指定名称的流程定义创建了一个新的jBPM流程实例。

(3)

Seam的 @StartTask 注解用来启动任务,@EndTask 用来结束任务,并允许恢复执行业务流程。

在实际的应用中,@StartTask@EndTask 不会出现在同一个方法中,因为为了完成任务,通常用应用中有许多工作要做。

最后,该应用的主要内容在 todo.jsp 中:

Example 1.17. 

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://jboss.com/products/seam/taglib" prefix="s" %>
<html>
<head>
<title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>
<f:view>
   <h:form id="list">
      <div>
         <h:outputText value="There are no todo items." rendered="#{empty taskInstanceList}"/>
         <h:dataTable value="#{taskInstanceList}" var="task" rendered="#{not empty taskInstanceList}">
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Description"/>
                </f:facet>
                <h:inputText value="#{task.description}"/>
            </h:column>
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Created"/>
                </f:facet>
                <h:outputText value="#{task.taskMgmtInstance.processInstance.start}">
                    <f:convertDateTime type="date"/>
                </h:outputText>
            </h:column>
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Priority"/>
                </f:facet>
                <h:inputText value="#{task.priority}" style="width: 30"/>
            </h:column>
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Due Date"/>
                </f:facet>
                <h:inputText value="#{task.dueDate}" style="width: 100">
                    <f:convertDateTime type="date" dateStyle="short"/>
                </h:inputText>
            </h:column>
            <h:column>
                <s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/>
            </h:column>
         </h:dataTable>
      </div>
      <div>
      <h:messages/>
      </div>
      <div>
         <h:commandButton value="Update Items" action="update"/>
      </div>
   </h:form>
   <h:form id="new">
      <div>
         <h:inputText value="#{todoList.description}"/>
         <h:commandButton value="Create New Item" action="#{todoList.createTodo}"/>
      </div>
   </h:form>
</f:view>
</body>
</html>

让我们对此逐一加以说明。

该JSP页面将从Seam内置组件 taskInstanceList 获得的任务渲染成任务列表,此列表在JSF表单内被定义。

<h:form id="list">
   <div>
      <h:outputText value="There are no todo items." rendered="#{empty taskInstanceList}"/>
      <h:dataTable value="#{taskInstanceList}" var="task" rendered="#{not empty taskInstanceList}">
         ...
      </h:dataTable>
   </div>
</h:form>

列表中的每个元素就是一个jBPM类 taskinstance 的实例。 以下代码简单地展示了列表中每一任务的有趣特性。为了让用户能更改description、priority及due date的值,我们使用了输入控件。

<h:column>
    <f:facet name="header">
       <h:outputText value="Description"/>
    </f:facet>
    <h:inputText value="#{task.description}"/>
</h:column>
<h:column>
    <f:facet name="header">
        <h:outputText value="Created"/>
    </f:facet>
    <h:outputText value="#{task.taskMgmtInstance.processInstance.start}">
        <f:convertDateTime type="date"/>
    </h:outputText>
</h:column>
<h:column>
    <f:facet name="header">
        <h:outputText value="Priority"/>
    </f:facet>
    <h:inputText value="#{task.priority}" style="width: 30"/>
</h:column>
<h:column>
    <f:facet name="header">
        <h:outputText value="Due Date"/>
    </f:facet>
    <h:inputText value="#{task.dueDate}" style="width: 100">
        <f:convertDateTime type="date" dateStyle="short"/>
    </h:inputText>
</h:column>

该按钮通过调用被注解为 @StartTask @EndTask 的动作方法来结束任务。它把任务id作为请求参数传给Seam:

<h:column>
    <s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/>
</h:column>

(请注意,这是在使用Seam seam-ui.jar 包中的JSF <s:button> 控件。)

这个按钮是用来更新任务属性。当提交表单时,Seam和jBPM将直接更改任务的持久化,不需要任何的动作监听器方法:

<h:commandButton value="Update Items" action="update"/>

第二个表单通过调用注解为 @CreateProcess的动作方法来创建新的项目(item)。

<h:form id="new">
    <div>
        <h:inputText value="#{todoList.description}"/>
        <h:commandButton value="Create New Item" action="#{todoList.createTodo}"/>
    </div>
</h:form>

这个例子还需要另外几个文件,但它们只是标准的jBPM和Seam配置并不是很有趣。

1.4.2. 工作原理

待完成

1.5. Seam页面流:猜数字范例

对有相对自由(特别)导航的Seam应用程序而言,JSF/Seam导航规则是定义页面流的一个完美的方法。 而对于那些带有更多约束的导航,特别是带状态的用户界面而言,导航规则反而使得系统流程变得难以理解。 要理解整个流程,你需要从视图页面、动作和导航规则里一点点把它拼出来。

Seam允许你使用一个jPDL流程定义来定义页面流。下面这个简单的猜数字范例将演示这一切是如何实现的。

1.5.1. 理解代码

这个例子由一个JavaBean、三个JSP页面和一个jPDL页面流定义组成。让我们从页面流开始:

Example 1.18. 

<pageflow-definition name="numberGuess">

   <start-page name="displayGuess" view-id="/numberGuess.jsp">
      <redirect/>
      <transition name="guess" to="evaluateGuess">
          <action expression="#{numberGuess.guess}" />
      </transition>                                                                      (1)
   </start-page>                                                                         (2)
                                                                                         (3)
   <decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
      <transition name="true" to="win"/>
      <transition name="false" to="evaluateRemainingGuesses"/>
   </decision>                                                                           (4)

   <decision name="evaluateRemainingGuesses" expression="#{numberGuess.lastGuess}">
      <transition name="true" to="lose"/>
      <transition name="false" to="displayGuess"/>
   </decision>

   <page name="win" view-id="/win.jsp">
      <redirect/>
      <end-conversation />
   </page>

   <page name="lose" view-id="/lose.jsp">
      <redirect/>
      <end-conversation />
   </page>

</pageflow-definition>
(1)

<page>元素定义了一个等待状态,在该状态中系统显示一个JSF视图等待用户输入。 view-id与简单JSF导航规则中的view id一样。 redirect属性告诉Seam在导航到页面时使用post-then-redirect。(这会带来友好的浏览器URL。)

(2)

<transition> 元素命名了一个JSF输出。当一个JSF动作导致那个输出时会触发转换。 在任何jBPM转换动作调用后,执行会进行到页面流程图的下一个节点。

(3)

一个转换动作 <action> 就像JSF动作,不同的就是它只发生在一个jBPM转换发生时。 转换动作能调用任何Seam组件。

(4)

<decision> 节点用来划分页面流,通过计算JSF EL表达式决定要执行的下一个节点。

这个页面流在JBossIDE页面流编辑器里看上去是这个样子的:

看过了页面流,现在再来理解剩下的程序就变得十分简单了!

这是应用程序的主页面numberGuess.jspx

Example 1.19. 

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title>Guess a number...</title>
</head>
<body>
<h1>Guess a number...</h1>
<f:view>
    <h:form>
        <h:outputText value="Higher!" rendered="#{numberGuess.randomNumber>numberGuess.currentGuess}" />
        <h:outputText value="Lower!" rendered="#{numberGuess.randomNumber<numberGuess.currentGuess}" />
        <br />
        I'm thinking of a number between <h:outputText value="#{numberGuess.smallest}" /> and
        <h:outputText value="#{numberGuess.biggest}" />. You have
        <h:outputText value="#{numberGuess.remainingGuesses}" /> guesses.
        <br />
        Your guess:
        <h:inputText value="#{numberGuess.currentGuess}" id="guess" required="true">
            <f:validateLongRange
                maximum="#{numberGuess.biggest}"
                minimum="#{numberGuess.smallest}"/>
        </h:inputText>
        <h:commandButton type="submit" value="Guess" action="guess" />
        <br/>
        <h:message for="guess" style="color: red"/>
    </h:form>
</f:view>
</body>
</html>

请注意名为 guess 的命令按钮是如何进行转换而不是直接调用一个动作的。

win.jspx 页面的内容是可想而知的:

Example 1.20. 

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title>You won!</title>
</head>
<body>
<h1>You won!</h1>
<f:view>
    Yes, the answer was <h:outputText value="#{numberGuess.currentGuess}" />.
    It took you <h:outputText value="#{numberGuess.guessCount}" /> guesses.
    Would you like to <a href="numberGuess.seam">play again</a>?
  </f:view>
</body>
</html>

lose.jsp 也差不多(我就不重复复制/粘贴了)。最后,JavaBean Seam组件是这样的:

Example 1.21. 

@Name("numberGuess")
@Scope(ScopeType.CONVERSATION)
public class NumberGuess {

   private int randomNumber;
   private Integer currentGuess;
   private int biggest;
   private int smallest;
   private int guessCount;
   private int maxGuesses;

   @Create                                                                               (1)
   @Begin(pageflow="numberGuess")                                                        (2)
   public void begin()
   {
      randomNumber = new Random().nextInt(100);
      guessCount = 0;
      biggest = 100;
      smallest = 1;
   }

   public void setCurrentGuess(Integer guess)
   {
      this.currentGuess = guess;
   }

   public Integer getCurrentGuess()
   {
      return currentGuess;
   }

   public void guess()
   {
      if (currentGuess>randomNumber)
      {
         biggest = currentGuess - 1;
      }
      if (currentGuess<randomNumber)
      {
         smallest = currentGuess + 1;
      }
      guessCount ++;
   }

   public boolean isCorrectGuess()
   {
      return currentGuess==randomNumber;
   }

   public int getBiggest()
   {
      return biggest;
   }

   public int getSmallest()
   {
      return smallest;
   }

   public int getGuessCount()
   {
      return guessCount;
   }

   public boolean isLastGuess()
   {
      return guessCount==maxGuesses;
   }

   public int getRemainingGuesses() {
      return maxGuesses-guessCount;
   }

   public void setMaxGuesses(int maxGuesses) {
      this.maxGuesses = maxGuesses;
   }

   public int getMaxGuesses() {
      return maxGuesses;
   }

   public int getRandomNumber() {
      return randomNumber;
   }
}
(1)

一开始,JSP页面请求一个 numberGuess 组件,Seam会为该组件创建一个新的实例,并调用 @Create 方法,允许组件初始化自己。

(2)

@Begin 注解启动了一个Seam 业务会话(conversation) (稍后详细说明),并指定业务会话页面流所要使用的页面流定义。

如你所见,这个Seam组件是纯业务逻辑的!它不需要知道任何关于用户交互的东西。这点使得组件更易被复用。

1.5.2. 工作原理

TODO

1.6. 一个完整的Seam应用程序:宾馆预订范例

1.6.1. 介绍

该系统是一个完整的宾馆客房预订系统,它由下列功能组成:

  • 用户注册

  • 登录

  • 注销

  • 设置密码

  • 搜索宾馆

  • 选择宾馆

  • 客房预订

  • 预订确认

  • 当前预订列表

应用程序中使用了JSF、EJB 3.0和Seam,视图部分结合了Facelets。也可以选择使用JSF、Facelets、Seam、JavaBeans和Hibernate3。

在使用过一段时间后你会发现该应用程序非常 健壮。你能使用回退按钮、刷新浏览器、打开多个窗口, 或者键入各种无意义的数据,会发现都很难让它崩溃。你也许会想我们花了几个星期测试修复该系统才达到了这个目标。 事实却不是这样的,Seam的设计使你能够用它方便地构建健壮的web应用程序,而且Seam还提供了很多以前需要通过编码才能实现的健壮性。

在你浏览范例程序代码研究它是如何运行时,注意观察声明式的状态管理和集成的验证是如何被用来实现这种健壮性的。

1.6.2. 预订系统概况

这个项目的结构和上一个一样,要安装部署该应用程序请参考Section 1.1, “试试看”。 当应用程序启动后,可以通过 http://localhost:8080/seam-booking/ 进行访问。

只需要用9个类(加上6个Session Bean的本地接口)就能实现这个应用程序。6个Session Bean动作监听器包括了以下功能的所有业务逻辑。

  • BookingListAction 获得当前登录用户的预订列表。
  • ChangePasswordAction 修改当前用户的密码。
  • HotelBookingAction 实现了应用程序的核心功能:宾馆客房搜索、选择、预订和预订确认。 这功能是以 业务对话(conversation) 形式实现的,所以它是整个程序中最有意思的一个类。
  • RegisterAction 注册一个新用户。

应用程序的持久化模型由三个实体bean实现。

  • Hotel 是表示一个宾馆的实体Bean
  • Booking 是表示一个预订的实体Bean
  • User 是表示一个能够进行宾馆预订的用户的实体Bean

1.6.3. 理解Seam业务对话(Conversation)

我们鼓励您随意浏览源代码。在这个教程里我们将关注功能中的某一特定部分:宾馆搜索、选择、预订和确认。 从用户的角度来看,从选择宾馆到确认的每一步都是工作中的一个连续单元,属于一个 业务对话。 然而搜索却 是该对话的一部分。用户能在不同浏览器标签页中的相同搜索结果页面中选择多个宾馆。

大多数Web应用程序架构没有提供表示业务对话的一级构件(first class construct)。这在管理与对话相关的状态时带来了很多麻烦。 通常情况下,Java的Web应用程序结合两种技术来应对这一情况:一是将某些状态丢入 HttpSession;二是将可持久化的状态在每个请求(Request)后写入数据库,并在每个新请求的开始将之重建。

由于数据库是最不可扩展的一层,因此这么做往往导致完全无法接受的扩展性低下。在每次请求时访问数据库所造成的额外流量和等待时间也是一个问题。 要降低冗余流量,Java应用程序常引入一个(二级)数据缓存来保存被经常访问的数据。 然而这个缓存是很低效的,因为它的失效算法是基于LRU(最近最少使用)策略,而不是基于用户何时结束与该数据相关的工作。 此外,由于该缓存被许多并发事务共享,要保持缓存与数据库的状态一致,我们需要引入了一套完整的机制。

现在再让我们考虑将状态保存在 HttpSession 里。通过精心设计的编程,我们也许能控制session数据的大小。 但这远比听起来要麻烦的多,因为Web浏览器允许特殊的非线性导航。 但假设我们在系统开发到一半的时候突然发现一个需求,它要求用户可以拥有 多并发业务对话(我就碰到过)。 要开发一些机制,以分离与不同并发业务会话相关的session状态,并引入故障保护,在用户关闭浏览器窗口或标签页时销毁业务会话状态。 这对普通人来说可不是一件轻松的事情(我就实现过两次,一次是为一个客户应用程序,另一次是为Seam,幸好我是出了名的疯子)。

现在提供一个更好的方法。

Seam引入了 对话上下文 来作为一级构件。你能在其中安全地保存业务对话状态,它会保证状态有一个定义良好的生命周期。 而且,你不用再不停地在应用服务器和数据库间传递数据,因为业务对话上下文就是一个天然的缓存,用来缓存用户的数据。

通常情况下,我们保存在业务对话上下文中的组件是有状态的Session Bean。(我们也在其中保存实体Bean和JavaBeans。) 在Java社区中一直有一个谣传,认为有状态的Session Bean是扩展性的杀手。在1998年WebFoobar 1.0发布时的确如此。 但今天的情况已经变了。像JBoss 4.0这样的应用服务器都有很成熟的机制处理有状态Session Bean的状态复制。 (例如,JBoss EJB3容器可以执行很细致的复制,只复制那些属性值被改变过的bean。) 请注意,所有那些传统技术中关于有状态Bean是低效的争论也同样发生在 HttpSession 上,所以说将状态从业务层的有状态Session Bean迁移到Web Session中以提高性能的做法毫无疑问是被误导的。 不正确地使用有状态的Bean,或者是将它们用在错误的地方上都会使应用程序变得无法扩展。 但这并不意味着你应该 永远不要 使用它们。总之,Seam会告诉你一个安全使用的模型。欢迎来到2005年。

OK,不再多说了,话题回到这个指南上吧。

宾馆预订范例演示了不同作用域的有状态组件是如何协同工作实现复杂的行为的。 它的主页面允许用户搜索宾馆。搜索的结果被保存在Seam的session域中。 当用户导航到其中一个宾馆时,一个业务会话便开始了,一个业务会话域组件回调session域组件以获得选中的宾馆。

宾馆预订范例还演示了如何使用Ajax4JSF在不用手工编写JavaScript的情况下实现富客户端(Rich Client)行为。

搜索功能用了一个Session域的有状态Session Bean来实现,有点类似于我们在上面的消息列表范例里看到的那个Session Bean。

Example 1.22. 

@Stateful                                                                                (1)
@Name("hotelSearch")
@Scope(ScopeType.SESSION)
@Restrict("#{identity.loggedIn}")                                                        (2)
public class HotelSearchingAction implements HotelSearching
{

   @PersistenceContext
   private EntityManager em;

   private String searchString;
   private int pageSize = 10;
   private int page;

   @DataModel
   private List<Hotel> hotels;                                                           (3)

   public String find()
   {
      page = 0;
      queryHotels();
      return "main";
   }

   public String nextPage()
   {
      page++;
      queryHotels();
      return "main";
   }

   private void queryHotels()
   {
      String searchPattern = searchString==null ? "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%';
      hotels = em.createQuery("select h from Hotel h where lower(h.name) like :search or lower(h.city) like :search or lower(h.zip) like :search or lower(h.address) like :search")
            .setParameter("search", searchPattern)
            .setMaxResults(pageSize)
            .setFirstResult( page * pageSize )
            .getResultList();
   }

   public boolean isNextPageAvailable()
   {
      return hotels!=null && hotels.size()==pageSize;
   }

   public int getPageSize() {
      return pageSize;
   }

   public void setPageSize(int pageSize) {
      this.pageSize = pageSize;
   }

   public String getSearchString()
   {
      return searchString;
   }

   public void setSearchString(String searchString)
   {
      this.searchString = searchString;
   }

   @Destroy @Remove
   public void destroy() {}                                                              (4)

}
(1)

EJB标准中的 @Stateful 注解表明这个类是一个有状态的Session Bean。它们的默认作用域是业务对话上下文。

(2)

@Restrict注解给组件加上了一个安全限制。只有登录过的用户才能访问该组件。安全章节中更详细地讨论了Seam的安全问题。

(3)

@DataModel 注解将一个 List 作为JSF ListDataModel 暴露出去。 这简化了搜索界面的可单击列表的实现。在这个例子中,宾馆的列表是以名为 hotelsListDataModel 业务对话变量暴露给页面的。

(4)

EJB标准中的 @Remove 注解指定了一个有状态的Session Bean应该在注解的方法被调用后被删除且其状态应该被销毁。 在Seam里,所有有状态的Session Bean都应该定义一个标有 @Destroy @Remove 的方法。 这是Seam在销毁Session上下文时要调用的EJB删除方法。实际上 @Destroy 注解更有用,因为它能在Seam上下文结束时被用来做各种各样的清理工作。如果没有一个 @Destroy @Remove 方法,那么状态会泄露,你就会碰到性能上的问题。

应用程序的主页面是一个Facelets页面。让我们来看下与宾馆搜索相关的部分:

Example 1.23. 

<div class="section">
<h:form>

  <span class="errors">
    <h:messages globalOnly="true"/>
  </span>

  <h1>Search Hotels</h1>
  <fieldset>
     <h:inputText value="#{hotelSearch.searchString}" style="width: 165px;">
        <a:support event="onkeyup" actionListener="#{hotelSearch.find}"                  (1)
                   reRender="searchResults" />
     </h:inputText>
     &#160;
     <a:commandButton value="Find Hotels" action="#{hotelSearch.find}"
                      styleClass="button" reRender="searchResults"/>
     &#160;
     <a:status>                                                                          (2)
        <f:facet name="start">
           <h:graphicImage value="/img/spinner.gif"/>
        </f:facet>
     </a:status>
     <br/>
     <h:outputLabel for="pageSize">Maximum results:</h:outputLabel>&#160;
     <h:selectOneMenu value="#{hotelSearch.pageSize}" id="pageSize">
        <f:selectItem itemLabel="5" itemValue="5"/>
        <f:selectItem itemLabel="10" itemValue="10"/>
        <f:selectItem itemLabel="20" itemValue="20"/>
     </h:selectOneMenu>
  </fieldset>

</h:form>
</div>

<a:outputPanel id="searchResults">                                                       (3)
  <div class="section">
  <h:outputText value="No Hotels Found"
                rendered="#{hotels != null and hotels.rowCount==0}"/>
  <h:dataTable value="#{hotels}" var="hot" rendered="#{hotels.rowCount>0}">
    <h:column>
      <f:facet name="header">Name</f:facet>
      #{hot.name}
    </h:column>
    <h:column>
      <f:facet name="header">Address</f:facet>
      #{hot.address}
    </h:column>
    <h:column>
      <f:facet name="header">City, State</f:facet>
      #{hot.city}, #{hot.state}, #{hot.country}
    </h:column>
    <h:column>
      <f:facet name="header">Zip</f:facet>
      #{hot.zip}
    </h:column>
    <h:column>
      <f:facet name="header">Action</f:facet>
      <s:link value="View Hotel" action="#{hotelBooking.selectHotel(hot)}"/>             (4)
    </h:column>
  </h:dataTable>
  <s:link value="More results" action="#{hotelSearch.nextPage}"
          rendered="#{hotelSearch.nextPageAvailable}"/>
  </div>
</a:outputPanel>
(1)

Ajax4JSF的 <a:support> 标签允许一个JSF动作事件监听器在类似 onkeyup 这样的JavaScript事件发生时被异步的 XMLHttpRequest 调用。 更棒的是,reRender 属性让我们可以在收到异步响应时渲染一个JSF页面的片段并执行一个页面的局部修改。

(2)

Ajax4JSF的 <a:status> 标签使我们能在等待异步请求返回时显示一个简单的动画。

(3)

Ajax4JSF的 <a:outputPanel> 标签定义了一块能被异步请求修改的页面区域。

(4)

Seam的<s:link> 标签使我们能将一个JSF动作监听器附加在一个普通的(非JavaScript)HTML链接上。 用它取代标准JSF的 <h:commandLink> 的好处就是它在“在新窗口中打开”和“在新标签页中打开”时仍然有效。 值得注意的另一点就是我们用了一个绑定了参数的方法:#{hotelBooking.selectHotel(hot)}。 在标准的统一EL中这是不允许的,但Seam对EL的扩展进行了扩展,使表达式能够支持带参数的方法。

这个页面根据我们的键入动态地显示搜索结果,让我们选择一家宾馆并将它传给 HotelBookingActionselectHotel() 方法,这个对象才是 真正 有趣的地方。

现在让我们来看看宾馆预定范例程序是如何使用一个对话域的有状态的Session Bean的,这个Session Bean实现了业务会话相关持久化数据的天然缓存。 下面的代码很长。但如果你把它理解为实现业务会话的多个步骤的一系列动作的话,它是不难理解的。我们把这个类当作故事一样从头开始阅读。

Example 1.24. 

@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking
{

   @PersistenceContext(type=EXTENDED)                                                    (1)
   private EntityManager em;

   @In                                                                                   (2)
   private User user;

   @In(required=false) @Out
   private Hotel hotel;

   @In(required=false)
   @Out(required=false)
   private Booking booking;

   @In
   private FacesMessages facesMessages;

   @In
   private Events events;

   @Logger
   private Log log;

   @Begin                                                                                (3)
   public String selectHotel(Hotel selectedHotel)
   {
      hotel = em.merge(selectedHotel);
      return "hotel";
   }

   public String bookHotel()
   {
      booking = new Booking(hotel, user);
      Calendar calendar = Calendar.getInstance();
      booking.setCheckinDate( calendar.getTime() );
      calendar.add(Calendar.DAY_OF_MONTH, 1);
      booking.setCheckoutDate( calendar.getTime() );

      return "book";
   }

   public String setBookingDetails()
   {
      if (booking==null || hotel==null) return "main";
      if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) )
      {
         facesMessages.add("Check out date must be later than check in date");
         return null;
      }
      else
      {
         return "confirm";
      }
   }

   @End                                                                                  (4)
   public String confirm()
   {
      if (booking==null || hotel==null) return "main";
      em.persist(booking);
      facesMessages.add("Thank you, #{user.name}, your confimation number for #{hotel.name} is #{booking.id}");
      log.info("New booking: #{booking.id} for #{user.username}");
      events.raiseEvent("bookingConfirmed");
      return "confirmed";
   }

   @End
   public String cancel()
   {
      return "main";
   }

   @Destroy @Remove                                                                      (5)
   public void destroy() {}

}
(1)

这个bean使用EJB3的 扩展持久化上下文,所以任意实体实例在整个有状态Session Bean的生命周期中一直受到管理。

(2)

@Out 注解声明了一个属性值在方法调用后会 向外注入 到一个上下文变量中的。 在这个例子中,名为 hotel 的上下文变量会在每个动作监听器调用完成后被设置为 hotel 实例变量的值。

(3)

@Begin 注解表明被注解的方法开始一个 长期业务对话,因此当前业务对话上下文在请求结束后不会被销毁。相反,它会被关联给当前窗口的每次请求,在业务对话超时时或者一个 @End 方法被调用后销毁。

(4)

@End 注解表明被注解的方法被用来结束一个长期业务对话,所以当前业务对话上下文会在请求结束后被销毁。

(5)

这个EJB删除方法会在Seam销毁业务对话上下文时被调用。不要忘记定义该方法!

HotelBookingAction 包含了实现选择、预订和预订确认的所有动作监听器方法,并在它的实例变量中保存与之相关的状态。 我们认为你一定会同意这个代码比起获取和设置 HttpSession 的属性来说要简洁的多。

而且,一个用户能在每个登录Session中拥有多个独立的业务对话。试试吧!登录系统,执行搜索,在多个浏览器标签页中导航到不同的宾馆页面。 你能在同一时间建立两个不同的宾馆预约。如果某个业务对话被闲置太长时间,Seam最终会判其超时并销毁它的状态。如果在结束业务对话后, 你按了退回按钮回到那个会话的某一页,尝试执行一个动作,Seam会检测到那个业务对话已经被结束了,并将你重定向到搜索页面。

1.6.4. Seam的UI控制库

如果你查看下预订系统的WAR文件,你会在 WEB-INF/lib 目录中找到 seam-ui.jar。 这个包里有许多Seam的JSF自定义控件。本应用程序在从搜索界面导航到宾馆页面时使用了 <s:link> 控件:

<s:link value="View Hotel" action="#{hotelBooking.selectHotel}"/>

这里的 <s:link> 允许我们在不打断浏览器的“在新窗口打开”功能的情况下给HTML链接附加上一个动作监听器。 标准的JSF <h:commandLink> 无法在“在新窗口打开”的情况下正常工作。 稍后我们会看到 <s:link> 还能提供很多其他有用的特性,包括业务会话传播规则。

宾馆预订系统里还用了些别的Seam和Ajax4JSF控件,特别是在 /book.xhtml 页面里。我们在这里不深入讨论这些控件,如果你想看懂这些代码,请参考介绍Seam的JSF表单验证功能的章节。

1.6.5. Seam调试页面

WAR文件还包括了 seam-debug.jar。如果把这个jar部属在 WEB-INF/lib 下,结合Facelets,你能在 web.xml 或者 seam.properties 里设置如下的Seam属性:

<context-param>
    <param-name>org.jboss.seam.core.init.debug</param-name>
    <param-value>true</param-value>
</context-param>

这样就能访问Seam调试页面了。这个页面可以让你浏览并检查任意与你当前登录Session相关的Seam上下文中的Seam组件。 只需浏览 http://localhost:8080/seam-booking/debug.seam 即可。

1.7. 一个使用Seam和jBPM的完整范例:DVD商店

DVD商店程序演示了如何在任务管理和页面流中使用jBPM。

用户界面应用jPDL页面流实现了搜索和购物车功能。

管理员界面使用jBPM来管理订单的审批和送货周期。业务流程可以通过选择不同的流程定义实现动态改变。

TODO

dvdstore目录。

1.8. 结合Seam和Hibernate的范例:Hibernate预订系统

Hibernate预订系统是之前客房预订系统的另一个版本,它使用Hibernate和JavaBeans代替了会话Bean实现持久化。

TODO

hibernate目录。

1.9. 一个RESTful的Seam应用程序:Blog范例

Seam可以很方便地实现在服务器端保存状态的应用程序。 然而,服务器端状态在有些情况下并不合适,特别是对那些用来提供内容的功能。 针对这类问题,我们常需要让用户能够收藏页面,有一个相对无状态的服务器,这样一来能够在任何时间通过书签来访问那些被收藏的页面。 Blog范例演示了如何用Seam来实现一个RESTful的应用程序。应用程序中的每个页面都能被收藏,包括搜索结果页面。

Blog范例演示了“拉”风格("pull"-style)的MVC,它不使用动作监听器方法来获取数据和为视图准备数据,而是视图在被显示时从组件中拉数据。

1.9.1. 使用“拉”风格的MVC

index.xhtml Facelets页面中取出的片断显示了blog的最近文章列表:

Example 1.25. 

<h:dataTable value="#{blog.recentBlogEntries}" var="blogEntry" rows="3">
   <h:column>
      <div class="blogEntry">
         <h3>#{blogEntry.title}</h3>
         <div>
            <h:outputText escape="false"
                  value="#{blogEntry.excerpt==null ? blogEntry.body : blogEntry.excerpt}"/>
         </div>
         <p>
            <h:outputLink value="entry.seam" rendered="#{blogEntry.excerpt!=null}">
               <f:param name="blogEntryId" value="#{blogEntry.id}"/>
               Read more...
            </h:outputLink>
         </p>
         <p>
            [Posted on
            <h:outputText value="#{blogEntry.date}">
               <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
            </h:outputText>]
            &#160;
            <h:outputLink value="entry.seam">[Link]
               <f:param name="blogEntryId" value="#{blogEntry.id}"/>
            </h:outputLink>
         </p>
      </div>
   </h:column>
</h:dataTable>

如果我们通过收藏夹访问这个页面,那么 <h:dataTable> 的数据是怎么被初始化的呢? 事实上,Blog 是延迟加载的,即在需要时才被名为 blog 的Seam组件“拉”出来。 这与传统的基于动作的web框架(例如Struts)的控制流程正好相反。

Example 1.26. 

@Name("blog")
@Scope(ScopeType.STATELESS)
public class BlogService
{

   @In                                                                                   (1)
   private EntityManager entityManager;

   @Unwrap                                                                               (2)
   public Blog getBlog()
   {
      return (Blog) entityManager.createQuery("from Blog b left join fetch b.blogEntries")
            .setHint("org.hibernate.cacheable", true)
            .getSingleResult();
   }

}
(1)

这个组件使用了一个 受Seam管理的持久化上下文(seam-managed persistence context)。 与我们看过的其他例子不同,这个持久化上下文是由Seam管理的,而不是EJB3容器。 持久化上下文贯穿于整个Web请求中,这使得在视图里访问未抓取的关联数据时可以避免发生任何异常。

(2)

@Unwrap 注解告诉Seam将 Blog 而不是 BlogService 组件作为方法的返回值提供给客户端。 这是Seam的 管理员组件模式(manager component pattern)

这些看起来已经很不错了,那如何来收藏诸如搜索结果页这样的表单提交结果页面呢?

1.9.2. 可收藏的搜索结果页面

Blog范例在每个页面的右上方都有一个很小的表单,这个表单允许用户搜索文章。 这是定义在一个名为 menu.xhtml 的文件里的,它被Facelets模板 template.xhtml 所引用:

Example 1.27. 

<div id="search">
   <h:form>
      <h:inputText value="#{searchAction.searchPattern}"/>
      <h:commandButton value="Search" action="/search.xhtml"/>
   </h:form>
</div>

要实现一个可收藏的搜索结果页面,我们需要在处理搜索表单提交后执行一个浏览器重定向。 因为我们用JSF视图id作为动作输出,所以Seam会在表单提交后自动重定向到该表单id。除此之外,我们也能像这样来定义一个导航规则:

Example 1.28. 

<navigation-rule>
   <navigation-case>
      <from-outcome>searchResults</from-outcome>
      <to-view-id>/search.xhtml</to-view-id>
      <redirect/>
   </navigation-case>
</navigation-rule>

然后表单看起来会是这个样子的:

Example 1.29. 

<div id="search">
   <h:form>
      <h:inputText value="#{searchAction.searchPattern}"/>
      <h:commandButton value="Search" action="searchResults"/>
   </h:form>
</div>

在重定向时,我们需要将表单的值作为请求参数包括进来,得到的书签URL会是这个样子: http://localhost:8080/seam-blog/search.seam?searchPattern=seam。 JSF没有为此提供一个简单的途径,但Seam却有。我们能在 WEB-INF/pages.xml 中定义一个 页面参数

Example 1.30. 

<pages>
   <page view-id="/search.xhtml">
      <param name="searchPattern" value="#{searchService.searchPattern}"/>
   </page>
   ...
</pages>

这告诉Seam在重定向时将 #{searchService.searchPattern} 的值作为名字是 searchPattern 的请求参数包括进去,并在显示页面前重新将这个值赋上。

重定向会把我们带到 search.xhtml 页面:

Example 1.31. 

<h:dataTable value="#{searchResults}" var="blogEntry">
   <h:column>
      <div>
         <h:outputLink value="entry.seam">
            <f:param name="blogEntryId" value="#{blogEntry.id}"/>
            #{blogEntry.title}
         </h:outputLink>
         posted on
         <h:outputText value="#{blogEntry.date}">
            <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
         </h:outputText>
      </div>
   </h:column>
</h:dataTable>

此处同样使用“拉”风格的MVC来获得实际搜索结果:

Example 1.32. 

@Name("searchService")
public class SearchService
{

   @In
   private EntityManager entityManager;

   private String searchPattern;

   @Factory("searchResults")
   public List<BlogEntry> getSearchResults()
   {
      if (searchPattern==null)
      {
         return null;
      }
      else
      {
         return entityManager.createQuery("select be from BlogEntry be where lower(be.title) like :searchPattern or lower(be.body) like :searchPattern order by be.date desc")
               .setParameter( "searchPattern", getSqlSearchPattern() )
               .setMaxResults(100)
               .getResultList();
      }
   }

   private String getSqlSearchPattern()
   {
      return searchPattern==null ? "" : '%' + searchPattern.toLowerCase().replace('*', '%').replace('?', '_') + '%';
   }

   public String getSearchPattern()
   {
      return searchPattern;
   }

   public void setSearchPattern(String searchPattern)
   {
      this.searchPattern = searchPattern;
   }

}

1.9.3. 在RESTful应用程序中使用“推”风格("push"-style)的MVC

有些时候,用“推”风格的MVC来处理RESTful页面更有意义,为此Seam提供了 页面动作。 Blog范例在文章页面 entry.xhtml 里使用了页面动作。请注意这里是故意这么做的,因为此处使用“拉”风格的MVC会更容易。

entryAction 组件工作起来非常像传统“推”风格MVC的面向动作框架例如Struts里的动作类(action class):

Example 1.33. 

@Name("entryAction")
@Scope(STATELESS)
public class EntryAction
{
   @In(create=true)
   private Blog blog;

   @Out
   private BlogEntry blogEntry;

   public void loadBlogEntry(String id) throws EntryNotFoundException
   {
      blogEntry = blog.getBlogEntry(id);
      if (blogEntry==null) throw new EntryNotFoundException(id);
   }

}

pages.xml 里也定义了页面动作:

Example 1.34. 

<pages>
   ...

   <page view-id="/entry.xhtml" action="#{entryAction.loadBlogEntry(blogEntry.id)}">
      <param name="blogEntryId" value="#{blogEntry.id}"/>
   </page>

   <page view-id="/post.xhtml" action="#{loginAction.challenge}"/>

   <page view-id="*" action="#{blog.hitCount.hit}"/>

</pages>

范例中还将页面动作运用于一些其他的功能上 — 登录和页面访问记数器。另外一点值得注意的是在页面动作绑定中使用了一个参数。 这不是标准的JSF EL,是Seam为你提供的,你不仅能在页面动作中使用它,还可以将它使用在JSF方法绑定中。

entry.xhtml 页面被请求时,Seam先为模型绑定上页面参数 blogEntryId,然后运行页面动作,该动作获取所需的数据 — blogEntry — 并将它放在Seam事件上下文中。最后显示以下内容:

Example 1.35. 

<div class="blogEntry">
   <h3>#{blogEntry.title}</h3>
   <div>
      <h:outputText escape="false" value="#{blogEntry.body}"/>
   </div>
   <p>
      [Posted on&#160;
      <h:outputText value="#{blogEntry.date}">
         <f:convertDateTime timezone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
      </h:outputText>]
   </p>
</div>

如果在数据库中没有找到blog entry,就会抛出 EntryNotFoundException 异常。 我们想让该异常引起一个404错误,而非505,所以为这个异常类添加个注解:

Example 1.36. 

@ApplicationException(rollback=true)
@HttpError(errorCode=HttpServletResponse.SC_NOT_FOUND)
public class EntryNotFoundException extends Exception
{
   EntryNotFoundException(String id)
   {
      super("entry not found: " + id);
   }
}

该范例的另一个实现在方法绑定中没有使用参数:

Example 1.37. 

@Name("entryAction")
@Scope(STATELESS)
public class EntryAction
{
   @In(create=true)
   private Blog blog;

   @In @Out
   private BlogEntry blogEntry;

   public void loadBlogEntry() throws EntryNotFoundException
   {
      blogEntry = blog.getBlogEntry( blogEntry.getId() );
      if (blogEntry==null) throw new EntryNotFoundException(id);
   }

}
<pages>
   ...

   <page view-id="/entry.xhtml" action="#{entryAction.loadBlogEntry}">
      <param name="blogEntryId" value="#{blogEntry.id}"/>
   </page>

   ...
</pages>

你可以根据自己的喜好来选择实现。

Chapter 2. 用Seam-gen起步

Seam的发布包里已包含了命令行工具,用它可以很方便地搭建Eclipse项目,以及生成一些简单的Seam骨架代码,并能从已存在的数据库反向工程到应用程序。

它能让你感受到Seam给开发所带来的快捷,当你在电梯里看到那些令人厌烦的Ruby家伙在吹嘘他们的新玩艺儿是如何优美地在应用中 将繁琐的数据放进数据库时,你就可以取笑他们了。

在此版本中,seam-gen能很好地与JBoss AS一起工作。通过对项目配置进行些许的手工修改,seam-gen生成的项目就可与其它J2EE或Java EE 5应用服务器一起工作。

请注意,并不只限在Eclipse中使用seam-gen。但在本教程中,我们将为你演示如何在Eclipse中用它来完成调试与集成测试。 若你不想安装Eclipse,你仍可跟随教程的步骤,因为所有的操作都是在命令行中完成的。

Seam-gen的Ant脚本与Hibernate工具包放一起,并同时提供了一些模板。这样我们就很容易地根据自己项目的需要来作些修改。

2.1. 准备活动

请确保已安装了JDK 5或者JDK6,JBoss AS 4.2和Ant 1.6,以及较新版的Eclipse、JBoss IDE和TestNG的Eclipse 插件。 在Eclipse的JBoss Server View中将JBoss安装路径添加进去。然后以debug模式启动JBoss,并在弹出式命令窗口中进入Seam的目录。

JBoss很好地支持WAR和EAR的热重部署,但麻烦的是,由于在JVM中存在着多个Bug,在开发进程中多次的重部署EAR是常见的事,但这最终会耗尽PermGen 空间(Permanent Generation Space)。 因此建议你在开发的过程中加大perm gen空间。若你是在JBoss IDE中运行JBoss,那你就可以在服务器运行配置中的VM arguments进行配置,建议依此修改:

-Xms512m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=512

如果你没有那么多的可用内存,你只好用我们推荐的最小内存了:

-Xms256m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256

若是在命令行模式中运行JBoss,那你就要在 bin/run.conf 文件中对JVM选项作修改了。

当然,我们可以先不理会这些。当你在开发中第一次碰到 OutOfMemoryException 异常时再回过头来作此修改。

2.2. 建立一个新的Eclipse项目

首先,我们需要根据现有的开发环境对seam-gen进行配置:JBoss AS安装目录、Eclipse workspace及数据库连接。这些都很容易,只要敲入:

cd jboss-seam-2.0.x
seam setup

根据弹出的提示输入开发环境的相关信息:

C:\Projects\jboss-seam>seam setup
Buildfile: build.xml

setup:
    [echo] Welcome to seam-gen :-)
    [input] Enter your Java project workspace [C:/Projects]

    [input] Enter your JBoss home directory [C:/Program Files/jboss-4.2.0.GA]

    [input] Enter the project name [myproject]
helloworld
    [input] Is this project deployed as an EAR (with EJB components) or a WAR (with no EJB support) [ear] (ear,war,)

    [input] Enter the Java package name for your session beans [com.mydomain.helloworld]
org.jboss.helloworld
    [input] Enter the Java package name for your entity beans [org.jboss.helloworld]

    [input] Enter the Java package name for your test cases [org.jboss.helloworld.test]

    [input] What kind of database are you using? [hsql] (hsql,mysql,oracle,postgres,mssql,db2,sybase,)
mysql
    [input] Enter the Hibernate dialect for your database [org.hibernate.dialect.MySQLDialect]

    [input] Enter the filesystem path to the JDBC driver jar [lib/hsqldb.jar]
../../mysql-connector.jar
    [input] Enter JDBC driver class for your database [com.mysql.jdbc.Driver]

    [input] Enter the JDBC URL for your database [jdbc:mysql:///test]

    [input] Enter database username [sa]
gavin
    [input] Enter database password []

    [input] skipping input as property hibernate.default_schema.new has already been set.
    [input] Enter the database catalog name (it is OK to leave this blank) []

    [input] Are you working with tables that already exist in the database? [n] (y,n,)
y
    [input] Do you want to drop and recreate the database tables and data in import.sql each time you deploy? [n] (y,n,)
n
[propertyfile] Creating new property file: C:\Projects\jboss-seam\seam-gen\build.properties
     [echo] Installing JDBC driver jar to JBoss server
     [echo] Type 'seam new-project' to create the new project

BUILD SUCCESSFUL
Total time: 1 minute 17 seconds
C:\Projects\jboss-seam>

该工具提供了相应的默认值,因此你可以直接按Enter键。

最重要的是你要对EAR部署还是WAR部署进行选择。EAR项目支持EJB 3.0 并需要Java EE 5。而WAR包不支持EJB 3.0,但可在J2EE环境中部署。 另外WAR也更较简单,便于理解。假若你已安装了EJB3 profile,那你就用ear好了,否则,就只好用 war。 在此假设我们选择了EAR部署,当然此教程也适用于WAR部署。

如果你手上有现成的数据模型,请确保你已输入现有数据库的表名。

这些设置保存在 seam-gen/build.properties 文件中,但你可通过运行 seam setup 来再次修改。

现在我们就可以在Eclipse workspace目录中创建一个新的项目,只需输入:

seam new-project
C:\Projects\jboss-seam>seam new-project
Buildfile: build.xml

validate-workspace:

validate-project:

copy-lib:
     [echo] Copying project jars ...
     [copy] Copying 58 files to C:\Projects\helloworld\lib
     [copy] Copying 9 files to C:\Projects\helloworld\embedded-ejb

file-copy-war:

file-copy-ear:
     [echo] Copying resources needed for EAR deployment to the C:\Projects\helloworld/resources directory...

new-project:
     [echo] A new Seam project named 'helloworld' was created in the C:\Projects directory
     [echo] Type 'seam explode' and go to http://localhost:8080/helloworld
     [echo] Eclipse Users: Add the project into Eclipse using File > New > Project and select General > Project (not Java Project)
     [echo] NetBeans Users: Open the project in NetBeans

BUILD SUCCESSFUL
Total time: 7 seconds
C:\Projects\jboss-seam>

这组操作复制了Seam jar文件及相应的jar文件与JDBC驱动jar到新建的Eclipse项目中,并生成了所需的源文件及其配置文件、一个模板文件和样式文件,及相应的Eclipse元数据及Ant构建脚本。 只要你依此操作 New -> Project... -> General -> Project -> Next,输入Project name (此例为helloworld),并接着点击 Finish,就可将Eclipse项目自动部署到JBoss AS分解式的(exploded)目录结构中,请不要在新项目向导中选择 Java Project

若Eclipse中的默认的JDK不是Java SE 5 或Java SE 6,你就得通过 Project -> Properties -> Java Compiler 来选择与Java SE 5 兼容的JDK。

另外,可在Eclipse之外输入 seam explode 来部署项目。

http://localhost:8080/helloworld 中查看此应用的首页。 view/home.xhtml 是个使用 view/layout/template.xhtml 模板生成的Facelets 页面,试着在Eclipse中编辑此页面或该模板,并在浏览器中刷新页面,立即看到结果。

别被在项目目录中的如此多的XML配置文件给吓晕了。那都是些标准的Java EE的东西,它们只需生成一次就不用再去理会了。 在所有的Seam项目中,90%的配置内容都是一样的(这些可由seam-gen来帮我们完成)。

新生成的项目包含了三个数据库及持久化配置文件。jboss-beans.xmlpersistence-test.xmlimport-test.sql 文件是用在当TestNG对HSQLDB进行单元测试时。 在 import-test.sql 中的数据库Schema及其测试数据总是在测试前就已转入数据库中。 myproject-dev-ds.xmlpersistence-dev.xmlimport-dev.sql 文件是在部署应用到开发数据库时使用的。 数据库schema是否可在部署时自动导出,取决于你是否在设置seam-gen环境时配置了已存在的数据库。 myproject-prod-ds.xmlpersistence-prod.xmlimport-prod.sql 文件是在部署应用到生产数据库时使用的。在部署时数据库schema并不自动导出。

2.3. 创建新动作

若你熟知传统的action-style Web框架,你或许想知道在Java中如何来创建无状态action方法的简单Web页面。如果你输入:

seam new-action

则Seam将弹出一些信息并为你的项目生成新的Facelets页面及Seam组件。

C:\Projects\jboss-seam>seam new-action
Buildfile: build.xml

validate-workspace:

validate-project:

action-input:
    [input] Enter the Seam component name
ping
    [input] Enter the local interface name [Ping]

    [input] Enter the bean class name [PingBean]

    [input] Enter the action method name [ping]

    [input] Enter the page name [ping]


setup-filters:

new-action:
     [echo] Creating a new stateless session bean component with an action method
     [copy] Copying 1 file to C:\Projects\helloworld\src\action\org\jboss\helloworld
     [copy] Copying 1 file to C:\Projects\helloworld\src\action\org\jboss\helloworld
     [copy] Copying 1 file to C:\Projects\helloworld\src\action\org\jboss\helloworld\test
     [copy] Copying 1 file to C:\Projects\helloworld\src\action\org\jboss\helloworld\test
     [copy] Copying 1 file to C:\Projects\helloworld\view
     [echo] Type 'seam restart' and go to http://localhost:8080/helloworld/ping.seam

BUILD SUCCESSFUL
Total time: 13 seconds
C:\Projects\jboss-seam>

新增Seam组件后,我们需要重启分解式目录部署(exploded directory deployment)。 输入seam restart,或在Eclipse中已生成项目的 build.xml 中运行 restart target就可完成。 另一种方式是在Eclipse中通过编辑 resources/META-INF/application.xml 文件来强制重启。 请注意,在每次修改应用程序时并不需要重启JBoss。

试着在浏览器中输入 http://localhost:8080/helloworld/ping.seam 地址并点击按钮,看看发生了什么。 在项目的 src 目录中可看到完成此动作的源代码。 试着在 ping() 方法中设置个断点,再次点击按钮,又发生了什么?

最后,在测试包中找到 PingTest.xml 文件,并用Eclipse的TestNG插件来运行测试。 此外,还可用 seam test 或生成的build文件中的 test target来运行测试。

2.4. 创建有动作的表单(form)

下一步就是来创建表单了。请输入:

seam new-form
C:\Projects\jboss-seam>seam new-form
Buildfile: C:\Projects\jboss-seam\seam-gen\build.xml

validate-workspace:

validate-project:

action-input:
    [input] Enter the Seam component name
hello
    [input] Enter the local interface name [Hello]

    [input] Enter the bean class name [HelloBean]

    [input] Enter the action method name [hello]

    [input] Enter the page name [hello]


setup-filters:

new-form:
     [echo] Creating a new stateful session bean component with an action method
     [copy] Copying 1 file to C:\Projects\hello\src\com\hello
     [copy] Copying 1 file to C:\Projects\hello\src\com\hello
     [copy] Copying 1 file to C:\Projects\hello\src\com\hello\test
     [copy] Copying 1 file to C:\Projects\hello\view
     [copy] Copying 1 file to C:\Projects\hello\src\com\hello\test
     [echo] Type 'seam restart' and go to http://localhost:8080/hello/hello.seam

BUILD SUCCESSFUL
Total time: 5 seconds
C:\Projects\jboss-seam>

再次重启应用程序,并在浏览器中输入 http://localhost:8080/helloworld/hello.seam 就可看到结果了。 接着看下所生成的代码,并运行测试。试着给表单加入一些字段及Seam组件(记着在每次更改Java代码时重新部署)。

2.5. 从现有数据库生成应用程序

在数据库中手工创建一些表。(如果你需要切换不同的数据库,只需再次运行 seam setup 即可。)现请输入:

seam generate-entities

接着重新部署,并在浏览器中输入 http://localhost:8080/helloworld 就可看到结果了。 你可以试着浏览数据库,编辑现有的对象,并创建新的对象。如果你看下所生成的代码,你可能会对如此简单的代码感到惊讶。 让开发人员,尤其是那些不甘于受Seam-gen摆布的开发人员,简单地手工编写数据访问代码,是Seam的设计目标之一。

2.6. 将应用部署为EAR

最后,我们想知道能否用标准的Java EE包来部署应用。首先,通过运行 seam unexplode 来移走分解式目录(exploded directory)。 在命令行中输入 seam deploy 或运行生成的Build脚本文件中的 deploy target就可完成EAR的部署,用 seam undeploy 命令或运行 undeploy 目标可卸下EAR。

默认情况下,应用程序会用 dev profile 来部署,EAR将包含persistence-dev.xmlimport-dev.sql文件,myproject-dev-ds.xml 文件也会被部署。 通过输入以下的命令你就可以更改profile, 并可使用 prod profile

seam -Dprofile=prod deploy

你甚至可以给你的应用程序定义新的部署profile,只需在项目中加入合适的文件, 例如:persistence-staging.xmlimport-staging.sqlmyproject-staging-ds.xml — 并选择使用了 -Dprofile=staging 名字的profile。

2.7. Seam与增量热部署

将Seam应用部署成exploded目录的好处是,你能在开发时得到增量热部署的支持。 你只需在 components.xml 中添加这一行来启用Seam和Facelet中的debug模式即可:

<core:init debug="true"/>

这样一来,重新部署以下文件时就不一定要完全地重启web应用了:

  • 任意Facelet页面

  • 任意 pages.xml 文件

若想对Java代码进行变更,就需要完全的应用重启。 (在JBoss中,对于EAR部署,这需要用touch命令改变顶层的部署描述文件:对于EAR部署,则是application.xml,而对于WAR部署,则是web.xml 。)

但你真正想加快编辑/编译/测试的流程,Seam支持对JavaBean组件进行增量式重部署。 为了用上此功能,你必须把JavaBean组件部署到 WEB-INF/dev 目录中,以便它们能被特殊的Seam类加载器加载,而不是WAR或EAR类加载器。

请注意以下的限制:

  • 必须是JavaBean组件,而不能是EJB3 Beans(此限制正在解决中)

  • 实体Bean不可热部署

  • 通过 components.xml 部署的组件可能无法热部署

  • WEB-INF/dev 之外部署的任何类都无法访问可热部署的组件

  • 须启用Seam的debug模式

如果你用Seam-gen创建WAR项目,增量热部署对于src/action目录下的类是直接可用的,但是对于EAR项目不行。

2.8. 在Jboss 4.0下使用Seam

Seam 2.0是针对JavaServer Faces 1.2开发的,所以我们推荐在JBoss 4.2下使用Seam,因为它包含了JSF 1.2参考实现。 然而仍然有办法在Jboss 4.0下使用Seam。需要两个步骤:安装启用了EJB3的Jboss 4.0版本并且把MyFaces替换为JSF1.2参考实现。 你完成这两个步骤后,Seam2.0就可以在JBoss 4.0下部署了。

2.8.1. 安装JBoss 4.0

JBoss 4.0没有针对Seam的默认配置。想要运行Seam,你必须用JEMS 1.2安装器并且选择EJB3 profile。 如果没有EJB3支持Seam是不能正常运行的。JEMS安装器可以在这里下载: http://labs.jboss.com/jemsinstaller/downloads

2.8.2. 安装JSF 1.2 RI

JBoss 4.0的配置可以在 server/default/deploy/jbossweb-tomcat55.sar 找到。 你需要从 jsf-libs 目录删除 myfaces-api.jarmyfaces-impl.jar文件。 你还需要把 jsf-api.jarjsf-impl.jarel-api.jarel-ri.jar 复制到那个目录下。你可以在Seam的lib文件夹下找到这些JAR文件。EL JAR文件可以从Seam 1.2发行版中获取。

你还需要编辑 conf/web.xml 文件,把 myfaces-impl.jar 替换为 jsf-impl.jar

Chapter 3. 上下文相关的组件模型

Seam中的两个核心概念是 context(上下文)思想component(组件)思想。组件是具有状态的对象,通常是EJB,组件的实例会和上下文绑定,在此上下文中具有一个名字。Bijection(双向注入)可以将内部的组件名(实例变量名)别名为上下文相关的名字,允许Seam动态组装组件树,还可以重新组装。

让我们从了解Seam内置的上下文开始。

3.1. Seam上下文

Seam上下文是由框架创建和销毁的。应用程序不能通过显式的Java API调用来控制上下文划分。上下文通常是隐含的。然而,在某些情况下,上下文可以通过annotation(注解)划分。

基本的Seam上下文有:

  • Stateless context

  • Event (or request) context

  • Page context

  • Conversation context

  • Session context

  • Business process context

  • Application context

你可能在servlet及相关规范中已经见过其中一些上下文了,但其中有两个可能从未见过:conversation context(业务对话上下文),和 business process context(业务流程上下文)。 在Web应用程序中,状态管理如此凌乱和容易出错的原因就是,内置的三个上下文(request, session 和application)从业务逻辑的角度来看不是很有意义。 例如,用户登录session的构建,对应用实际的工作流程来说就是相当随意的。 因此,大部分的Seam组件被限定在业务会话或者业务流程上下文中,因为这些上下文从应用的角度来说最有意义。

让我们按顺序来考察每个context(上下文)。

3.1.1. Stateless context(无状态上下文)

那些确实没有状态的组件(主要是无状态Session Bean)总是运行在无状态上下文中(实际上就是上下文无关)。 无状态组件没什么太大的意思,也有争议认为它们不十分面向对象。但不管怎么样,它们还是很重要,并且通常很有用。

3.1.2. Event context(事件上下文)

事件上下文是“最窄”的有状态上下文,是Web Request 上下文的泛化,用以包含其他种类的事件。 然而,与JSF请求的生命周期相关联的事件上下文是事件上下文最重要的实例,并且也是你最常打交道的。 与事件上下文相关联的组件在请求结束时被销毁,但是它们的状态至少在请求的生命周期中是存在并且是定义良好的。

当你通过RMI或者Seam Remoting调用Seam组件的时候,一个事件上下文将为这个调用而被创建和销毁。

3.1.3. Page context(页面上下文)

页面上下文允许你将状态与一个渲染页面的实例相关联。 你可以在Event Listener中初始化状态,或者在实际渲染页面的时候初始化状态,任何源于该页面的事件都可以访问到这些状态。 这在支持像可点击列表这种的功能时特别有用,列表的内容通过服务器端的数据变化产生。 实际上状态被序列化到了客户端,因此在多窗口操作或者回退按钮的时候,这种结构是非常健壮的。

3.1.4. Conversation context(业务会话上下文)

业务会话上下文是Seam中最核心的概念。conversation(业务会话)是从用户的视角看待的一个工作单元。 它可能跨越与用户交互的多个Servlet、多个请求,和多个数据库事务。但是对用户来说,一个业务会话解决一个单一的问题。 例如说:“预订酒店”,“批准合同”,“创建订单”都是业务会话。 你可以将业务会话理解成对一个“use case(用例)”或“user story(用户故事)”的实现,当然特定的业务关联并非与此类例子完全一致。

业务会话保存关于“在此窗口中,用户正在干什么”的状态。在任何时间,一个用户可能同时位于多个业务会话活动中,一般是在几个不同窗口中。 业务会话上下文让我们可以确保不同业务会话的状态不会互相干扰,不会导致Bug。

你可能要花上一点时间才能习惯以这一业务会话的观点来思考你的应用程序。 但一旦你习惯于它,你会喜欢上这个术语,并且再不会不用业务会话来思考了!

一些业务会话仅存在在一次请求中。跨域多个请求的业务会话必须通过Seam提供的annotation注解来划分。

一些业务会话同时也是tasks(任务)。任务是一种业务会话,它特指一个长时间运行的业务流程,当正确完成后,可能会触发一个业务流程状态的转换。Seam为任务划分提供了专门的注解。

业务对话可以是nested(嵌套)的,一个业务对话嵌套“在”一个更大的业务对话中。这是一项高级特性。

通常,业务对话状态实际上由Seam保存在Servlet Session 中,跨越请求。Seam实现了可配置的 conversation timeout,可以自动销毁不活动的业务会话,这就可以确保,如果用户取消对话,用户的登录Session中保存的状态不会无限增长。

对于在一个长时间运行的业务会话中所产生的并发请求,Seam按顺序执行。

除此之外,Seam也可以配置成把对话状态保存在客户端浏览器中。

3.1.5. Session context(Session上下文)

Session上下文保存与用户登录session相关联的状态。虽然当需要在多个业务会话中交换状态的时候这很有用,但我们一般不建议使用Session 上下文保存组件,除非是保存有关登录用户的全局信息。

在JSR-168 Portal环境下,Session上下文代表Portlet上下文。

3.1.6. Business process context (业务流程上下文)

业务流程上下文保存了长时间运行的业务流程相关的状态。这种状态由BPM引擎(jBPM)管理和持久化。 业务流程跨越多个用户的交互,因此状态在多个用户之间通过良好定义的方式共享。 当前的任务决定当前的业务流程实例,业务流程的生命周期通过外置的 process definition language(流程定义语言) 来定义,因此没有特别的annotation注解用于划分业务流程。

3.1.7. Application context(应用上下文)

Application上下文就是Servlet规范中的Servlet上下文。应用程序上下文在保存静态信息方面有用,例如配置数据,引用数据或者元模型。 例如,Seam把自己的配置和元模型保存在应用程序上下文中。

3.1.8. Context variables(上下文变量)

上下文定义了命名空间,一组 context variables(上下文变量)。 这些工作很类似Servlet规范中对Session或Request attributes的定义。 你可以绑定任何你喜欢的值到Context Variable,但通常我们会绑定Seam组件实例到Context Variables。

因此,在上下文中,组件实例是通过上下文变量名字来辨别的(通常是这样,但并非绝对,就和组件名称一样)。 你可以通过程序在特定范围内访问被命名的组件实例,这是通过 Contexts 类进行的,它提供了对 Context 接口的几个线程绑定的实例的访问:

User user = (User) Contexts.getSessionContext().get("user");

你也可以通过名字来设置或修改变量值:

Contexts.getSessionContext().set("user", user);

但通常,我们通过注射(injection)来从上下文中获得组件,并且通过反向注射(outjection)把组件实例返回上下文。

3.1.9. Context搜索优先级

有时候如上面的例子所示,组件实例是从某个特定的已知范围内获取的。 其他的时候则是通过 priority order(优先级顺序) 在所有有状态范围内搜寻。这个顺序是这样的:

  • Event context

  • Page context

  • Conversation context

  • Session context

  • Business process context

  • Application context

你可以通过调用 Contexts.lookupInStatefulContexts() 来执行带优先级的搜索。你在JSF页面中通过名字访问组件的时候,执行的就是这种带优先级的搜索。

3.1.10. 并发模型

Servlet和EJB规范都没有定义任何关于如何管理来自同一个客户端的并发请求的条款。 Servlet容器简单地让所有的线程并发运行,把线程资源安全共享的任务交给应用程序代码。 EJB容器允许无状态组件并发访问,但如果并发访问一个有状态Session Bean,就会抛出一个异常。

旧式的Web应用程序是围绕细粒度的同步请求编写的,因此这种行为可能还OK。 但是对现代的程序而言,由于大量使用了很多细粒度的异步(AJAX)请求,并发是实际存在的,并且必须被程序模型支持。 Seam在其上下文模型中加入了并发管理层。

Seam Session 和应用上下文是多线程的。Seam允许在一个上下文中并发请求,并发处理。事件(Event)和页面(Page)上下文自然是单线程的。 业务流程(business process)上下文严格而言是多线程的,但实际情况中并发很少见,因此大多数情况不会出现并发。 最后,Seam为Conversation Context提供了 每对话每进程单线程 模型,这是通过把同一个长时间运行的对话上下文中的并发请求序列化实现的。

因为Session上下文是多线程的,并且经常包含不稳定的状态,所以Session范围内的组件总是被Seam保护以防止并发操作。 Seam默认把针对Session范围的Session Bean和JavaBean的请求序列化(并且检测、解决任何发生的死锁)。 对Application Scoped的组件来说,这却不是默认行为,因为Application Scoped的组件通常不会包含的不稳定状态,并且在全局级别进行同步代价 极其 高昂。但是,你可以强制对任何Session Bean或JavaBean组件采用序列化的线程模型,要做的就是加上 @Synchronized 注解。

并发模型意味着AJAX客户端可以安全的使用不稳定的Session和会话状态,并且不需要开发者做任何特别的工作。

3.2. Seam 组件

Seam组件是POJO(Plain Old Java Objects)。特别地,他们是JavaBean或者EJB 3.0 enterprise bean。Seam并不强求组件是EJB,甚至可以不使用EJB 3.0兼容的容器,Seam在设计的时候处处考虑对EJB 3.0的支持,并且包含对EJB 3.0的深度整合。

  • EJB 3.0 stateless Session Beans

  • EJB 3.0 stateful Session Beans

  • EJB 3.0 entity beans

  • JavaBeans

  • EJB 3.0 message-driven beans

3.2.1. 无状态Session Bean

无状态Session Bean组件无法在多次调用之间保持状态。因此,它们通常在不同的Seam上下文中,操作其他组件的状态。他们可以作为JSF的action listener,但是不能为JSF组件的显示提供属性。

因为每次请求都产生一个新的实例,无状态session bean可以并发访问。把其实例和请求相关联是EJB3容器的责任(通常这些实例会从一个可重用的池中分配,所以你可能会发现某些实例变量还保存着上次使用的痕迹。)

无状态Session Bean总是生活在无状态上下文中。

无状态Session Bean是Seam组件中最没趣的了。

Seam无状态Session Bean组件可以使用 Component.getInstance() 或者 @In(create=true) 实例化。它们不能直接使用JNDI或者 new 操作实例化。

3.2.2. 有状态Session Bean

有状态Session Bean不仅可以在bean的多次调用之间保持状态,而且在多次请求之间也可以保持状态。 不由数据库保存的状态通常应该由有状态Session Bean保持。这是Seam和其他web框架之间的一个显著的不同点。 其他框架把当前会话的信息直接保存在 HttpSession 中,而在Seam中你应该把它们保存在有状态Session Bean的实例中,该实例被绑定到会话上下文。这可以让Seam来替你管理状态的生命周期,并且保证在多个不同的并发会话中没有状态冲突。

有状态Session Bean经常被作为JSF action listener使用,也可以作为JSF显示或者form提交的backing bean(支持bean ,或称后台bean),提供属性供组件访问。

默认情况下,有状态Session Bean会被绑定到Conversation Context。它们绝不会绑定到page或stateless context。

对Session范围的有状态Session Bean的并发请求,会被Seam按顺序串行处理。

Seam有状态Session Bean组件可以使用 Component.getInstance() 或者 @In(create=true) 实例化。它们不能直接使用JNDI或者 new 操作实例化。

3.2.3. 实体Bean

实体Bean可以被绑定到上下文变量,起到Seam组件的作用。因为Entity除了上下文标识之外,还有持久标识,Entity实体通常明确的由Java Code绑定,而非由Seam隐性初始化。

Entity Bean实体不支持双向注入或者上下文划分。对Entity Bean的调用也不会触发验证。

Entity Bean通常不作为JSF的action listener使用,但经常作为JSF组件用于显示或者form提交的后台bean,提供属性功能。 特别是,当Entity作为后台Bean的时候,它会和一个无状态Session Bean扮演的action listener联用,来实现CRUD之类的功能。

默认情况下,Entity Bean被绑定到Conversation Context。他们永远不能被绑定到无状态Context。

注意,在集群环境中,把Entity Bean直接绑定到Conversation或者Session范围的Seam上下文变量,与在有状态Session Bean中保持一个对Entity Bean的引用相比,性能比较差。因此,并非所有的Seam应用程序都会把Entity Bean定义为Seam组件。

Seam实体Bean组件可以使用 Component.getInstance()@In(create=true) 或者直接使用 new 操作来实例化。

3.2.4. JavaBeans

JavaBeans可以像无状态或者有状态Session Bean那样使用。但是,它们不能提供Session Bean那么多的功能(声明式事务划分、声明式安全性、高效的集群状态复制、EJB 3.0持久化、超时方法等等)。

在后面有一章,我们会展示如何在没有EJB容器的情况下使用Seam和Hibernate。此时,组件是JavaBeans,而非Session Beans。 但是注意,在很多应用服务器中,对Conversation或Session 范围的Seam JavaBean组件集群操作,要比对有状态Session Bean组件集群慢。

默认,JavaBeans是绑定到Event Context的。

对Session范围的JavaBeans的并发请求总是会被Seam转化为串行执行。

Seam JavaBean组件可以使用 Component.getInstance()@In(create=true) 或者直接使用 new 操作来实例化。

3.2.5. 消息驱动Bean

消息驱动Bean通常作为Seam组件。但是,消息驱动Bean与其他Seam组件的调用方式非常不同——它们并非通过Context变量调用,它们会监听发送到JMS Queue或者Topic的消息。

消息驱动Bean不能被绑定到Seam上下文。它们也不能访问它们的“调用者”的Session或者会话状态。但是,它们支持双向注入和一些其他的Seam功能。

消息驱动Bean不会被应用实例化,它是在接受到一条消息时由EJB容器来完成实例化的。

3.2.6. 拦截

为了表演Seam的魔术(双向注入,上下文划分,校验等),它必须对组件调用进行拦截。 对JavaBean而言,Seam可以完全控制组件的初始化,不需要特别的配置。对于Entity Bean,也不需要拦截器,因为双向注入和上下文划分不起作用。 对Session Bean,我们必须为它注册EJB拦截器。我们可以使用注解,比如:

@Stateless
@Interceptors(SeamInterceptor.class)
public class LoginAction implements Login {
    ...
}

但是更好的办法是在 ejb-jar.xml 中定义拦截器。

<interceptors>
   <interceptor>
      <interceptor-class>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
   </interceptor>
</interceptors>

<assembly-descriptor>
   <interceptor-binding>
      <ejb-name>*</ejb-name>
      <interceptor-class>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
   </interceptor-binding>
</assembly-descriptor>

3.2.7. 组件名字

所有Seam组件都需要名字。我们可以通过 @Name 注解来命名组件:

@Name("loginAction")
@Stateless
public class LoginAction implements Login {
    ...
}

这个名字是 seam component name,和EJB规范定义的任何其他名字都没有关系。 但是,Seam组件名字就相当于JSF管理的Bean Name的名字,因此,可以理解为这两个概念是等同的。

@Name 不是定义组件名称的唯一方式,但是我们总得要在 某个地方 来指定名字。 否则,Seam 所有的注解部分就无法工作。

就如同在JSF中,Seam组件实例绑定成上下文变量时,其名字通常和组件名相同。 因此,例如我们可以通过 Contexts.getStatelessContext().get("loginAction") 来访问 LoginAction。 特别是,不管Seam自己何时初始化一个组件,它将这个新实例以组件的名字绑定成一个变量。 但是,又和JSF一样,应用程序也可以把组件绑定成其他的上下文变量,只需通过API编程调用。 例如,当前登录的用户(User)可以被绑定成为Session上下文中的 currentUser 变量,而同时,另一个用作某种管理功能的用户则被绑定成对话上下文的 user 变量。

对非常大型的应用程序,经常使用全限定名;内置的Seam组件就是这样。

@Name("com.jboss.myapp.loginAction")
@Stateless
@Interceptors(SeamInterceptor.class)
public class LoginAction implements Login {
    ...
}

我们可以在Java代码和JSF表达式语言中使用全限定的组件名称。

<h:commandButton type="submit" value="Login"
                 action="#{com.jboss.myapp.loginAction.login}"/>

这很啰嗦,Seam也提供了把全限定名简写的办法。在 components.xml 文件中加入类似这样的一行:

<factory name="loginAction" scope="STATELESS" value="#{com.jboss.myapp.loginAction}"/>

所有的Seam内置组件都有全限定名,但大多数都在Seam jar文件的 components.xml 中简写为简单的名字。

3.2.8. 定义组件范围(Defining the Component Scope)

我们可以使用 @Scope 注解来覆盖默认的组件范围(上下文)。这可以让我们定义组件实例被Seam初始化后绑定到的具体上下文。

@Name("user")
@Entity
@Scope(SESSION)
public class User {
    ...
}

org.jboss.seam.ScopeType定义了可能范围的枚举.

3.2.9. 具有多个角色的组件(Components with multiple roles)

有些Seam组件类可以在系统中具有多个角色。例如,我们经常有一个 User 类用作Session-Scoped组件,代表当前用户,同时它又在用户管理界面中被用作Conversation-Scoped组件。@Role 注解让我们可以定义组件在另一个范围中的额外角色名 —— 这可以让我们把相同的组件类绑定成不同的上下文变量。(任何Seam组件 实例 都可以被绑定到多个上下文变量,但@Role使得我们也可以在类的级别做到这一点,从而享受自动实例化的优点。)

@Name("user")
@Entity
@Scope(CONVERSATION)
@Role(name="currentUser", scope=SESSION)
public class User {
    ...
}

@Roles 注解可以让我们为组件指定任意多的附加角色。

@Name("user")
@Entity
@Scope(CONVERSATION)
@Roles({@Role(name="currentUser", scope=SESSION),
        @Role(name="tempUser", scope=EVENT)})
public class User {
    ...
}

3.2.10. 内置组件

和很多优秀的框架一样,Seam自产自用,主要实现了一系列的内置Seam拦截器(后文详述)和Seam组件。这让应用程序在运行时和内置的组件交互变得很容易,甚至可以用自己编写的实现来替换掉内置组件,由此来定制Seam的基本功能。内置组件在 org.jboss.seam.core 这个Seam 命名空间中定义,Java包名也是相同的。

像所有Seam组件一样,内置组件也可以被注射,但是它们也提供了便利的instance()静态方法:

FacesMessages.instance().add("Welcome back, #{user.name}!");

3.3. 双向注入

Dependency injection(依赖注入)inversion of control(控制反转) 现在对大多数Java 开发者来说都是熟悉的概念了。依赖注入允许一个组件通过容器“注入”另一个组件到一个setter方法或者实例变量的方式,来获得被“注入”组件的引用(reference)。我们之前看过的所有依赖注入的实现,注入发生在组件创建的时候,在此后实例的整个生命周期中不再改变。对无状态组件,这么做是有道理的。从客户端的角度来看,特定种类的无状态组件的所有实例都是可以替换的。另一方面,Seam着重处理有状态组件。此时传统的依赖注入不再是非常有效了。Seam引入了 bijection(双向注入) 这个名词,用来作为注入的广义概括。和injection(单向注入)对比,bijection是:

  • contextual(上下文相关的) - 双向注入用来针对不同的上下文来组装有状态组件(在较大范围的上下文中的组件,可以引用较小范围上下文中的组件)

  • bidirectional(双向的) - 被触发后,值从上下文变量中注射到组件属性中,也可以从组件属性outjected(反向注入) 回上下文,这样被调用的组件可以只通过改写自己的实例变量就同时操作了上下文变量的值

  • dynamic(动态的) - 因为上下文变量的值随着时间不断改变,而且因为Seam组件是有状态的,双向注入在每次组件被调用的时候都发生。

基本上,通过设置实例变量是需要注入、反向注入、还是二者皆是,双向注入让你将上下文变量映射到组件实例变量。当然,我们使用注解来设置双向注入。

@In 注解指明应该注入值,可能是注入实例变量:

@Name("loginAction")
@Stateless
public class LoginAction implements Login {
    @In User user;
    ...
}

或者注入setter方法:

@Name("loginAction")
@Stateless
public class LoginAction implements Login {
    User user;

    @In
    public void setUser(User user) {
        this.user=user;
    }

    ...
}

默认情况下,针对被注入的属性或者实例变量名, Seam会对所有的上下文进行优先级搜索。 如果你希望明确指定上下文变量名,可以这样写:@In("currentUser")

如果没有组件实例绑定到具名的上下文变量,你可能希望Seam创建一个,你可以指定 @In(create=true)。 如果值是可选的(可以为null),请指定 @In(required=false)

对于某些组件,到处指定 @In(create=true) 是很繁琐的。 你可以注解整个组件为 @AutoCreate,它就会在任何需要的时候自动创建,不需要明确的指定 create=true

你还可以注入表达式的值:

@Name("loginAction")
@Stateless
public class LoginAction implements Login {
    @In("#{user.username}") String username;
    ...
}

(在下一章,有更多的关于组件生命周期和注射的内容。)

@Out 注解指定了某个属性需要对外注入,可能是从实例变量:

@Name("loginAction")
@Stateless
public class LoginAction implements Login {
    @Out User user;
    ...
}

或者从某个getter方法:

@Name("loginAction")
@Stateless
@Interceptors(SeamInterceptor.class)
public class LoginAction implements Login {
    User user;

    @Out
    public User getUser() {
        return user;
    }

    ...
}

属性可以既是被注入的,也可以对外注入:

@Name("loginAction")
@Stateless
public class LoginAction implements Login {
    @In @Out User user;
    ...
}

或者:

@Name("loginAction")
@Stateless
public class LoginAction implements Login {
    User user;

    @In
    public void setUser(User user) {
        this.user=user;
    }

    @Out
    public User getUser() {
        return user;
    }

    ...
}

3.4. Lifecycle methods(生命周期方法)

Session Bean和实体Bean Seam组件支持所有通用的EJB3.0生命周期回调(@PostConstruct, @PreDestroy, 等等)。但是Seam也同样支持JavaBean组件使用任意的这些回调。然而,一但这些注解在J2EE环境中失效,Seam定义了两个附加组件完成生命周期回调,这等同于 @PostConstruct@PreDestroy.

@Create 方法在Seam实例化一个组件后被调用。组件只可以定义一个 @Create 方法。

@Destroy 方法在Seam组件被绑定的上下文结束时被调用。组件只可以定义一个 @Destroy 方法。

另外,有状态Session Bean组件 必须 定义一个无参并注解为 @Remove 的方法。这个方法在上下文结束时被Seam调用。

最后,相关的注解还有 @Startup,它可以用在任何Application或者Session范围的组件上。 @Startup 注解告诉Seam在上下文开始的时候立刻初始化组件,而不是在被客户访问的时候才创建。 控制startup组件的初始化顺序通过指定 @Startup(depends={....}) 进行。

3.5. 条件装载(Conditional installation)

@Install 注解让你控制组件的条件装载,允许随着不同的部署情形而改变。例如:

  • 希望在测试中mock out一些基础组件。

  • 希望在一些特殊的部署情形下改变组件的实现。

  • 希望只有满足依赖条件的时候才安装某些组件(对框架作者很有用)。

@Install 通过让你指定 precedence(优先级)dependencies(依赖) 来运作。

组件的优先级是一个数字,当在classpath中存在多个同组件名的类的时候,seam依靠它来决定安装哪个组件。 Seam会选取优先级数字最大的。有一些预定义的优先级值:(按升序排列):

  1. BUILT_IN — 优先级最低的组件,是内置在Seam中的组件。

  2. FRAMEWORK — 第三方框架定义的组件可能覆盖内置组件,但被应用程序组件所重载。

  3. APPLICATION — 默认优先级。大部分应用程序组件适合这一级别。

  4. DEPLOYMENT — 和部署相关的应用程序组件

  5. MOCK — 为在测试中使用的mock objects所准备。

假设我们有一个组件,名为 messageSender ,和一个JMS队列交互。

@Name("messageSender")
public class MessageSender {
    public void sendMessage() {
        //do something with JMS
    }
}

在我们的单元测试中,我们并没有JMS队列可用,因此我们需要stub这个方法。我们会创建一个 mock 组件,在单元测试运行时放在classpath中,但绝不会在部署应用程序时出现。

@Name("messageSender")
@Install(precedence=MOCK)
public class MockMessageSender extends MessageSender {
    public void sendMessage() {
        //do nothing!
    }
}

当seam在classpath中发现多个组件时,precedence 帮助Seam决定使用哪个版本。

如果我们能精确控制使用 classpath中存在的类,是很美妙的。 但是如果我在编写一个可重用的框架,具有很多依赖条件,我不希望用那么多的jar来肢解框架。 我希望通过已经安装了哪些了组件,以及classpath中存在哪些组件,来决定安装组件。 @Install 注解也控制这一功能。Seam内部使用这一机制来控制很多内部组件的条件安装。虽然你可能不会在你的程序中使用它。

3.6. 日志

面对下面的代码,谁都会被搞得七窍生烟:

private static final Log log = LogFactory.getLog(CreateOrderAction.class);

public Order createOrder(User user, Product product, int quantity) {
    if ( log.isDebugEnabled() ) {
        log.debug("Creating new order for user: " + user.username() +
            " product: " + product.name()
            + " quantity: " + quantity);
    }
    return new Order(user, product, quantity);
}

难以想象为何简单的log信息会被搞得如此之复杂。用于log的代码函数比用于实际业务逻辑的还要多!很惊讶,Java社区10年内都没有对此加以改变。

Seam提供了可以显著简化上述代码的logging API:

@Logger private Log log;

public Order createOrder(User user, Product product, int quantity) {
    log.debug("Creating new order for user: #0 product: #1 quantity: #2", user.username(), product.name(), quantity);
    return new Order(user, product, quantity);
}

是否把 log 变量声明为static并不重要,它都可以工作,除非是Entity Bean组件,需要把 log 声明为静态的。

注意你并不需要繁杂的 if ( log.isDebugEnabled() ) 保卫语句,因为字符串相加操作是在 debug() 方法 内部 发生的。也请注意通常不需要显式指定log类型,因为Seam知道哪个组件正在注入 Log

假设 UserProduct 是当前上下文中可用的Seam组件,写起来更加简便:

@Logger private Log log;

public Order createOrder(User user, Product product, int quantity) {
    log.debug("Creating new order for user: #{user.username} product: #{product.name} quantity: #0", quantity);
    return new Order(user, product, quantity);
}

Seam loging自动选择发送结果到log4j或者JDK logging。如果log4j在classpath中,Seam会使用它,否则,Seam会使用JDK logging.

3.7. Mutable接口和@ReadOnly

很多应用服务器的 HttpSession 集群实现都有问题,对绑定到Session的可变对象状态的改变只有在明确调用 setAttribute() 的时候才会被复制。 这是Bug的一个源头,这些Bug难以在开发阶段有效找出,因为它们只会在应用服务器失效切换的时候才会被发现。 而且,实际的复制信息包含了绑定到Session的所有序列化对象图,这是低效的。

当然,EJB 有状态Session Bean必须进行自动dirty checking,并进行可变状态的复制,并且EJB 容器也应该引入优化,例如属性级别的复制。 但不幸的是,并非所有的Seam用户都有这么好的运气,他们的环境可能并不支持EJB 3.0。 因此,对于Session和Conversation范围内的JavaBean和Entity Bean组件,在Web容器的Session集群之上,Seam提供了额外的集群安全的状态管理层。

对于Session或Conversation范围的JavaBean组件,每次组件被引用程序调用的时候,Seam自动通过调用一次 setAttribute() 来触发复制。当然,对大部分是读操作的组件来说,这效率不高。你可以通过实现 org.jboss.seam.core.Mutable 接口来控制这一行为。或者扩展 org.jboss.seam.core.AbstractMutable,在组件内实现自己的dirty-checking逻辑。例如,

@Name("account")
public class Account extends AbstractMutable
{
    private BigDecimal balance;

    public void setBalance(BigDecimal balance)
    {
        setDirty(this.balance, balance);
        this.balance = balance;
    }

    public BigDecimal getBalance()
    {
        return balance;
    }

    ...

}

或者,你可以使用 @ReadOnly 注解来达到类似的效果:

@Name("account")
public class Account
{
    private BigDecimal balance;

    public void setBalance(BigDecimal balance)
    {
        this.balance = balance;
    }

    @ReadOnly
    public BigDecimal getBalance()
    {
        return balance;
    }

    ...

}

对Session或Conversation范围的Entity Bean组件,在每次被请求的时候Seam自动通过一次 setAttribute() 调用来触发复制,除非(对话范围的)实体和一个Seam管理的持久化上下文相关联,此时无需复制。这一策略不是最高效的,因此Session或Conversation范围的Entity Bean应该小心使用。你总是可以编写有状态的Session Bean或者JavaBean组件来“管理”Entity Bean实例。例如,

@Stateful
@Name("account")
public class AccountManager extends AbstractMutable
{
    private Account account; // an entity bean

    @Unwrap
    public void getAccount()
    {
        return account;
    }

    ...

}

注意,对于 EntityHome 类,Seam应用框架提供了一个非常好的例子来说明如何使用Seam组件来管理实体Bean的实例.

3.8. Factory和Manager组件

我们经常需要与非Seam组件的对象打交道。但是我们仍然希望把它们通过 @In 注入我们的组件,并在值和方法表达式中使用它们。 有时候,我们甚至需要把它们绑定到Seam 上下文的生命周期里(例如@Destroy)。 所以Seam上下文可以容纳非Seam组件的对象,并且Seam提供了一些很好的特性,这些特性使得我们与绑定到上下文里的非组件对象打交道更加容易。

factory component pattern(工厂组件模式)让Seam组件作为非组件对象的构造器。 当上下文变量被引用,但是没有值被绑定到它时,会调用一个factory method(工厂方法)。 我们通过@Factory注解来定义工厂方法。 工厂方法把一个值绑定到上述上下文变量,并且决定被绑定的值的范围。有两种工厂方法。第一种返回一个值,Seam会把它绑定到上下文里:

@Factory(scope=CONVERSATION)
public List<Customer> getCustomerList() {
    return ... ;
} 

第二种方法返回 void,它自己把值绑定到上下文变量:

@DataModel List<Customer> customerList;

@Factory("customerList")
public void initCustomerList() {
    customerList = ...  ;
} 

两种情况下,当我们引用 customerList 上下文变量,而其值为null时,工厂方法被调用,然后对这个值生命周期的其他部分就无法操纵了。 更加强大的模式是 manager component pattern(管理者组件模式)。 在这种情况下,有一个Seam组件绑定到上下文变量,它管理着上下文变量的值,对客户端不可见。

管理者组件可以是任何组件,它需要一个 @Unwrap 方法。 该方法返回对客户端可见的值,每次 上下文变量被引用的时候都会被调用。

@Name("customerList")
@Scope(CONVERSATION)
public class CustomerListManager
{
    ...

    @Unwrap
    public List<Customer> getCustomerList() {
        return ... ;
    }
}

当你有一个对象并需要对其组件的生命周期更多的控制时,管理组件模式就显得尤其有用。 例如,如果你有一个重量级的对象,当上下文结束时你想对其进行清除操作,你可以@Unwrap对象,并在管理组件的 @Destroy 方法中执行清除操作。

@Name("hens")
@Scope(APPLICATION)
public class HenHouse {

    Set<Hen> hens;

    @In(required=false) Hen hen;

    @Unwrap
    public List<Hen> getHens() {
        if (hens == null) {
            // Setup our hens
        }
        return hens;
    }

    @Observer({"chickBorn", "chickenBoughtAtMarket"})
    public addHen() {
        hens.add(hen);
    }

    @Observer("chickenSoldAtMarket")
    public removeHen() {
        hens.remove(hen);
    }

    @Observer("foxGetsIn")
    public removeAllHens() {
        hens.clear();
    }
    ...
} 

这里,被管理的组件观察那些改变在下面的对象的事件。组件自己管理这些动作,并且由于对象在每一次访问中都被解开,所以这里提供了一个统一的视图。

Chapter 4. 配置Seam组件

Seam所崇尚的哲学是XML配置最小化。不过,基于不同的原因,我们有时候还是要利用XML来配置Seam组件。这些原因包括: 将Java代码与特定于部署的信息分离;要建立可重用的框架;配置Seam的内置功能等等。 Seam提供了两种基本的配置组件方法:通过在properties文件或者 web.xml 中设置属性来配置, 或者通过 components.xml 进行配置。

4.1. 通过属性设置来配置组件

Seam组件的配置属性可以通过两种方式得到:通过servlet context参数,或者通过位于classpath下的 seam.properties 属性文件进行。

可配置的Seam组件必须为可配置的属性暴露JavaBean风格的属性setter方法。例如,一个名为 com.jboss.myapp.settings 的Seam组件拥有一个名为 setLocale() 的setter方法,我们就可以在 seam.properties 文件中提供一个名为 com.jboss.myapp.settings.locale 的属性,或者作为一个servlet context参数, 这样,一旦该组件被实例化,Seam将自动为这个名为 locale 的属性注入相应的值。

Seam本身的配置也采用了相同的机制。例如,要设置对话超时,我们可以在 web.xml 或者 seam.properties 中为 org.jboss.seam.core.manager.conversationTimeout 提供一个值。 (在Seam内置的组件 org.jboss.seam.core.manager 中,已经包含了一个名为 setConversationTimeout() 的setter方法。)

4.2. 通过 components.xml 来配置组件

components.xml 文件的功能要比属性设置的更强大一些。它让你:

  • 配置那些已经被自动安装的组件—包括内置组件以及那些带有 @Name 注解, 且被Seam的部署扫描器识别到的那些应用组件。

  • 将那些没有 @Name 注解的类安装成为Seam组件— 这一点对于那些需要以不同的名字进行多次安装的结构组件特别有用(例如,Seam管理的持久化上下文)。

  • 安装那些 具有 @Name 注解,但是默认情况下未被安装的Seam组件。 因为 @Install 注解表明该组件不应当被安装。

  • 覆盖组件的范围。

components.xml 文件可以出现在下面三个不同地方中的任何一处:

  • war 包的 WEB-INF 目录下。

  • jar 包的 META-INF 目录下。

  • 包含带有 @Name 注解类的 jar 包下的任何目录。

通常情况下,当Seam部署扫描器在包含 seam.properties 文件或者 META-INF/components.xml 文件的文件夹中识别到一个包含 @Name 注解的类时, Seam将安装载该组件。(除非这个组件具有一个 @Install 注解,表示它不应该被默认安装。) components.xml 文件让我们去处理那些需要覆盖注解的特殊情况。

例如,下面的 components.xml 文件安装了jBPM:

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:bpm="http://jboss.com/products/seam/bpm">
    <bpm:jbpm/>
</components>

这个例子实现了相同的功能:

<components>
    <component class="org.jboss.seam.bpm.Jbpm"/>
</components>

这个例子安装并配置了Seam管理的两个不同的持久化上下文:

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:persistence="http://jboss.com/products/seam/persistence"

    <persistence:managed-persistence-context name="customerDatabase"
                       persistence-unit-jndi-name="java:/customerEntityManagerFactory"/>

    <persistence:managed-persistence-context name="accountingDatabase"
                       persistence-unit-jndi-name="java:/accountingEntityManagerFactory"/>

</components>

这个例子也一样:

<components>
    <component name="customerDatabase"
              class="org.jboss.seam.persistence.ManagedPersistenceContext">
        <property name="persistenceUnitJndiName">java:/customerEntityManagerFactory</property>
    </component>

    <component name="accountingDatabase"
              class="org.jboss.seam.persistence.ManagedPersistenceContext">
        <property name="persistenceUnitJndiName">java:/accountingEntityManagerFactory</property>
    </component>
</components>

这个例子创建了一个Seam管理的session范围持久化上下文(这在实际项目中并不推荐使用)

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:persistence="http://jboss.com/products/seam/persistence"

  <persistence:managed-persistence-context name="productDatabase"
                                          scope="session"
                     persistence-unit-jndi-name="java:/productEntityManagerFactory"/>

</components>
<components>

    <component name="productDatabase"
              scope="session"
              class="org.jboss.seam.persistence.ManagedPersistenceContext">
        <property name="persistenceUnitJndiName">java:/productEntityManagerFactory</property>
    </component>

</components>

通常会给像持久化上下文这样的基础结构对象使用 auto-create 选项, 它能在你使用 @In 注解时,不必显式地指定 create=true

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:persistence="http://jboss.com/products/seam/persistence"

  <persistence:managed-persistence-context name="productDatabase"
                                    auto-create="true"
                     persistence-unit-jndi-name="java:/productEntityManagerFactory"/>

</components>
<components>

    <component name="productDatabase"
        auto-create="true"
              class="org.jboss.seam.persistence.ManagedPersistenceContext">
        <property name="persistenceUnitJndiName">java:/productEntityManagerFactory</property>
    </component>

</components>

<factory> 声明让你指定一个值或者方法来绑定一个表达式,当它第一次被引用时,将被执行用来初始化一个context变量的值。

<components>

    <factory name="contact" method="#{contactManager.loadContact}" scope="CONVERSATION"/>

</components>

你也可以为Seam组件创建一个别名(第二个名字),就像这样:

<components>

    <factory name="user" value="#{actor}" scope="STATELESS"/>

</components>

你甚至可以给常用的表达式定义别名:

<components>

    <factory name="contact" value="#{contactManager.contact}" scope="STATELESS"/>

</components>

auto-create="true" 用在 <factory> 声明中尤其常见。

<components>

    <factory name="session" value="#{entityManager.delegate}" scope="STATELESS" auto-create="true"/>

</components>

我们在部署或者测试期间,有时候想要通过略微的改动,来重用同一个 components.xml文件。 Seam允许你在 components.xml 文件中使用 @wildcard@ 形式的通配符, 这些通配符可以在部署的时候被Ant构建脚本替换,也可以在开发时通过在classpath中提供一个名为 components.properties 的文件进行替换。 你会在Seam的示例程序中找到这个用法。

4.3. 细粒度的配置文件

如果你有大量的组件需要在XML中进行配置,那么就很有必要将 components.xml 文件中的内容分散到多个文件中去。 Seam允许你把类(例如名为 com.helloworld.Hello )的配置放到一个资源中(名为 com/helloworld/Hello.component.xml)。 (你对这种模式可能很熟悉,因为它与我们在Hibernate中使用的相同)。 文件的根元素应该是 <components> 或者 <component>

第一个选项允许你在一个文件中定义多个组件:

<components>
    <component class="com.helloworld.Hello" name="hello">
        <property name="name">#{user.name}</property>
    </component>
    <factory name="message" value="#{hello.message}"/>
</components>

第二个选项只允许你定义或者配置单个组件,不过麻烦会少一点:

<component name="hello">
    <property name="name">#{user.name}</property>
</component>

在第二个选项中,类名与组件定义所在的文件是一致的。

你还可以选择将所有类的配置都放在 com/helloworld/components.xmlcom.helloworld 包中。

4.4. 可配置的属性类型

String的属性、基本类型以及基本类型的包装类型可以像我们期望的那样进行配置:

org.jboss.seam.core.manager.conversationTimeout 60000
<core:manager conversation-timeout="60000"/>
<component name="org.jboss.seam.core.manager">
    <property name="conversationTimeout">60000</property>
</component>

也支持由String或者基本类型构成的数组、Set和List:

org.jboss.seam.bpm.jbpm.processDefinitions order.jpdl.xml, return.jpdl.xml, inventory.jpdl.xml
<bpm:jbpm>
    <bpm:process-definitions>
        <value>order.jpdl.xml</value>
        <value>return.jpdl.xml</value>
        <value>inventory.jpdl.xml</value>
    </bpm:process-definitions>
</bpm:jbpm>
<component name="org.jboss.seam.bpm.jbpm">
    <property name="processDefinitions">
        <value>order.jpdl.xml</value>
        <value>return.jpdl.xml</value>
        <value>inventory.jpdl.xml</value>
    </property>
</component>

甚至也支持那些包含String值为键、String或者基本类型值的Map:

<component name="issueEditor">
    <property name="issueStatuses">
        <key>open</key> <value>open issue</value>
        <key>resolved</key> <value>issue resolved by developer</value>
        <key>closed</key> <value>resolution accepted by user</value>
    </property>
</component>

最后,你可以利用值绑定表达式来将所有的组件装配起来。 注意这与使用 @In 注解进行注入非常不同,因为它是在组件初始化而不是被调用时起作用的。 因而它与传统的IoC容器例如JSF或者Spring所提供的依赖注入功能非常非常类似。

<drools:managed-working-memory name="policyPricingWorkingMemory" rule-base="#{policyPricingRules}"/>
<component name="policyPricingWorkingMemory"
          class="org.jboss.seam.drools.ManagedWorkingMemory">
    <property name="ruleBase">#{policyPricingRules}</property>
</component>

4.5. 使用XML命名空间

纵观整个示例,有两种完全不同的声明组件的方式:使用或者不使用XML命名空间。 下面的示例展示了一个典型的 components.xml 文件,它没有使用命名空间,而是使用Seam Components DTD:

<?xml version="1.0" encoding="UTF-8"?>
<components xmlns="http://jboss.com/products/seam/components"
            xsi:schemaLocation="http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.0.xsd">

    <component class="org.jboss.seam.core.init">
        <property name="debug">true</property>
        <property name="jndiPattern">@jndiPattern@</property>
    </component>

</components>

正如你所见,这样的配置有点繁琐。更糟的是,这些组件和属性的名称在开发时是无法被校验的。

使用命名空间的配置看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation=
                "http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.0.xsd
                 http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.0.xsd">

    <core:init debug="true" jndi-pattern="@jndiPattern@"/>

</components>

虽然Schema的声明很繁琐,不过实际的XML内容是清晰而简单易懂的。 Schema提供了关于每个可用组件和属性的详细信息,这使得XML编辑器可以发挥其自动完成的功效。 所以,使用命名空间的元素使生成和维护正确的 components.xml 文件都变得更加简单。

现在,这种方式对于Seam内建的组件工作得很好,但是对于用户自定义的组件又如何呢? 这里有两种选择:第一种,Seam支持两种模型的混合使用,允许使用普通的 <component> 声明来配置用户自定义的组件,同时也使用命名空间来配置内置组件。 不过更好的方法是,Seam允许你快速地为你自己的组件声明命名空间。

任何Java包都可以通过用 @Namespace 注解该包,而与XML命名空间而关联起来。 (包级别的注解是在一个名为 package-info.java 的文件中声明的,该文件处于包的同级目录下)。 下面是一个来自seampay演示的例子:

@Namespace(value="http://jboss.com/products/seam/examples/seampay")
package org.jboss.seam.example.seampay;

import org.jboss.seam.annotations.Namespace;

这样,你就可以在 components.xml 中使用命名空间的方式了!现在,你可以这么写:

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:pay="http://jboss.com/products/seam/examples/seampay"
            ... >

    <pay:payment-home new-instance="#{newPayment}"
                      created-message="Created a new payment to #{newPayment.payee}" />

    <pay:payment name="newPayment"
                 payee="Somebody"
                 account="#{selectedAccount}"
                 payment-date="#{currentDatetime}"
                 created-date="#{currentDatetime}" />
     ...
</components>

或者:

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:pay="http://jboss.com/products/seam/examples/seampay"
            ... >

    <pay:payment-home>
        <pay:new-instance>"#{newPayment}"</pay:new-instance>
        <pay:created-message>Created a new payment to #{newPayment.payee}</pay:created-message>
    </pay:payment-home>

    <pay:payment name="newPayment">
        <pay:payee>Somebody"</pay:payee>
        <pay:account>#{selectedAccount}</pay:account>
        <pay:payment-date>#{currentDatetime}</pay:payment-date>
        <pay:created-date>#{currentDatetime}</pay:created-date>
     </pay:payment>
     ...
</components>

这些示例展示了命名空间元素的两种使用模式。 在第一个声明中,<pay:payment-home> 指向 paymentHome 组件。

package org.jboss.seam.example.seampay;
...
@Name("paymentHome")
public class PaymentController
    extends EntityHome<Payment>
{
    ...
}

元素的名称是连字符号(-)形式的组件名称。元素的属性是连字符号(-)形式的属性名称。

在第二个声明中,<pay:payment> 元素指向 org.jboss.seam.example.seampay 包中的 Payment 类。 在这个例子中,Payment 是一个被定义成Seam组件的实体。

package org.jboss.seam.example.seampay;
...
@Entity
public class Payment
    implements Serializable
{
    ...
}

如果我们需要用户自定义组件的验证和自动完成功能,我们就需要一个Schema。 目前Seam还无法提供为一组组件自动生成Schema的机制,所以你必需手工生成。标准Seam包的Schema定义可以当作示范。

以下是Seam所使用的命名空间:

  • components — http://jboss.com/products/seam/components

  • core — http://jboss.com/products/seam/core

  • drools — http://jboss.com/products/seam/drools

  • framework — http://jboss.com/products/seam/framework

  • jms — http://jboss.com/products/seam/jms

  • remoting — http://jboss.com/products/seam/remoting

  • theme — http://jboss.com/products/seam/theme

  • security — http://jboss.com/products/seam/security

  • mail — http://jboss.com/products/seam/mail

  • web — http://jboss.com/products/seam/web

  • pdf — http://jboss.com/products/seam/pdf

  • spring — http://jboss.com/products/seam/spring

Chapter 5. 事件、拦截器和异常处理

两个更深入的概念补充了上下文组件模型,这两个概念推动了极端松耦合这一Seam应用程序的独特特性。 第一个是强有力的事件模型,事件可以通过类似JSF绑定表达式的方法映射到事件监听器。 第二个是普遍使用注解和拦截器,这使我们总能跨越式地切入到到实现业务逻辑的组件。

5.1. Seam事件

Seam组件模型是为使用 事件驱动的应用程序 而开发的,特别是在一个细粒度的事件模型里进行细粒度的松耦合组件的开发。 Seam的事件有几种类型,大部分是我们已经见过的:

  • JSF事件

  • jBPM的状态转移事件

  • Seam页面动作

  • Seam组件驱动事件

  • Seam上下文事件

所有这些不同种类的事件都通过绑定了表达式的JSF EL方法映射到Seam组件去。JSF事件是在JSF模板中定义的:

<h:commandButton value="Click me!" action="#{helloWorld.sayHello}"/>

对于jBPM的转换事件,是在jBPM过程定义或页面流定义中指定的:

<start-page name="hello" view-id="/hello.jsp">
    <transition to="hello">
        <action expression="#{helloWorld.sayHello}"/>
    </transition>
</start-page>

你可以在其他地方找到更多关于JSF事件和jBPM事件的信息。我们现在主要关注由Seam定义的两种新增类型的事件上。

5.1.1. 页面动作

Seam的页面动作是指就在我们渲染页面之前发生的事件。我们在 WEB-INF/pages.xml 中声明页面动作。 我们可以为任何一个特殊的JSF视图id定义一个页面动作:

<pages>
    <page view-id="/hello.jsp" action="#{helloWorld.sayHello}"/>
</pages>

或者,我们可以使用一个通配符 * 作为 view-id 的后缀来指定一个动作,应用到所有符合该模式的视图ID中:

<pages>
    <page view-id="/hello/*" action="#{helloWorld.sayHello}"/>
</pages>

如果多通配符的页面动作匹配当前的view-id,Seam将按照从最通用到最特殊的顺序来调用所有的动作。

页面动作方法可以返回一个JSF的结果。如果这个结果非空,Seam将用定义好的导航规则导航到一个视图中去。

此外,在元素 <page> 里提到的视图id不需要对应一个真实的JSP或Facelets页面! 因此,我们可以再生传统的面向动作的框架的功能,就像Struts或WebWork使用页面动作那样。例如:

TODO: translate struts action into page action

如果你想要应non-faces的请求做点复杂的事情(例如HTTP GET请求),这就非常有用。

对于多页面或者条件页面的动作,可以使用 <action> 标签指定:

<pages>
    <page view-id="/hello.jsp">
        <action execute="#{helloWorld.sayHello}" if="#{not validation.failed}"/>
        <action execute="#{hitCount.increment}"/>
    </page>
</pages>

5.1.1.1. 页面参数

一个JSF faces请求(表单提交)同时封装了一个“动作action”(一个方法绑定)和“多个参数parameters”(输入值绑定)。一个页面动作也可能需要参数!

由于GET请求是可以做标记的,页面参数是作为人类易读的请求参数来传递的。(不像JSF form的输入,什么都有就是不具有可读性!)

你可以使用页面参数,带不带动作方法都可以。

5.1.1.1.1. 将请求参数映射到模型

Seam让我们提供一个值绑定,来将一个已命名的请求参数映射成一个模型对象的属性。

<pages>
      <page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
          <param name="firstName" value="#{person.firstName}"/>
          <param name="lastName" value="#{person.lastName}"/>
      </page>
  </pages>

<param> 的声明是双向的,就像一个JSF输入的值绑定:

  • 当视图id的一个non-faces(GET)请求发生时,Seam在执行了相应的类型转变之后,就在模型对象上设置已命名的请求参数的值。

  • 任何 <s:link><s:button> 透明地或者说自动地包括request带有的参数。 参数的值由渲染阶段(当 <s:link> 被渲染)的绑定值来决定。

  • 使用 <redirect/> 到视图id的任何导航规则很明显是含有请求参数。 参数的值由调用应用程序阶段结束时的值绑定大小来决定。

  • 这个值很明显是由带有视图id的被提交的任何JSF页面传播的。 这意味着视图参数表现得就像faces请求的 PAGE 范围内上下文变量一样。

最理想的情形是 无论 我们从什么页面到 /hello.jsp (或者从/hello.jsp回到/hello.jsp), 在值绑定中被引用的模型属性的值都应该被“记住”,而不需要对话来存储(或者其他的服务器端状态来存储)。

5.1.1.1.2. 传播请求参数

如果只是指定 name 属性,那么请求参数就会利用 PAGE 进行上下文传播(它没有被映射成模型属性)。

<pages>
      <page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
          <param name="firstName" />
          <param name="lastName" />
      </page>
  </pages>

如果你想要建立多层的复杂CRUD页面,页面参数的传递尤其有用。你可以用它“记住”你前面到过的页面(例如当按了保存按钮时)和正在编辑的实体。

  • 很明显,如果参数是视图的页面参数的话,任何 <s:link> 或者 <s:button> 都会传播请求参数。

  • 这个值很明显是由带有指定视图id的页面的任何jsf页面表单提交传播的。 (这意味着视图参数表现得就像faces请求的PAGE范围内视图参数一样。)

所有这些听起来很复杂,你可能会想这么一个外来的构造是否真的值得去努力。实际上,一旦你“掌握了它”,有这种想法非常自然。 理解这些资料显然需要花费时间的。页面参数是跨越non-faces请求来传播状态的最优雅方式。 对于用可标记的结果页,搜索屏幕的问题尤其有效,在这种情况下,我们喜欢可以写应用程序代码、用同一段代码来处理POST和GET请求。 页面参数消除了视图定义中请求参数的重复清单,并使得重定向更容易用代码实现。

5.1.1.1.3. 转换和验证

你可以为复杂的模型属性指定一个JSF转换器:

<pages>
    <page view-id="/calculator.jsp" action="#{calculator.calculate}">
        <param name="x" value="#{calculator.lhs}"/>
        <param name="y" value="#{calculator.rhs}"/>
        <param name="op" converterId="com.my.calculator.OperatorConverter" value="#{calculator.op}"/>
    </page>
</pages>

或者:

<pages>
    <page view-id="/calculator.jsp" action="#{calculator.calculate}">
        <param name="x" value="#{calculator.lhs}"/>
        <param name="y" value="#{calculator.rhs}"/>
        <param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
    </page>
</pages>

JSF验证器和 required="true" 也可以这样用:

<pages>
    <page view-id="/blog.xhtml">
        <param name="date"
               value="#{blog.date}"
               validatorId="com.my.blog.PastDate"
               required="true"/>
    </page>
</pages>

或者:

<pages>
    <page view-id="/blog.xhtml">
        <param name="date"
               value="#{blog.date}"
               validator="#{pastDateValidator}"
               required="true"/>
    </page>
</pages>

更好的方式,基于模型的Hibernate验证器注解会自动被识别和验证。

当类型转换或者验证失败后,一个全局的 FacesMessage 就会被添加到 FacesContext 中。

5.1.1.2. 导航

你可以使用在Seam应用程序的 faces-config.xml 中定义的标准JSF导航规则。然而,JSF导航规则也有许多烦人的限制:

  • 在重定向时,不可能指定一个将要使用的请求参数。

  • 不可能由一个规则来开始或者结束对话。

  • 通过给动作方法求取返回值来运作规则;不可能去给一个任意的EL表达式取值。

更深层次的问题在于”管理“逻辑在 pages.xmlfaces-config.xml 之间是分散的。 最好是把这种逻辑统一进 pages.xml 中。

这个JSF导航规则:

<navigation-rule>
    <from-view-id>/editDocument.xhtml</from-view-id>

    <navigation-case>
        <from-action>#{documentEditor.update}</from-action>
        <from-outcome>success</from-outcome>
        <to-view-id>/viewDocument.xhtml</to-view-id>
        <redirect/>
    </navigation-case>

</navigation-rule>

可以重写如下:

<page view-id="/editDocument.xhtml">

    <navigation from-action="#{documentEditor.update}">
        <rule if-outcome="success">
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>

</page>

如果我们不必用字符类型的返回值(JSF的结果)来污染 DocumentEditor 组件的话会更好。 因此Seam允许我们写成:

<page view-id="/editDocument.xhtml">

    <navigation from-action="#{documentEditor.update}"
                   evaluate="#{documentEditor.errors.size}">
        <rule if-outcome="0">
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>

</page>

或者甚至可以写成:

<page view-id="/editDocument.xhtml">

    <navigation from-action="#{documentEditor.update}">
        <rule if="#{documentEditor.errors.empty}">
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>

</page>

第一种形式计算一个值绑定,来确定要被后面的一系列导航规则所使用的结果值。 第二种方法忽略结果,并为每个可能的规则来计算值绑定。

当然,当一个更新成功,我们可能想要结束当前的对话。我们可以这样做:

<page view-id="/editDocument.xhtml">

    <navigation from-action="#{documentEditor.update}">
        <rule if="#{documentEditor.errors.empty}">
            <end-conversation/>
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>

</page>

由于我们终止了会话,后面的任何请求都无法知道我们对哪个文档感兴趣。 我们可以将文档id作为一个请求参数传递,这样也使得视图变成是可标记的:

<page view-id="/editDocument.xhtml">

    <navigation from-action="#{documentEditor.update}">
        <rule if="#{documentEditor.errors.empty}">
            <end-conversation/>
            <redirect view-id="/viewDocument.xhtml">
                <param name="documentId" value="#{documentEditor.documentId}"/>
            </redirect>
        </rule>
    </navigation>

</page>

在JSF中,null是一个特殊的结果。结果null被解释成“重新显示页面”。 下面的导航规则符合任何非null的结果,而 不符合 null的结果:

<page view-id="/editDocument.xhtml">

    <navigation from-action="#{documentEditor.update}">
        <rule>
            <render view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>

</page>

如果结果出现null,你还想执行导航,就使用下面的形式:

<page view-id="/editDocument.xhtml">

    <navigation from-action="#{documentEditor.update}">
        <render view-id="/viewDocument.xhtml"/>
    </navigation>

</page>

view-id可以作为一个JSF EL表达式提供:

<page view-id="/editDocument.xhtml">

    <navigation if-outcome="success">
        <redirect view-id="/#{userAgent}/displayDocument.xhtml"/>
    </navigation>

</page>

5.1.1.3. 导航的定义、页面动作和参数的细粒度文件

如果你有很多不同的页面动作和页面参数,或者甚至是很多导航规则,你就会很想把这些声明分开放到多个文件中去。 你可以在一个名为 calc/calculator.page.xml 的资源中,为一个有着视图id /calc/calculator.jsp 的页面定义动作和参数。 这个例子中的根元素是 <page> 元素,隐含着视图id:

<page action="#{calculator.calculate}">
    <param name="x" value="#{calculator.lhs}"/>
    <param name="y" value="#{calculator.rhs}"/>
    <param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
</page>

5.1.2. 组件驱动的事件

Seam组件可以通过方法间简单的调用相互影响。状态组件甚至实现 Observer/Observable 模式。 但在组件直接调用彼此方法的时候,为了使组件在一个比可能存在的更加松耦合的方式下相互作用,Seam提供了 组件驱动事件

我们在 components.xml 里指定了事件监听器(观察者)。

<components>
    <event type="hello">
        <action execute="#{helloListener.sayHelloBack}"/>
        <action execute="#{logger.logHello}"/>
    </event>
</components>

在这里,event type 是任意的字符串。

事件发生时,该事件已经注册过的动作将按照它们在 components.xml 中出现的顺序被依次调用。 组件如何发起事件?Seam为此提供了一个内置的组件。

@Name("helloWorld")
public class HelloWorld {
    public void sayHello() {
        FacesMessages.instance().add("Hello World!");
        Events.instance().raiseEvent("hello");
    }
}

或者你可以使用注解。

@Name("helloWorld")
public class HelloWorld {
    @RaiseEvent("hello")
    public void sayHello() {
        FacesMessages.instance().add("Hello World!");
    }
}

注意这个事件产生器没有依赖任何事件消费者。事件监听器现在可以完全不依赖于产生器而实现:

@Name("helloListener")
public class HelloListener {
    public void sayHelloBack() {
        FacesMessages.instance().add("Hello to you too!");
    }
}

上述在 components.xml中定义的方法绑定关心把事件映射到消费者去。 如果你不喜欢 components.xml 文件中的那一套,可以用注解来替代:

@Name("helloListener")
public class HelloListener {
    @Observer("hello")
    public void sayHelloBack() {
        FacesMessages.instance().add("Hello to you too!");
    }
}

你可能想知道为什么在这个讨论中没有提到关于任何事件对象的东西。 在Seam中,对事件对象而言,不需要在事件生产者和监听器之间传播状态。 状态保留在Seam上下文中,在组件之间共享。然而,如果你真想传递事件对象,你可以:

@Name("helloWorld")
public class HelloWorld {
    private String name;
    public void sayHello() {
        FacesMessages.instance().add("Hello World, my name is #0.", name);
        Events.instance().raiseEvent("hello", name);
    }
}
@Name("helloListener")
public class HelloListener {
    @Observer("hello")
    public void sayHelloBack(String name) {
        FacesMessages.instance().add("Hello #0!", name);
    }
}

5.1.3. 上下文事件

Seam定义了许多内置事件,应用程序可以用它们来进行特殊类型的框架集成。这些事件是:

  • org.jboss.seam.validationFailed — JSF验证失败时被调用

  • org.jboss.seam.noConversation — 没有长时间运行的对话在运行或者长时间运行的对话被请求时调用

  • org.jboss.seam.preSetVariable.<name> — 设置上下文变量 <name> 时调用

  • org.jboss.seam.postSetVariable.<name> — 设置上下文变量 <name> 时调用

  • org.jboss.seam.preRemoveVariable.<name> — 未设置上下文变量 <name> 时调用

  • org.jboss.seam.postRemoveVariable.<name> — 未设置上下文变量 <name> 时调用

  • org.jboss.seam.preDestroyContext.<SCOPE> — 在 <SCOPE> 上下文被销毁之前调用

  • org.jboss.seam.postDestroyContext.<SCOPE> — 在 <SCOPE> 上下文被销毁之后调用

  • org.jboss.seam.beginConversation — 当一个长时间运行的对话开始的时候调用

  • org.jboss.seam.endConversation — 当一个长时间运行的对话结束的时候调用

  • org.jboss.seam.beginPageflow.<name> — 在页面流 <name> 开始时调用

  • org.jboss.seam.endPageflow.<name> — 在页面流 <name> 结束时调用

  • org.jboss.seam.createProcess.<name> — 在创建进程 <name> 时调用

  • org.jboss.seam.endProcess.<name> — 在进程 <name> 结束时调用

  • org.jboss.seam.initProcess.<name> — 在进程 <name> 与对话相关联时调用

  • org.jboss.seam.initTask.<name> — 在任务 <name> 与对话相关联时调用

  • org.jboss.seam.startTask.<name> — 在任务 <name> 开始时调用

  • org.jboss.seam.endTask.<name> — 在结束任务 <name> 时调用

  • org.jboss.seam.postCreate.<name> — 在创建组件 <name> 时调用

  • org.jboss.seam.preDestroy.<name> — 在销毁组件 <name> 时调用

  • org.jboss.seam.beforePhase — 在开始一个JSF阶段之前调用

  • org.jboss.seam.afterPhase — 在一个JSF阶段结束之后调用

  • org.jboss.seam.postInitialization — 当Seam被初始化并启动所有组件时被调用

  • org.jboss.seam.postAuthenticate.<name> — 用户认证之后调用

  • org.jboss.seam.preAuthenticate.<name> — 在尝试认证用户之前调用

  • org.jboss.seam.notLoggedIn — 在不需要认证用户和需要认证的时候调用

  • org.jboss.seam.rememberMe — 当Seam安全在cookie中发现用户名时发生

  • org.jboss.seam.exceptionHandled.<type> — 在Seam处理未被捕捉的异常时被调用

  • org.jboss.seam.exceptionHandled — 在Seam处理未被捕捉的异常时被调用

  • org.jboss.seam.exceptionNotHandled — 在没有未被捕捉异常的处理器时被调用

  • org.jboss.seam.afterTransactionSuccess — 当事务在Seam Application Framework中成功时调用

  • org.jboss.seam.afterTransactionSuccess.<name> — 当管理具名 <name> 实体的事务在Seam Application Framework中成功时调用

  Seam组件可以用它们观察任何其他组件驱动事件的同样方式来观察这些事件中的任何一种。

5.2. Seam 拦截器

EJB 3.0为会话Bean组件引入了一个标准的拦截器模型。 要往Bean里添加拦截器,你需要写一个类,该类有一个被注解过的方法 @AroundInvoke,并用 @Interceptors 来注解这个Bean以指定拦截器类的名称。 例如,下面的拦截器检查用户是否在允许调用动作监听器方法之前登录:

public class LoggedInInterceptor {

   @AroundInvoke
   public Object checkLoggedIn(InvocationContext invocation) throws Exception {

      boolean isLoggedIn = Contexts.getSessionContext().get("loggedIn")!=null;
      if (isLoggedIn) {
         //the user is already logged in
         return invocation.proceed();
      }
      else {
         //the user is not logged in, fwd to login page
         return "login";
      }
   }

}

要把这个拦截器应用到一个作为动作监听器的会话Bean上,我们必须注解这个会话Bean @Interceptors(LoggedInInterceptor.class)。 这个注解有点丑陋。在EJB 3.0中,Seam通过允许将 @Interceptors 作为元注解使用,而依赖于拦截器框架。 在我们的例子中,将创建一个 @LoggedIn 注解,如下所示:

@Target(TYPE)
@Retention(RUNTIME)
@Interceptors(LoggedInInterceptor.class)
public @interface LoggedIn {}

现在,我们可以简单地用 @LoggedIn 来注解我们的动作监听器Bean以应用拦截器。

@Stateless
@Name("changePasswordAction")
@LoggedIn
@Interceptors(SeamInterceptor.class)
public class ChangePasswordAction implements ChangePassword {

    ...

    public String changePassword() { ... }

}

如果拦截器的顺序很重要(通常是这样),你可以将 @Interceptor 注解添加到你的拦截器类,来指定拦截器的部分顺序

@Interceptor(around={BijectionInterceptor.class,
                     ValidationInterceptor.class,
                     ConversationInterceptor.class},
             within=RemoveInterceptor.class)
public class LoggedInInterceptor
{
    ...
}

你甚至可以有一个“客户端”的拦截器,运行关于任何EJB3的内置功能:

@Interceptor(type=CLIENT)
public class LoggedInInterceptor
{
    ...
}

EJB拦截器是有状态的,有着和它们所拦截组件相同的生命周期。 对哪些不需要维护状态的拦截器而言,Seam通过指定 @Interceptor(stateless=true) 让你获得性能优化。

Seam的很多功能是作为一套内置的Seam拦截器来实现的,包括前面例子里提到的拦截器。 你没有必要通过注解你的组件来明确指定这些拦截器;它们为所有的可注解Seam组件而存在。

你甚至可以在JavaBean组件中使用Seam拦截器,不仅仅只有EJB3 Bean能用它们!

EJB定义拦截器,不仅为了业务方法(用@AroundInvoke),也为了生命周期方法 @PostConstruct@PreDestroy@PrePassivate@PostActive。 Seam支持组件和拦截器中所有这些生命周期方法,不仅仅支持EJB3 Bean,也支持JavaBean组件(除了@PreDestroy 对JavaBean组件而言没有意义之外)。

5.3. 管理异常

JSF在异常处理方面的能力有限得令人吃惊。 作为解决这个问题的部分权宜之计,Seam让你定义如何通过注解这个异常类来处理异常的特殊类,或者在XML文件中声明这个异常类。 这个工具是想要和EJB3.0标准的 @ApplicationException 的注解组合在一起,这个注解指定了这个异常是否应该导致一个事务回滚。

5.3.1. 异常和事务

EJB指定了定义良好的规则,用以控制异常是否立即标记当前的事务,以便在这个Bean的业务方法抛出一个异常时回滚: 系统异常 总是导致一个事务回滚,应用程序异常 默认是不导致事务回滚的,但是如果指定了 @ApplicationException(rollback=true),则会导致事物回滚。 (应用程序异常是任何checked异常,或者任何用 @ApplicationException 注解过的unchecked的异常。系统异常是任何没有用 @ApplicationException 注解过的unchecked异常)。

注意:在标记事务回滚和实际的回滚两者之间有一点不同。 异常规则说,只有被标记过的事务应该回滚,但是在异常抛出之后,事务仍然可以是有效的。

Seam对Seam JavaBean组件也应用EJB 3.0 异常回滚规则。

但是,这些规则仅仅应用于Seam组件层。没有捕捉到的异常传播到Seam组件层之外,或是传播到JSF层之外怎么办? 恩,让一个悬空摇摆的事务处于打开状态是不对的,当异常发生,而你又没有在Seam组件层捕捉到它时,Seam会回滚任何活动的事务。

5.3.2. 激活Seam异常处理

要激活Seam的异常处理,需要确保已经在 web.xml 中声明了主要的Servlet过滤器:

<filter>
    <filter-name>Seam Filter</filter-name>
    <filter-class>org.jboss.seam.servlet.SeamFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>Seam Filter</filter-name>
    <url-pattern>*.seam</url-pattern>
</filter-mapping>

如果你想激活异常处理器,还需要禁用 web.xml 中Facelets的开发模式,和 components.xml 中的调试模式。

5.3.3. 使用注解处理异常

每当异常传播到Seam组件层之外时,下列异常都会导致一个HTTP 404错误。 抛出异常时,它并不立即回滚当前事务,但是如果这个异常没有被其他的Seam组件捕捉到,当前事务将被回滚。

@HttpError(errorCode=404)
public class ApplicationException extends Exception { ... }

每当异常传播到Seam组件层之外时,这个异常会导致浏览器的重定向。它也同时结束当前的对话,导致当前事务立即回滚。

@Redirect(viewId="/failure.xhtml", end=true)
@ApplicationException(rollback=true)
public class UnrecoverableApplicationException extends RuntimeException { ... }

注意:对于那些在JSF生命周期的渲染阶段发生的异常而言,@Redirect 无效。

你也可以用EL指定 viewId 来重定向。

当异常传播到Seam组件层之外时,这个异常导致一个重定向,并给用户一条消息。它也立即回滚当前事务。

@Redirect(viewId="/error.xhtml", message="Unexpected error")
public class SystemException extends RuntimeException { ... }

5.3.4. 用XML处理异常

考虑到不能对我们感兴趣的所有异常类添加注解,Seam也允许我们在 pages.xml 中指定这个功能。

<pages>

   <exception class="javax.persistence.EntityNotFoundException">
      <http-error error-code="404"/>
   </exception>

   <exception class="javax.persistence.PersistenceException">
      <end-conversation/>
      <redirect view-id="/error.xhtml">
          <message>数据库访问失败 Database access failed</message>
      </redirect>
   </exception>

   <exception>
      <end-conversation/>
      <redirect view-id="/error.xhtml">
          <message>意外的失败 Unexpected failure</message>
      </redirect>
   </exception>

</pages>

最后一个 <exception> 声明没有指定类,它捕捉所有的那些没有通过注解或在 pages.xml 中特别指定的任何异常。

你也可以通过EL指定 view-id 来重定向。

你也可以通过EL访问处理后的异常实例,Seam把它放在对话上下文中,比如访问异常的消息。

...
throw new AuthorizationException("You are not allowed to do this!");

<pages>

    <exception class="org.jboss.seam.security.AuthorizationException">
        <end-conversation/>
        <redirect view-id="/error.xhtml">
            <message severity="WARN">#{org.jboss.seam.handledException.message}</message>
        </redirect>
    </exception>

</pages>

org.jboss.seam.handledException 保存着实际上由异常处理器处理的嵌套异常。 最外层的(包装器)异常也可以访问,如 org.jboss.seam.exception

5.3.5. 一些常见的异常

如果你正在使用JPA:

<exception class="javax.persistence.EntityNotFoundException">
   <redirect view-id="/error.xhtml">
      <message>Not found</message>
   </redirect>
</exception>

<exception class="javax.persistence.OptimisticLockException">
   <end-conversation/>
   <redirect view-id="/error.xhtml">
      <message>另一个用户修改了相同的数据,请重试 Another user changed the same data, please try again</message>
   </redirect>
</exception>

如果你正在使用Seam应用框架:

<exception class="org.jboss.seam.framework.EntityNotFoundException">
   <redirect view-id="/error.xhtml">
      <message>Not found</message>
   </redirect>
</exception>

如果你正在使用Seam安全:

<exception class="org.jboss.seam.security.AuthorizationException">
   <redirect>
      <message>You don't have permission to do this</message>
   </redirect>
</exception>

<exception class="org.jboss.seam.security.NotLoggedInException">
   <redirect view-id="/login.xhtml">
      <message>Please log in first</message>
   </redirect>
</exception>

那么,对于JSF:

<exception class="javax.faces.application.ViewExpiredException">
   <redirect view-id="/error.xhtml">
      <message>您的会话已经超时,请重试 Your session has timed out, please try again</message>
   </redirect>
</exception>

如果用户会话过期并且返回到原来的页面,就会抛出 ViewExpiredException 异常。 如果你在一个对话里面,no-conversation-view-idconversation-required 可以让你更细粒度地控制会话超期。

Chapter 6. 对话以及工作区管理

现在该更详细地了解一下Seam的对话模型了。

从历史上看,Seam的“对话Conversation”概念是由三个不同的概念合并而成的。

  • 工作区(workspace) 的概念,是我2002年在给英国政府做项目中遇到的概念,当时我被迫在struts之上实现工作间,我祈求永远不要再重复这样的遭遇。

  • 语义乐观的 应用程序事务(application transaction with optimistic semantics) 的概念,以及基于无状态构架的现有框架的实现,都无法提供对被扩展持久化上下文的有效管理。 (Hibernate团队确实已经受够了由于 LazyInitializationException 异常的指责,但这实际上并不是Hibernate自身的错误,而是因为像Spring框架这样的无状态构架,或者J2EE中传统的 无状态会话Facade (反)模式支持极端限制的持久化上下文模型所造成的。)

  • 工作流 任务的概念。

通过统一以上这些概念并提供底层框架的支持,我们就有了一个强大的构造能力,它使我们能够用比以前更少的代码构建出功能更加丰富且更加高效的应用程序。

6.1. Seam的对话模型

我们目前为止所看到的例子仅仅使用非常简单的对话模型,它遵循以下这些规则:

  • 在应用JSF请求值、处理验证、更新模型值、调用应用程序,以及渲染JSF请求生命周期的响应阶段期间,始终都有一个激活的会话上下文。

  • 在JSF请求生命周期恢复视图阶段的最后,Seam将会试图恢复之前长时间运行的任何对话上下文。 如果这种上下文不存在,Seam将会创建一个新的临时对话上下文。

  • 当遇到 @Begin 方法时,临时对话上下文会被提升为“长时间运行”的对话。

  • 当遇到 @End 方法时,任何“长时间运行”对话上下文都将会被降级为临时对话。

  • 在JSF请求生命周期渲染阶段的最后,Seam会保存“长时间运行”对话的内容,或者销毁临时对话上下文的内容。

  • 任何“faces request”(一种JSF postback)都会传播对话上下文。 在默认情况下,非“faces request”(例如GET请求)都不会传播对话上下文,欲知详情,请看下面分解。

  • 如果JSF请求生命周期被一个重定向redirect命令中止,Seam将会透明地保存并恢复当前的对话上下文— 除非该对话已经通过 @End(beforeRedirect=true) 中止。

Seam透明地在JSF postback以及重定向redirect时传递对话上下文。 如果你不需要做任何特殊的事情,使用 “non-faces request” (例如GET请求)就不会传递对话上下文,并且它会在一个新的临时对话中被处理。这通常(但并非总是)是我们期望的一种行为。

如果你希望在“non-faces request”中传递Seam对话,就需要显式地将Seam的 conversation id 编写为一个request参数:

<a href="main.jsf?conversationId=#{conversation.id}">Continue</a>

或者更JSF的做法是:

<h:outputLink value="main.jsf">
    <f:param name="conversationId" value="#{conversation.id}"/>
    <h:outputText value="Continue"/>
</h:outputLink>

如果你使用Seam标签库,就等同于:

<h:outputLink value="main.jsf">
    <s:conversationId/>
    <h:outputText value="Continue"/>
</h:outputLink>

如果你不想给一个postback传播会话上下文,可以使用一个类似的小窍门:

<h:commandLink action="main" value="Exit">
    <f:param name="conversationPropagation" value="none"/>
</h:commandLink>

如果你使用Seam标签库,则等同于:

<h:commandLink action="main" value="Exit">
    <s:conversationPropagation type="none"/>
</h:commandLink>

注意不使用对话上下文传播与结束对话绝对不是同一回事。

请求参数 conversationPropagation,或者 <s:conversationPropagation> 标签甚至都可以用来开始和结束对话,或者开始一个嵌套对话。

<h:commandLink action="main" value="Exit">
    <s:conversationPropagation type="end"/>
</h:commandLink>
<h:commandLink action="main" value="Select Child">
    <s:conversationPropagation type="nested"/>
</h:commandLink>
<h:commandLink action="main" value="Select Hotel">
    <s:conversationPropagation type="begin"/>
</h:commandLink>
<h:commandLink action="main" value="Select Hotel">
    <s:conversationPropagation type="join"/>
</h:commandLink>

这种会话模型可以非常容易地创建基于多窗口操作的应用系统。对于许多应用程序来说,这已经足够了。 但是另外一些复杂的应用程序还会需要以下额外需求中的一点或两点。

  • 一个对话范围跨越多个更小的用户交互单元,这些小单元逐个或者同步地执行。 更小的 嵌套对话 拥有它们自己的一套独立的对话状态,并且也可以访问外部对话的状态。

  • 用户能够在同一个浏览窗口中的多个对话之间进行切换。这种功能称做 工作区管理

6.2. 嵌套对话

嵌套对话是通过在一个现有对话的范围内调用一个名为 @Begin(nested=true) 的方法进行创建的。 嵌套对话有它自己的对话上下文,还可以只读地访问外部对话的上下文(它可以读取外部对话的上下文变量,但是不可以写)。 随后当遇到 @End 时,嵌套对话会被销毁,并且外部对话会弹出会话堆栈继续运行。 理论上,对话可以嵌套到任意层深。

某个用户活动(工作区管理,或返回按钮)可以在内部对话结束之前就恢复外部对话。 在这种情况下,一个外部对话就有可能同时拥有多个嵌套对话。 如果外部对话在嵌套对话之前就被结束,Seam将会把嵌套对话和外部对话一起销毁掉。

对话可以被认为是一个 连续的状态 。 嵌套对话允许应用程序在不同的用户交互点捕捉一致连续的状态,因此必须确保在返回按钮以及工作区管理的面上有真正的正确行为。

TODO:说明当你点击返回按钮时嵌套对话如何防止错误发生的一个例子。

通常,如果一个组件存在于当前嵌套对话的父对话中,嵌套对话会使用同一个实例。 少数情况下,在每个嵌套对话中都使用不同的实例会很有用,以便存在于父对话中的组件实例对其子对话是不可见的。 你可以通过给这个组件注解 @PerNestedConversation 来实现。

6.3. 使用GET请求来开始一个对话

JSF并没有定义任何类型的action监听器,这种监听器会在通过非JSF请求“non-faces request”访问页面的时候被触发(例如,一个HTTP GET请求)。 这种触发会发生在当用户用书签保存了这个页面,或者在我们通过 <h:outputLink> 访问页面的时候。

有时候,我们希望在访问页面的时候立即开始一个对话。 由于没有JSF action方法,我们不能以寻常的通过用 @Begin 标注action的方式来解决这个问题,

当页面需要把一些状态抓取到上下文变量中时,另一个问题也就随之产生了。我们已经看到有两种方法可以解决这个问题。 如果这个状态是Seam组件所持有的,我们就可以通过 @Create 方法来抓取。 如果不是,我们就可以为这个上下文变量定义一个 @Factory 方法。

如果以上两种办法都不适合你,Seam还允许你在 pages.xml 文件中定义一个 page action

<pages>
    <page view-id="/messageList.jsp" action="#{messageManager.list}"/>
    ...
</pages>

这个action方法在开始渲染响应阶段的时候被调用,即在页面就要被渲染的任何时候。 如果页面action返回一个非空的值,Seam将会根据Seam导航规则处理任何合适的JSF,可能导致渲染另外一个完全不同的页面。

如果你在渲染页面之前想要做的 仅仅 是开始一个对话,那你可以使用一个内建的action方法,它正好具备这种功能:

<pages>
    <page view-id="/messageList.jsp" action="#{conversation.begin}"/>
    ...
</pages>

注意你也可以从JSF控制器中调用内建的action来开始一个对话,同样地,你可以使用 #{conversation.end} 来结束一个对话。

如果你想要更多的控制,以加入现有的对话或开始一个嵌套对话,开始一个页面流或者开始一个原子的对话,你应该使用 <begin-conversation> 元素。

<pages>
    <page view-id="/messageList.jsp">
       <begin-conversation nested="true" pageflow="AddItem"/>
    <page>
    ...
</pages>

<end-conversation>元素也可以结束一个对话。

<pages>
    <page view-id="/home.jsp">
       <end-conversation/>
    <page>
    ...
</pages>

为了解决第一个问题,我们现在有五种选择:

  • @Begin 注解 @Create 方法

  • @Begin 注解 @Factory 方法

  • @Begin 注解Seam页面action

  • pages.xml 中使用 <begin-conversation>

  • 利用 #{conversation.begin} 作为Seam页面action方法

6.4. 利用<s:link>以及<s:button>

JSF命令链始终通过JavaScript来执行一个表单提交,它打破了浏览器的“在新窗口中打开”或者“在新标签中打开”这种特点。 在普通的JSF中,如果你需要这项功能,就需要使用 <h:outputLink>。 但是 <h:outputLink> 标签有两大限制。

  • JSF没有提供将action监听器附加给 <h:outputLink> 的方法。

  • 由于实际上没有提交表单,JSF并没有传播 DataModel 中的选中行。

Seam提供了一个 page action 的概念来帮助解决第一个问题,但是这对于第二个问题却无能为力。 我们 可以 利用REST的方法传递请求参数以及重新查询服务端的选中对象来解决这个问题。 在某些情况下——例如Seam博客上范例应用程序那样——这实际上最好的方法。REST风格支持书签,因为它不需要服务器端的状态。 在其他那些我们不需要关心书签的情况下,使用 @DataModel 以及 @DataModelSelection 就很方便也很透明!

为了填补这项缺失的功能,也为了使对话传播的管理变得更加简单,Seam提供 <s:link> 这样一个JSF标签。

这个连接可以仅指定JSF视图的id:

<s:link view="/login.xhtml" value="Login"/>

或者,它可以指定一个action方法(在这种情况下action的输出将会决定结果页面):

<s:link action="#{login.logout}" value="Logout"/>

如果你把JSF视图id和action方法这 两者 都指定的话,“视图”将会被使用, 除非action方法返回一个非空的结果:

<s:link view="/loggedOut.xhtml"  action="#{login.logout}" value="Logout"/>

这个连接自动地利用内部的 <h:dataTable> 传播 DataModel 的所选行。

<s:link view="/hotel.xhtml" action="#{hotelSearch.selectHotel}" value="#{hotel.name}"/>

你可以不指定现有对话的范围:

<s:link view="/main.xhtml" propagation="none"/>

你可以开始、结束或者嵌套对话:

<s:link action="#{issueEditor.viewComment}" propagation="nest"/>

如果一个连接开始了一个对话,你甚至可以指定一个要使用的页面流:

<s:link action="#{documentEditor.getDocument}" propagation="begin"
        pageflow="EditDocument"/>

如果使用jBPM任务列表,那么你可以使用 taskInstance 属性:

<s:link action="#{documentApproval.approveOrReject}" taskInstance="#{task}"/>

(请见DVD Store演示的应用程序中针对以上用法的范例。)

最后,如果你希望“链接”被渲染成为一个按钮,就使用 <s:button>

<s:button action="#{login.logout}" value="Logout"/>

6.5. 成功信息

给用户显示一条action执行成功或者失败的消息是相当常见的功能。为此使用JSF的 FacesMessage 是非常方便的。 不幸的是,成功的action通常需要一个浏览器重定向。这使得在普通的JSF中显示成功信息变得相当困难。

内建的会话范围的Seam组件 facesMessages 解决了这个问题。 (你必须安装Seam重定向过滤器。)

@Name("editDocumentAction")
@Stateless
public class EditDocumentBean implements EditDocument {
    @In EntityManager em;
    @In Document document;
    @In FacesMessages facesMessages;

    public String update() {
        em.merge(document);
        facesMessages.add("Document updated");
    }
}

对于当前的会话来说,任何加入到 facesMessages 的消息都正好用在下一个渲染阶段中。 甚至当没有“长时间运行”对话的时候也会奏效,因为Seam甚至在重定向过程中保留了临时对话。

你甚至可以在faces message概述中包含JSF EL表达式:

facesMessages.add("Document #{document.title} was updated");

你可以按照通常的方式显示消息,例如:

<h:messages globalOnly="true"/>

6.6. 使用“显式”的对话id

通常情况下,Seam会给每个对话产生一个无意义且唯一的id。你可以在你开始一个对话的时候定制id的值。

这个特性可以用来定制会话id生成算法,像这样:

@Begin(id="#{myConversationIdGenerator.nextId}")
public void editHotel() { ... }

或者它可以用来分配一个有意义的对话id:

@Begin(id="hotel#{hotel.id}")
public String editHotel() { ... }
@Begin(id="hotel#{hotelsDataModel.rowData.id}")
public String selectHotel() { ... }
@Begin(id="entry#{params['blogId']}")
public String viewBlogEntry() { ... }
@BeginTask(id="task#{taskInstance.id}")
public String approveDocument() { ... }

毫无疑问,每当选中一家特殊的酒店、一篇特殊的博客或者一项特殊的任务时,这些例子都会产生一个相同的对话id。 那么,如果一相新对话开始时已经存在一个包含相同对话id的对话时,会发生什么情况呢? 嗯,Seam竟然会发现现有的对话,并重定向到该对话,而不去再次运行 @Begin 方法。 这个特性会帮助我们控制在使用工作区管理时创建的多个工作区。

6.7. 工作区管理

工作区管理指的是可以在一个单独的窗口中"切换"多个对话的能力。 Seam在Java代码级别完全透明地管理工作区。为了启用工作区管理,你所需要做的全部事情如下:

  • 为每个视图id(在使用JSF或Seam导航规则时)或者页面节点(在使用JPDL页面流时)提供一个 描述 文本。 这个描述文本通过工作区切换器显示给用户。

  • 在你的页面中包含一个或多个标准JSP或facelets片断的工作区转换器。 标准片断支持通过下拉菜单、对话列表或者导航控件来管理工作区。

6.7.1. 工作区管理及JSF导航

当你使用JSF或者Seam导航规则的时候,Seam会通过恢复对话的当前 view-id 切换到该对话。 工作区的描述文本在一个名为 pages.xml 的文件中定义, Seam希望在 WEB-INF 目录中找到它,这个文件就放在 faces-config.xml 旁边。

<pages>
    <page view-id="/main.xhtml">Search hotels: #{hotelBooking.searchString}</page>
    <page view-id="/hotel.xhtml">View hotel: #{hotel.name}</page>
    <page view-id="/book.xhtml">Book hotel: #{hotel.name}</page>
    <page view-id="/confirm.xhtml">Confirm: #{booking.description}</page>
</pages>

注意,如果找不到这个文件,Seam应用程序会继续正常地运行!只不过会失去切换工作区的功能。

6.7.2. 工作区管理和jPDL页面流

当你使用jPDL页面流程定义的时候,Seam通过恢复当前jBPM流程状态切换到一个对话。 这是一个更加灵活的模型,因为它允许同一个 view-id 根据当前的 <页面> 节点而拥有不同的描述。 这个描述文本通过 <page> 节点来定义。

<pageflow-definition name="shopping">

   <start-state name="start">
      <transition to="browse"/>
   </start-state>

   <page name="browse" view-id="/browse.xhtml">
      <description>DVD Search: #{search.searchPattern}</description>
      <transition to="browse"/>
      <transition name="checkout" to="checkout"/>
   </page>

   <page name="checkout" view-id="/checkout.xhtml">
      <description>Purchase: $#{cart.total}</description>
      <transition to="checkout"/>
      <transition name="complete" to="complete"/>
   </page>

   <page name="complete" view-id="/complete.xhtml">
      <end-conversation />
   </page>

</pageflow-definition>

6.7.3. 对话转换器

在你的JSP或facelets页面中包含以下代码片断,以获得使你可以转换到任何当前对话或者应用程序的任何其他页面的一个下拉菜单ʍ