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页面中包含以下代码片断,以获得使你可以转换到任何当前对话或者应用程序的任何其他页面的一个下拉菜单:

<h:selectOneMenu value="#{switcher.conversationIdOrOutcome}">
    <f:selectItem itemLabel="Find Issues" itemValue="findIssue"/>
    <f:selectItem itemLabel="Create Issue" itemValue="editIssue"/>
    <f:selectItems value="#{switcher.selectItems}"/>
</h:selectOneMenu>
<h:commandButton action="#{switcher.select}" value="Switch"/>

In this example, we have a menu that includes an item for each conversation, together with two additional items that let the user begin a new conversation. 在这个例子中,我们有一菜单,它为每一个对话都包含了一个条目,另外还有两个让用户开始新对话的条目。

Only conversations with a description will be included in the drop-down menu.

6.7.4. 对话列表

除了对话列表会显示成为一个表格以外,它与对话转换器非常相似:

<h:dataTable value="#{conversationList}" var="entry"
        rendered="#{not empty conversationList}">
    <h:column>
        <f:facet name="header">Workspace</f:facet>
        <h:commandLink action="#{entry.select}" value="#{entry.description}"/>
        <h:outputText value="[current]" rendered="#{entry.current}"/>
    </h:column>
    <h:column>
        <f:facet name="header">Activity</f:facet>
        <h:outputText value="#{entry.startDatetime}">
            <f:convertDateTime type="time" pattern="hh:mm a"/>
        </h:outputText>
        <h:outputText value=" - "/>
        <h:outputText value="#{entry.lastDatetime}">
            <f:convertDateTime type="time" pattern="hh:mm a"/>
        </h:outputText>
    </h:column>
    <h:column>
        <f:facet name="header">Action</f:facet>
        <h:commandButton action="#{entry.select}" value="#{msg.Switch}"/>
        <h:commandButton action="#{entry.destroy}" value="#{msg.Destroy}"/>
    </h:column>
</h:dataTable>

我们设想你想要根据你自己的应用程序去定制这个。

只有带有描述的对话才会被包含在列表中。

注意对话列表允许用户销毁工作区。

6.7.5. 导航控件

导航控件在使用嵌套对话模式的应用程序中很有用。导航控件是当前对话堆栈中对话链接的一个列表。

<ui:repeat value="#{conversationStack}" var="entry">
    <h:outputText value=" | "/>
    <h:commandLink value="#{entry.description}" action="#{entry.select}"/>
</ui:repeat

6.8. 对话组件和JSF组件绑定

对话组件有一个小小的限制:它们不能够被用来保存对JSF组件的绑定。 (除非绝对必要,否则我们通常不喜欢使用JSF的这个特性,因为它创建了从应用程序逻辑到视图的强依赖关系。) 在一个postback请求中,组件绑定会在视图恢复阶段中且在Seam对话上下文恢复之前被更新。

为了解决这个问题,使用一个事件范围的组件来保存组件绑定,并将它注入到需要它的对话范围组件中。

@Name("grid")
@Scope(ScopeType.EVENT)
public class Grid
{
    private HtmlPanelGrid htmlPanelGrid;

    // getters and setters
    ...
}
@Name("gridEditor")
@Scope(ScopeType.CONVERSATION)
public class GridEditor
{
    @In(required=false)
    private Grid grid;

    ...
}

另外一种选择是,你可以通过隐式的 uiComponent 句柄来访问JSF组件树。 下面这个例子访问在迭代中支持数据表的 UIData 组件的 getRowIndex() ,它打印当前的行数:

<h:dataTable id="lineItemTable" var="lineItem" value="#{orderHome.lineItems}">
   <h:column>
      Row: #{uiComponent['lineItemTable'].rowIndex}
   </h:column>
   ...
</h:dataTable>

在这个map中,可以得到JSF UI组件和它们的客户标识符。

6.9. 对话组件的并发调用

在 ??? 中可以找到Seam组件并发调用的全部讨论。 在这里,我们要讨论是常见的一种并发情形,在这种情形下,你会遇到从AJAX请求中访问对话组件的并发—。 我们就要讨论一个Ajax客户端库应该提供用来控制源自客户端的事件的选项—,并看看RickFaces为你提供的选项。

对话组件实际上并不允许真正的并发访问,因此Seam会给每一个请求排一个队列并依次处理它们。这样允许每个请求都以一种特定的方式被执行。 但是,简单的队列不是那么强大—首先,由于某些原因,如果一个方法需要非常长的时间来完成,并且每次生成一个错误的请求时就一次又一次的反复运行(潜在拒绝服务攻击的可能)。 其次,AJAX经常被用于给用户提供快速的状态更新,因此持续地长时间运行该动作并没有什么用处。

因此Seam给action事件排队等待一段时间(并发的请求超时); 如果不能及时处理事件,它就会创建一个临时的对话并且给用户打印一条信息,让他们了解情况。 所以不要给服务器发送泛滥的AJAX事件,这是非常重要的。

我们可以在components.xml文件中给并发的请求超时设置一个合理的超时默认值(ms)。

<core:manager concurrent-request-timeout="500" />

到目前为止,我们已经讨论了同步的AJAX请求—— 客户端告诉服务器发生了一个事件,然后根据返回值来重新渲染局部页面。 当AJAX请求是轻量级时,采用这种方法就相当好(这些方法的调用也简单,如:计算一列数字的总和)。 但是当我们需要做复杂的计算时我们应该怎么做呢?

对于大量的计算我们应当使用真正的异步(基于轮询“Poll”的)方法 — 客户端发送一个AJAX请求到服务器,这使得一个action在服务器端被异步地执行(因此立即就会响应客户端);然后客户端会在服务器中查询更新。 当你运行一个长时间运行的操作时它很有用,因为每一个action都可以执行是非常重要的(你不会希望某些action由于重复或者超时而被丢弃)。

我们应如何来设计对话的AJAX应用程序呢?

首先,你需要决定是否想使用更简单的“同步”请方式,或者是否想要利用“Poll风格”的方法。

如果你要使用“同步”请求的方法,那么你需要评估一下你的AJAX请求需要多长时间才可以结束——它是不是比并发请求超时值要短? 如果不是,你也许要修改这个方法的并发请求超时时间(如上所述)。 接下来你或许需要在客户端给请求排一个队,防止请求全部涌入服务器。 如果这是一个经常发生的事件(例如,按钮按下和输入域的onblur),并且立即更新客户端又不是优先需要考虑的情况,你就应该在客户端设置一个请求延时。 当消耗完你的请求延时的时候,该事件中的操作也会在服务器段排成一个队列。

最后,客户端库可能会提供一个选项,它可以放弃最近未完成的重复请求。 对这个选项你需要很谨慎,因为在服务端没能放弃未完成的请求时,这个选项可能导致请求全部涌入服务器端。

使用“Poll风格”的设计比较不需要细调。 你只要给你的action方法 @Asynchronous 进行标注,并确定一个查询时间间隔就可以了:

int total;

// This method is called when an event occurs on the client
// It takes a really long time to execute
@Asynchronous
public void calculateTotal() {
   total = someReallyComplicatedCalculation();
}

// This method is called as the result of the poll
// It's very quick to execute
public int getTotal() {
   return total;
}

6.9.1. RichFaces Ajax

RichFaces AJAX是最常与Seam一起使用的Ajax库,它提供了上面讨论过的所有控制:

  • eventsQueue — 提供一个放置事件的队列。所有的事件都排成队列,并且请求被依次发送给服务器端。 当服务器没有被拒绝服务攻击时,若一个请求在服务器上需要花费一些时间来执行时,这个是很有用的(比如:大量的计算,从慢的数据源里获取信息)。

  • 如果最近在队列中已经有‘相似的’请求, ignoreDupResponses — 就会忽略由该请求产生的响应。 ignoreDupResponses="true" 不会取消 请求在服务端的处理 — 它只是在客户端防止不必要的更新。

    这个选项与Seam对话一起使用时应该很小心,因为它允许创建多个并发请求。

  • requestDelay — 定义请求存在于队列中的时间(ms)。 如果这个请求在这个时间内没有被处理,它就会被发送(不管是否接收到response)或者丢弃(如果队列中有更近的相似事件)。

    这个选项与Seam的对话一起使用应该很小心,因为它允许创建多个并发请求。 你要确定你所设置的延时时间(结合并发请求超时的时间)要长于action的执行时间。

  • <a:poll reRender="total" interval="1000" /> — 请求服务器端,并重新渲染一个需要的区域。

Chapter 7. 页面流和业务流程

JBoss jBPM 是一个可以运行于任何Java SE或EE环境的业务流程管理引擎。 jBPM使用一组节点组成的图形来描述一个业务流程或用户交互过程,这些节点分别表示等待状态、决策、任务、Web页面等等。 这个图形使用一种简单的、非常易读的,称为jPDL的XML方言来定义,并且可以使用一个Eclipse插件图形化显示和编辑。 jPDL是一种可扩展的语言,适合于解决许多问题,从定义Web应用程序页面流到传统的工作流管理,直到在SOA环境中组织服务。

Seam应用程序使用jBPM解决两个不同的问题:

  • 定义包含复杂用户交互的页面流,jDPL流程定义可以用来为一次对话定义页面流。 一次Seam对话可以理解为与一个单一用户进行的一次相关的短暂交互。

  • 定义主控业务流程。业务流程可能是横跨多个用户的多次对话。流程的状态被保存在jBPM数据库中,所以流程被认为是长时间运行的。 协调多个用户的行为远比处理一个单一用户的交互复杂得多,对此jBPM提供了成熟的工具来解决任务管理和多路并发执行的问题。

不要把这两个事情弄混了!它们运行在两个非常不同的层面或粒度中。 页面流Pageflow对话Conversation任务Task 全部来自于一次与单一用户的单一交互。 而一个业务流程则横跨许多任务。进一步说,这两种jBPM应用是完全正交的,可以在一起使用或是分开单独使用,或者都不用。

使用Seam不必了解jDPL。如果你完全乐于使用JSF或是Seam导航规则定义页面流,并且你的应用程序更多的是数据驱动而不是流程驱动,可能就不需要jBPM。 但是我们发现,根据一个定义良好的图形考虑用户交互有助于我们创建更加有生命力的应用程序。

7.1. Seam中的页面流

在Seam中定义页面流有两种方法:

  • 使用JSF或是Seam导航规则 – 无状态的导航模型

  • 使用jPDL – 有状态的导航模型

非常简单的应用程序只需要无状态的导航模型。非常复杂的应用程序则应该在不同的场合结合使用这两种模型。每种模型各有优缺点。

7.1.1. 两种导航模型

无状态的模型定义一种映射,把事件的一组命名的逻辑结果直接映射到视图的结果页面。导航规则除了记录哪个页面是事件来源之外不会保留任何状态。 这意味着Action监听方法有时必须进行一些页面流转的处理,因为只有它们能访问到应用程序的当前状态。

下面是一个使用JSF导航规则定义页面流的例子:

<navigation-rule>
    <from-view-id>/numberGuess.jsp</from-view-id>
        
    <navigation-case>
        <from-outcome>guess</from-outcome>
        <to-view-id>/numberGuess.jsp</to-view-id>
        <redirect/>
    </navigation-case>

    <navigation-case>
        <from-outcome>win</from-outcome>
        <to-view-id>/win.jsp</to-view-id>
        <redirect/>
    </navigation-case>
        
    <navigation-case>
        <from-outcome>lose</from-outcome>
        <to-view-id>/lose.jsp</to-view-id>
        <redirect/>
    </navigation-case>

</navigation-rule>

同一个例子,下面使用Seam导航规则定义:

<page view-id="/numberGuess.jsp">
        
    <navigation>
        <rule if-outcome="guess">
            <redirect view-id="/numberGuess.jsp"/>
        </rule>
        <rule if-outcome="win">
            <redirect view-id="/win.jsp"/>
        </rule>
        <rule if-outcome="lose">
            <redirect view-id="/lose.jsp"/>
        </rule>
    </navigation>

</page>

如果你觉得导航规则过于繁琐,可以在Action监听方法里直接返回View的id。

public String guess() {
    if (guess==randomNumber) return "/win.jsp";
    if (++guessCount==maxGuesses) return "/lose.jsp";
    return null;
}

注意这会导致一个重定向,甚至可以指定要在重定向中使用的一些参数。

public String search() {
    return "/searchResults.jsp?searchPattern=#{searchAction.searchPattern}";
}

有状态模型在一组具名的、合乎逻辑的应用状态之间定义一组重定向。 在这种模型中,可以完全使用jPDL页面流定义来表示任意用户的交互流程,并在编写Action监听方法完全不必知道用户的交互流程。

下面是一个使用jPDL定义页面流的例子:

<pageflow-definition name="numberGuess">
    
   <start-page name="displayGuess" view-id="/numberGuess.jsp">
      <redirect/>
      <transition name="guess" to="evaluateGuess">
      	<action expression="#{numberGuess.guess}" />
      </transition>
   </start-page>
   
   <decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
      <transition name="true" to="win"/>
      <transition name="false" to="evaluateRemainingGuesses"/>
   </decision>
   
   <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>

这里我们马上注意到了两件事:

  • JSF/Seam导航规则 更加 简单。(然而,隐藏在后面的Java代码会相当复杂。)

  • jPDL让用户交互的过程立刻变得直接易懂,我们甚至不需要考虑JSP或Java代码。

另外,有状态模型更加 受约束 。对于每一种逻辑状态(页面流中的每一步)而言,都存在一组受限的可能转到其他状态的重定向。 而无状态模型则是一种 实时的 模型,它适用于相对不受约束的、形式自由的导航,在这种情况下,由用户决定他/她下一步想重定向到哪里,而不是应用程序。

有状态和无状态导航之间的差别与传统的模态和非模态交互视图之间的差别很类似。 现在,Seam应用程序通常不是模态的,简单地说 -- 的确,使用对话的一个主要原因就是避免应用程序中的模态行为! 然而,在一个具体对话的级别里Seam应用程序可以是(甚至经常是)模态的。 众所周知,应该尽可能地避免模态行为;预知用户想要做事的顺序是非常困难的!然而,毫无疑问,有状态模型也是需要的。

这两种模型之间的最大不同是后退按钮行为。

7.1.2. Seam和后退按钮

当使用JSF或Seam导航规则时,Seam允许用户通过后退、前进和刷新按钮自由地导航。当用户有以上操作时,应用程序的职责是确保发生对话状态的内部保持一致。 许多开发者都将Web应用框架例如Struts或WebWork -- 它们不支持对话模型 -- 和无状态组件模型例如EJB无状态Session Beans或Spring框架相结合过,这使得他们认为这几乎是不可能的一件事。 然而,就我们的经验,在Seam的上下文中,存在一个定义良好的对话模型,基于有状态的Session Bean,实现会话状态的内部一致实际上非常容易。 通常,在Action监听方法的开始把 no-conversation-view-id 的使用和null检查结合起来是很简单的。 我们认为支持自由导航将几乎总是可行的。

既然如此,pages.xml 中就包含一个 no-conversation-view-id 声明。 它告诉Seam,如果在一个对话当中有一个页面渲染的请求,而这个对话不再存在,就重定向到一个不同的页面。

<page view-id="/checkout.xhtml" 
        no-conversation-view-id="/main.xhtml"/>

另一方面,在有状态的模型中,后退按钮被认为是一个返回前一状态的未定义跳转。 因为有状态模型强迫当前状态必须有一组明确定义的跳转,所以后退按钮在有状态模型中是被默认禁止的! Seam明确地监测到后退按钮的使用,并且阻止执行前一个Action和访问过期的页面,而后简单地将用户重定向回当前页面(并且显示一个消息)。 这到底是有状态模型的一个特性还是一个限制,则取决于你在其中的角色:作为一个开发者,这是一个特性; 作为用户,这可能是一个限制!可以为一些特殊的Page节点设置 back="enabled" 属性,从而允许后退按钮导航。

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

这个配置允许使用后退按钮 checkout 状态转到 任意前一个状态

当然,如果在一个页面流中有一个页面渲染的请求,并且这个页面流的对话不再存在,我们依然需要决定如何处理这种情况。 既然这样,需要在页面流定义中包括一个 no-conversation-view-id 声明:

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled" 
        no-conversation-view-id="/main.xhtml">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

在实践中,这两种导航模型都有它们自己的用处,你将很快学会如何在这两种模型之间做出选择。

7.2. 使用jPDL页面流

7.2.1. 安装页面流

我们需要安装Seam jBPM相关的组件,并且告诉它们在哪里找到页面流定义。我们可以在 components.xml 配置文件中指定这个配置。

<bpm:jbpm>
    <bpm:pageflow-definitions>
        <value>pageflow.jpdl.xml</value>
    </bpm:pageflow-definitions>
</bpm:jbpm>

第一行安装jBPM,第二行指向一个基于jPDL的页面流定义。

7.2.2. 开始页面流

我们可以通过在 @Begin@BeginTask@StartTask 的注解中指定流程定义的名字来“启动”一个基于jPDL的页面流。

@Begin(pageflow="numberguess")
public void begin() { ... }

作为选择,我们也可以使用pages.xml来启动一个页面流:

<page>
        <begin-conversation pageflow="numberguess"/>
    </page>

在上面这个例子中,如果我们在渲染阶段 RENDER_RESPONSE 阶段— 例如,在一个 @Factory@Create 方法中 -- 启动一个页面流,那就意味着我们已经处在已产生的页面中,就要使用页面流中的 <start-page> 节点作为页面流的第一个节点,如上例示所示。

但是如果这个页面流作为一个Action监听器的执行结果而启动的话,这个action监听器的结果则将决定哪个页面作为第一个被渲染的页面。 既然这样,我们在页面流中使用一个 <start-state> 作为第一个节点,并且为每一个可能的结果声明一个跳转。

<pageflow-definition name="viewEditDocument">

    <start-state name="start">
        <transition name="documentFound" to="displayDocument"/>
        <transition name="documentNotFound" to="notFound"/>
    </start-state>
    
    <page name="displayDocument" view-id="/document.jsp">
        <transition name="edit" to="editDocument"/>
        <transition name="done" to="main"/>
    </page>
    
    ...
    
    <page name="notFound" view-id="/404.jsp">
        <end-conversation/>
    </page>
    
</pageflow-definition>

7.2.3. 页面节点和跳转

每一个 <page> 节点描绘一种状态,在该状态下系统等待用户的输入:

<page name="displayGuess" view-id="/numberGuess.jsp">
    <redirect/>
    <transition name="guess" to="evaluateGuess">
        <action expression="#{numberGuess.guess}" />
    </transition>
</page>

其中的 view-id 是一个JSF 视图id。 <redirect/> 元素跟JSF导航规则中的 <redirect/> 有相同的作用: 即,一个post-then-redirect(提交后跳转)行为,这样做是为了防止用户点击了浏览器的刷新按钮进行重复刷新。 (需要注意的是,Seam会在这些浏览器重定向中保存对话的上下文,所以在Seam中不需要类似ROR(Ruby On Rails)风格的”flash”构造!)

跳转的名字是一个JSF输出的名字,在 numberGuess.jsp 中通过点击一个Command按钮或是Command链接触发这个跳转。

<h:commandButton type="submit" value="Guess" action="guess"/>

当点击这个按钮触发这个跳转之后,jBPM将会通过调用 numberGuess 组件的 guess() 方法激活跳转Action。 注意到jPDL中用于指定Action的语法类似于JSF的EL表达式,并且这个跳转的Action Handler不过是Seam当前上下文中的一个组件里的一个方法。 同JSF事件一样,我们可以在jBPM中拥有完全相同的事件模型!(同一类型准则)

在没有指定输出的情况下(例如,一个Command按钮没有定义 action 属性), 如果存在没有指定name的跳转,Seam将激活该跳转,或者如果所有的跳转都有name的话,Seam简单地重新显示该页。 所以我们可以稍微地简化示例页面流和这个按钮:

<h:commandButton type="submit" value="Guess"/>

将激活一个未命名的跳转:

<page name="displayGuess" view-id="/numberGuess.jsp">
    <redirect/>
    <transition to="evaluateGuess">
        <action expression="#{numberGuess.guess}" />
    </transition>
</page>

甚至可以通过点击一个按钮来执行一个Action方法,在这种情况下,Action执行的结果将决定采用哪个跳转:

<h:commandButton type="submit" value="Guess" action="#{numberGuess.guess}"/>
<page name="displayGuess" view-id="/numberGuess.jsp">
    <transition name="correctGuess" to="win"/>
    <transition name="incorrectGuess" to="evaluateGuess"/>
</page>

然而,这是一种不可取的方式,因为它把流程控制的职责从页面流定义转到了其他组件。最好还是把这些相关的职责集中到页面流本身。

7.2.4. 流程控制

通常,我们定义页面流的时候不需要更多强大的jPDL特性,然而我们还是需要 <decision> 节点。

<decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
    <transition name="true" to="win"/>
    <transition name="false" to="evaluateRemainingGuesses"/>
</decision>

通过在Seam上下文中执行一个JSF EL表达式来决定如何跳转。

7.2.5. 流程的结束

使用 <end-conversation> 或是 @End 结束对话。 (实际上,为了程序的易读性,鼓励 两者共同 使用。)

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

可选择的,可以指定一个jBPM的 状态转移(transition) 名字结束一个任务。 在这种情况下,Seam将给主控的业务流程发送一个结束当前任务的信号。

<page name="win" view-id="/win.jsp">
    <redirect/>
    <end-task transition="success"/>
</page>

7.2.6. 页面流组合

多个页面流可以进行组合,并当另一个页面流执行时暂停一个页面流。 这个 <process-state> 节点的作用就是暂停外部的页面流,同时开始执行一个命名的页面流。

<process-state name="cheat">
    <sub-process name="cheat"/>
    <transition to="displayGuess"/>
</process-state>

这个内部页面流从 <start-state> 节点开始执行。 当执行到 <end-state> 节点时,内部页面流执行完毕,同时外部的页面流会以 <process-state> 定义的跳转恢复执行。

7.3. Seam中的业务流程管理

一个业务流程由一系列定义良好的任务组成,这些任务必须由用户或是软件系统遵照一系列定义良好的规则来完成,这些规则规定了 可以执行任务,什么时候 应该执行。 Seam对jBPM的整合使得给用户显示任务列表以及使用户管理他们的任务变得简单。 Seam还使应用程序在 BUSINESS_PROCESS 的上下文中存储与业务流程相关的状态,通过jBPM变量来持久化这个状态。

一个简单的业务流程定义看起来跟页面流定义非常相似(是同一种类型的东西),不同的是用 <task-node> 节点替换了 <page> 节点。在一个长运行期的业务流程中,等待状态表示系统正在等待用户登录并执行任务。

<process-definition name="todo">
   
   <start-state name="start">
      <transition to="todo"/>
   </start-state>
   
   <task-node name="todo">
      <task name="todo" description="#{todoList.description}">
         <assignment actor-id="#{actor.id}"/>
      </task>
      <transition to="done"/>
   </task-node>
   
   <end-state name="done"/>
   
</process-definition>

我们很有可能在一个项目中同时使用jPDL业务流程定义和jPDL页面流程定义。 如果这样,他们二者的关系是:一个业务流程中的 <task> 对应一个完整的页面流 <pageflow-definition>

7.4. 使用jPDL业务流程定义

7.4.1. 安装流程定义

我们需要安装jBPM,并且告诉它到哪里可以找到业务流程定义文件:

<bpm:jbpm>
    <bpm:process-definitions>
        <value>todo.jpdl.xml</value>
    </bpm:process-definitions>
</bpm:jbpm>

7.4.2. 初始化Actor id

我们总是需要知道当前的登录用户。jBPM使用 actor idgroup actor id “识别”用户。 我们使用Seam内置的 actor 组件指定当前用户的id。

@In Actor actor;

public String login() {
    ...
    actor.setId( user.getUserName() );
    actor.getGroupActorIds().addAll( user.getGroupNames() );
    ...
}

7.4.3. 启动一个业务流程

使用 @CreateProcess 注解来启动一个业务流程实例。

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

也可用使用pages.xml来启动一个业务流程。

<page>
    <create-process definition="todo" />
</page>

7.4.4. 任务分配

当一个流程执行到一个任务节点时,会创建任务实例。这些任务实例必须分配给用户或是用户组。我们可以手动编码指定actor id或是委托给一个Seam组件。

<task name="todo" description="#{todoList.description}">
    <assignment actor-id="#{actor.id}"/>
</task>

在这里例子中,我们简单的将任务分配给当前用户。也可以将任务分配给一批用户(pool actor):

<task name="todo" description="#{todoList.description}">
    <assignment pooled-actors="employees"/>
</task>

7.4.5. 任务列表

几个内置的Seam组件使得显示任务列表变得简单。pooledTaskInstanceList 是一个汇集任务集合,用户可以把这些任务分配给他们自己。

<h:dataTable value="#{pooledTaskInstanceList}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{pooledTask.assignToCurrentActor}" value="Assign" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

请注意,我们可以使用普通的JSF标签 <h:commandLink> 来替代 <s:link> 标签:

<h:commandLink action="#{pooledTask.assignToCurrentActor}"> 
    <f:param name="taskId" value="#{task.id}"/>
</h:commandLink>

pooledTask 组件是一个内置组件,它简单把任务分配给当前用户。

taskInstanceListForType组件包含一种特殊类型的任务,这些任务分配给当前用户:

<h:dataTable value="#{taskInstanceListForType['todo']}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{todoList.start}" value="Start Work" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

7.4.6. 执行一个任务

我们在监听方法上使用 @StartTask 或者 @BeginTask 注解来执行一个任务。

@StartTask
public String start() { ... }

我们也可以使用pages.xml来执行一个任务。

<page>
    <start-task />
</page>

这些注解启动一种特殊类型的对话,这个对话在整个业务流程中具有意义。通过该对话可以访问保存在业务流程上下文中的状态。

如果我们使用 @EndTask 注解来结束该对话,Seam会发出信号完成该任务。

@EndTask(transition="completed")
public String completed() { ... }

作为选择,我们还可以使用pages.xml

<page>
    <end-task transition="completed" />
</page>

也可以使用EL在pages.xml指定transition 。

此时,jBPM接受指令并且继续执行业务流程定义。(在更加复杂的流程中,在流程向下执行之前,可能还需要完成其他一些任务。)

若想对jBPM处理复杂业务流程的高级特性有一个更加彻底的认识,请参阅jBPM的参考文档。

Chapter 8. Seam和对象/关系映射

Seam给两个最流行的Java持久化架构:Hibernate3和由EJB 3.0引入的Java Persistence API提供了广泛支持。 Seam独有的状态管理架构允许任意web应用框架与大多数成熟的ORM进行集成。

8.1. 简介

Seam是从Hibernate团队试图生成典型的无状态Java应用架构的挫折中成长起来的。 上一代Java应用程序的无状态特性让Hibernate团队饱受挫折,Seam吸取了他们的经验。 Seam的状态管理架构最早是用来解决持久化冲突相关问题的,特别是 乐观事务处理 相关的问题。可扩展的在线应用经常使用乐观事务。 一个原子(database/JTA)级的事务不应该跨用户交互,除非系统设计时就是只支撑很少量的并发客户端。 但几乎所有涉及到的工作都是先将数据展现给用户,没多久后更新这个数据。所以Hibernate是依据支持一种跨乐观事务的持久化上下文的思想设计的。

不幸的是这个先于Seam和EJB3.0出现的所谓“无状态”架构并不对乐观事务进行支持。而相反,这些架构提供对于原子事务级的持久化上下文的支持。 这当然给用户带来了很多麻烦,这也是用户抱怨排名第一的Hibernate的 LazyInitializationException 问题的原因。 我们需要的是在应用层构建对于乐观事务的支持。

EJB3.0认识到了此问题,并且也引入了有状态组件(有状态会话bean)的思想,它使用一个 扩展持久化上下文来跟踪组件的生命周期。 这是该问题的部分解决方案(对它自身而言也是一个有用的构想),然而还有两个问题:

  • 有状态会话bean的生命周期必须在Web层通过代码手动管理(这是个麻烦的问题,而且实践起来比听上去更复杂)。

  • 在同一个乐观事务的不同有状态组件间,传播持久化上下文是可行的,但很困难。

Seam通过提供对话(Conversation)和对话期间的有状态Session Bean组件来解决第一个问题(大多数会话实际上在数据层支持乐观事务)。 这对于很多不需要传递持久化上下文的简单应用(比如Seam的订阅演示程序)已经足够了。 对于更复杂的在每一个对话中的有很多松耦合组件的应用来说,组件间传播持久化上下文就成为一个重要的问题了。 所以Seam扩展了EJB 3.0的持久化上下文管理模型,以此来提供对话作用域的扩展持久化上下文。

8.2. Seam管理的事务

EJB会话Bean有声明式事务管理功能。当Bean被调用时,EJB容器能够透明地开始一个事务,在调用结束时关闭此事务。 如果我们写了一个作为JSF动作监听器的会话Bean方法,我们就可以在一个事务内处理所有与此action相关的工作,并且当我们完成此动作处理时事务必须被提交或回滚。 这是一个很棒的功能,在很多Seam应用程序中这是必需的。

但是,此方法还是有问题。Seam应用可能无法在对会话Bean的一次方法调用请求中完成所有的数据访问。

  • 此请求可能由几个松耦合组件处理,Web层独立地调用每一个组件。在Seam中,Web层的一个请求对EJB组件发起几次甚至多次调用的现象是很常见的。

  • 视图渲染可能需要延迟关联获取(lazy fetching of associations)。

每个请求的事务量越多,当我们的应用处理大量并发请求时越可能碰到原子和隔离问题。当然,所有的写操作要在一个事务中执行。

Hibernate用户开发了 "Open Session in View" 模式来解决该问题。 在Hibernate社区,"Open Session in View"曾经非常重要,这是因为像Spring这样的框架使用了事务作用域持久化上下文。 所以当未获得的关联被访问时渲染视图将引起 LazyInitializationException 异常。

这个模式通常作为一个跨越整个请求的事务来实现。 此实现方式会有几个问题,其中最严重的是只有我们提交了事务才能确认它成功完成——但在"Open Session in View"的事务提交时,视图已经完全渲染了,甚至渲染好的应答可能已经刷新到客户端。我们怎样才能通知用户他们的事务已失败呢?

Seam在解决"Open Session in View"问题时,也解决了事务隔离和关联获取问题。该方案有有两个部分:

  • 使用使用已扩展持久化上下文,可以覆盖一个会话作用域而不是单个事务作用域。

  • 每次请求使用两个事务;第一个从更新模型值的起始阶段到应用程序调用结束;第二个跨越渲染响应阶段。

下一节,我们将会告诉你如何安装一个会话作用域的持久化上下文。但首先我们需要你知道如何启用Seam事务管理。 注意你可以脱离Seam的事务管理来使用会话作用域持久化上下文。当你不使用Seam管理的持久化上下文时,你也有很多使用Seam事务管理的理由。 然而这两种功能被设计为一起使用的,一起使用时效果最好。

即使你使用EJB 3.0容器管理事务上下文,Seam事务管理也是很有用的。如果你在Java EE 5环境外使用Seam,或者在任何你想使用Seam管理的持久化上下文时,它同样很有用的。

8.2.1. 关闭Seam管理的事务

所有的JSF请求默认开启Seam事务管理。如果你想 关闭 该功能,你能在 components.xml 文件中做如下设置:

<core:init transaction-management-enabled="false"/>

<transaction:no-transaction />

8.2.2. 配置Seam事务管理器

Seam为事务的开始,提交,回滚,同步提供了一个事务管理抽象。默认情况下,Seam使用一个JTA事务组件,它同容器管理的EJB和编程式EJB事务集成。

Seam还为以下事务API提供事务组件:

  • javax.persistence.EntityTransaction 接口提供的JPA RESOURCE_LOCAL事务。
  • org.hibernate.Transaction 接口提供的Hibernate管理的事务。
  • org.springframework.transaction.PlatformTransactionManager 接口提供的Spring管理的事务。
  • 显式关闭Seam管理的事务。

向components.xml文件增加以下项来配置JPA RESOURCE_LOCAL事务,在配置文件中,#{entityManager}persistence:managed-persistence-context 组件的名称。(参考 Seam管理的持久化上下文。)

<transaction:entity-transaction entity-manager="#{entityManager}"/>

向你的components.xml文件声明以下项来配置Hibernate管理的事务,在配置文件中,#{hiberanteSession} 是项目中 persistence:managed-hibernate-session 组件的名称。(参考Seam管理的持久化上下文

<transaction:hibernate-transaction session="#{hiberanteSession}"/>

在components.xml中声明以下内容来显式地关闭Seam管理的事务:

<transaction:no-transaction />

参考 使用Spring PlatformTransactionManagement 来配置Spring管理的事务。

8.2.3. 事务同步

事务同步为各种各样的事务相关事件提供了回调功能,例如 beforeCompletion()afterCompletion() 事件。 默认情况下,Seam使用它自己的事务同步组件,一个事务被提交时,需要显式地使用Seam事务组件以确保同步回调被正确执行。 如果在Java EE 5环境里,应该在components.xml文件中声明 <transaction:ejb-transaction/>,从而保证容器在Seam的可预见范围外提交事务时,Seam同步回调被正确调用。

8.3. Seam管理的持久化上下文

如果你是在Java EE 5环境外使用Seam,你不能依靠容器来为你管理持久化上下文生命周期。 即使在Java EE 5 环境中,你可能有一个很多松耦合组件在会话作用域内相互协作的复杂应用,这种情况下你可能发现在组件间传递持久化上下文既困难又容易出错。

在任何一种情况下,你都需要在你的组件中使用一个 受管持久化上下文(在JPA中)或者一个 受管会话(Hibernate中)。一个Seam管理的持久化上下文是在会话上下文中管理一个 EntityManager 实例或者 Session 实例的内置Seam组件。你可以使用 @In 注入它。

Seam管理的持久化上下文在集群环境中尤其有效。EJB 3.0规范中不允许容器使用容器管理的扩展持久化上下文,Seam能够对此进行优化。 Seam支持扩展持久化上下文的透明故障恢复,而无需在节点间复制持久化上下文状态。(我们希望在EJB规范的下个版本中修复此漏洞。)

8.3.1. 在Seam管理的持久化上下文中使用JPA

配置一个Seam管理的持久化上下文很简单,在 components.xml 中加上:

<persistence:managed-persistence-context name="bookingDatabase"
                                  auto-create="true"
                   persistence-unit-jndi-name="java:/EntityManagerFactories/bookingData"/>

这个配置创建了一个名为 bookingDatabase 的对话作用范围Seam组件,它管理给持久化单元(EntityManagerFactory 实例)使用的 EntityManager 实例的生命周期。 其JNDI名为 java:/EntityManagerFactories/bookingData

当然,你需要确认已经在JNDI中绑定了 EntityManagerFactory。 在JBoss中,你能在 persistence.xml 中通过增加以下属性设置来绑定它。

<property name="jboss.entity.manager.factory.jndi.name"
          value="java:/EntityManagerFactories/bookingData"/>

现在我们能用以下方式来注入 EntityManager 了:

@In EntityManager bookingDatabase;

如果你正在使用EJB3,并且你的类或者方法上加了 @TransactionAttribute(REQUIRES_NEW),那么事务和持久化上下文不应该被传播到这个对象的方法调用上。 但是Seam管理的持久化上下文会被传播到会话内的所有组件上,它也会被传播到标有REQUIRES_NEW的方法上。 因此,如果你标记了一个REQUIRES_NEW方法,那么就应该用@PersistenceContext来访问实体管理器。

8.3.2. 使用Seam管理的Hibernate会话

Seam管理的Hibernate sessions和前者很相似。在 components.xml 中加入如下内容:

<persistence:hibernate-session-factory name="hibernateSessionFactory"/>

<persistence:managed-hibernate-session name="bookingDatabase"
                                auto-create="true"
                  session-factory-jndi-name="java:/bookingSessionFactory"/>

java:/bookingSessionFactory 是在 hibernate.cfg.xml 中配置的会话工厂名。

<session-factory name="java:/bookingSessionFactory">
    <property name="transaction.flush_before_completion">true</property>
    <property name="connection.release_mode">after_statement</property>
    <property name="transaction.manager_lookup_class">org.hibernate.transaction.JBossTransactionManagerLookup</property>
    <property name="transaction.factory_class">org.hibernate.transaction.JTATransactionFactory</property>
    <property name="connection.datasource">java:/bookingDatasource</property>
    ...
</session-factory>

注意:Seam并没有清空此会话,所以你应启用 hibernate.transaction.flush_before_completion,确保在JTA事务提交前会话被自动地清空缓存。

现在我们可以用以下代码向JavaBean组件中注入一个受管Hibernate Session了:

@In Session bookingDatabase;

8.3.3. Seam管理的持久化上下文和原子会话

会话期间的持久化上下文让你能编写跨越多个服务器请求的乐观事务,而且无需使用merge()操作,也不需要在每次请求开始时重载数据,也不需要处理 LazyInitializationException 异常或 NonUniqueObjectException 异常。

对任何乐观事务管理来说,事务隔离性和一致性可以使用乐观锁来获得。 幸运的是,Hibernate和EJB 3.0都通过使用 @Version 注解来使用乐观锁。

默认情况下,持久化上下文在每个事务结束时会清空缓存(与数据库同步)。有时这是我们期望的方式。 但经常我们希望所有的改变在内存中保存,且只有在会话成功结束时写回数据库。这对于正真的原子会话来说是允许的。 作为非JBoss,非Sun,非Sybase的EJB 3.0专家组成员的愚蠢和短视决策的结果,使用EJB 3.0持久化还不能简单、有效和方便的实现原子会话。 但是Hibernate通过扩展规范定义的 FlushModeType 提供了这个功能,我们也期望其它的厂商能尽快提供类似扩展。

Seam允许你在开始会话时指定FlushModeType.MANUAL参数。 目前,只有Hibernate作为持久化底层提供者时它才能正常工作,但是我们计划支持其它同类计算机厂商扩展。

@In EntityManager em; //a Seam-managed persistence context

@Begin(flushMode=MANUAL)
public void beginClaimWizard() {
    claim = em.find(Claim.class, claimId);
}

现在 claim 对象仍然在其余会话中被持久化上下文管理。我们能对此claim进行一些修改:

public void addPartyToClaim() {
    Party party = ....;
    claim.addParty(party);
}

除非我们进行显式的强制提交,否则这些改变不会被写到数据库中。

@End
public void commitClaim() {
    em.flush();
}

当然你可以在pages.xml文件中将 flushMode 值设置为 MANUAL,例如在一个导航规则中写入:

<begin-conversation flush-mode="MANUAL" />

8.4. 使用JPA “代理(delegate)”

EntityManager 接口能让你通过使用 getDelegate() 方法访问某个厂商特定的API。 很自然地,最让人感兴趣的厂商是Hibernate,其最强大的代理接口是 org.hibernate.Session。 你不必再用别的东西,相信我,我没有任何偏见。

不管你使用Hibernate(明智!)还是其他的东西(受虐狂,或者说不太聪明),你当然想在你的Seam组件中经常使用代理。下面是一个方法:

@In EntityManager entityManager;

@Create
public void init() {
    ( (Session) entityManager.getDelegate() ).enableFilter("currentVersions");
}

但类型转化毫无疑问是Java语言中最难看的语法。因此大家都应尽可能避免使用它们。这有另一个获得代理的方法。 首先将下列语句加到 components.xml 文件中:

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

现在我们可以直接注入此会话:

@In Session session;

@Create
public void init() {
    session.enableFilter("currentVersions");
}

8.5. 在EJB-QL/HQL中使用EL

无论你使用一个Seam管理的持久化上下文还是用 @PersistenceContext 注入一个容器管理的持久化上下文。 Seam都代理 EntityManager 或者 Session 对象,这使你能在查询语句中安全有效地使用EL表达式。例如这个例子:

User user = em.createQuery("from User where username=#{user.username}")
         .getSingleResult();

和下面等价:

User user = em.createQuery("from User where username=:username")
         .setParameter("username", user.getUsername())
         .getSingleResult();

当然你不要写成下面这样:

User user = em.createQuery("from User where username=" + user.getUsername()) //BAD!
         .getSingleResult();

(这在遭受SQL注入攻击时会变得低效且不堪一击。)

8.6. 使用Hibernate过滤器

Hibernate最酷最独特的功能就是 过滤器(filter)。过滤器能让你提供一个数据库中数据的受限视图。 但是应该有一个将过滤器合并到Seam应用中的简便方法,让它和Seam应用框架能工作得很好。

Seam管理的持久化上下文可以定义一系列的过滤器,这些过滤器在一个 EntityManager 或者 Hibernate Session 被创建时启用。(当然,它们只能在Hibernate做持久化底层时使用)

<persistence:filter name="regionFilter">
    <persistence:name>region</persistence:name>
    <persistence:parameters>
        <key>regionCode</key>
        <value>#{region.code}</value>
    </persistence:parameters>
</persistence:filter>

<persistence:filter name="currentFilter">
    <persistence:name>current</persistence:name>
    <persistence:parameters>
        <key>date</key>
        <value>#{currentDate}</value>
    </persistence:parameters>
</persistence:filter>

<persistence:managed-persistence-context name="personDatabase"
    persistence-unit-jndi-name="java:/EntityManagerFactories/personDatabase">
    <core:filters>
        <value>#{regionFilter}</value>
        <value>#{currentFilter}</value>
    </core:filters>
</persistence:managed-persistence-context>

Chapter 9. Seam中的JSF表单验证

在普通JSF中,验证在视图中定义:

<h:form>
    <h:messages/>

    <div>
        Country:
        <h:inputText value="#{location.country}" required="true">
            <my:validateCountry/>
        </h:inputText>
    </div>

    <div>
        Zip code:
        <h:inputText value="#{location.zip}" required="true">
            <my:validateZip/>
        </h:inputText>
    </div>

    <h:commandButton/>
</h:form>

在实践中,这种方式常常违背了DRY原则,因为很多“validation”实际上依赖的约束是数据模型的一部分,而且也有很多方法引入数据库Schema定义。 Seam使用Hibernate Validator来提供对基于model的约束支持。

让我们从定义 Location 类的约束开始:

public class Location {
    private String country;
    private String zip;

    @NotNull
    @Length(max=30)
    public String getCountry() { return country; }
    public void setCountry(String c) { country = c; }

    @NotNull
    @Length(max=6)
    @Pattern("^\d*$")
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}

好,这是一个不错的切入点,但在实践中使用自定义的约束可能比 Hibernate Validator 更优雅:

public class Location {
    private String country;
    private String zip;

    @NotNull
    @Country
    public String getCountry() { return country; }
    public void setCountry(String c) { country = c; }

    @NotNull
    @ZipCode
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}

无论我们使用哪种方式,都不需要在JSF页面中指定验证类型。我们可以使用 <s:validate> 来验证定义在model对象上的约束。

<h:form>
    <h:messages/>

    <div>
        Country:
        <h:inputText value="#{location.country}" required="true">
            <s:validate/>
        </h:inputText>
    </div>

    <div>
        Zip code:
        <h:inputText value="#{location.zip}" required="true">
            <s:validate/>
        </h:inputText>
    </div>

    <h:commandButton/>

</h:form>

注意: 在model上指定 @NotNull 并不能 在控制上省去 required="true"! 这是因为JSF验证架构的限制。

这种方式在model中 定义 约束,然后在表现层中 展示 约束违例 — 这显然是一种更好的设计。

但是,这并不比我们之前的方法简便多少,所以让我们试试 <s:validateAll>

<h:form>

    <h:messages/>

    <s:validateAll>

        <div>
            Country:
            <h:inputText value="#{location.country}" required="true"/>
        </div>

        <div>
            Zip code:
            <h:inputText value="#{location.zip}" required="true"/>
        </div>

        <h:commandButton/>

    </s:validateAll>

</h:form>

这个标签只是简单地给表单中的每个输入框增加 <s:validate> 标签。 对于一个大的表单来说,这能够减少很多打字工作量!

现在我们需要做些事情来显示验证失败时的反馈消息。当前我们是在表单的上方显示所有消息。 而我们真正想要做的是在值域后面显示错误的提示消息(普通JSF也可以实现),高亮显示字段和标签(普通JSF无法实现), 更好的是在字段后面显示一些图片(普通JSF同样无法实现)。 我们还希望在每个必须输入的字段所对应的标记前显示一个有颜色的*号。

这的确给表单中的每个字段增加了不少功能。 我们不希望给表单中的每一个字段指定图片、消息和输入字段的高亮显示和布局。所以我们将通用布局定义在facelets模板中。

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:h="http://java.sun.com/jsf/html"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:s="http://jboss.com/products/seam/taglib">

    <div>

        <s:label styleClass="#{invalid?'error':''}">
            <ui:insert name="label"/>
            <s:span styleClass="required" rendered="#{required}">*</s:span>
        </s:label>

        <span class="#{invalid?'error':''}">
            <h:graphicImage src="img/error.gif" rendered="#{invalid}"/>
            <s:validateAll>
                <ui:insert/>
            </s:validateAll>
        </span>

        <s:message styleClass="error"/>

    </div>

</ui:composition>

我们可以通过 <s:decorate> 让每一个表单字段都使用这个模板。

<h:form>

    <h:messages globalOnly="true"/>

    <s:decorate template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText value="#{location.country}" required="true"/>
    </s:decorate>

    <s:decorate template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText value="#{location.zip}" required="true"/>
    </s:decorate>

    <h:commandButton/>

</h:form>

最后,我们可以使用 RichFaces Ajax 来在用户浏览表单时显示验证消息:

<h:form>

    <h:messages globalOnly="true"/>

    <s:decorate id="countryDecoration" template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText value="#{location.country}" required="true">
            <a:support event="onblur" reRender="countryDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <s:decorate id="zipDecoration" template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText value="#{location.zip}" required="true">
            <a:support event="onblur" reRender="zipDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form>

最好为页面上的重要控件定义显式的id,特别是当你希望用像 Selenium 这样的工具来进行UI自动化测试时。 如果你没有提供显式的id,JSF会生成它们,但任何页面上的改动都会导致生成的值发生变化。

<h:form id="form">

    <h:messages globalOnly="true"/>

    <s:decorate id="countryDecoration" template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText id="country" value="#{location.country}" required="true">
            <a:support event="onblur" reRender="countryDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <s:decorate id="zipDecoration" template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText id="zip" value="#{location.zip}" required="true">
            <a:support event="onblur" reRender="zipDecoration" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form>

但是,如果你想在验证失败时指定去显示不同的消息又该怎么做呢?可以使用Seam对Hibernate Validator的消息绑定: (同样也包括其中那些类似el表达式和独立视图消息绑定之类的各种好处)

public class Location {
    private String name;
    private String zip;

    // Getters and setters for name

    @NotNull
    @Length(max=6)
    @ZipCode(message="#{messages['location.zipCode.invalid']}")
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}
location.zipCode.invalid = The zip code is not valid for #{location.name}

Chapter 10. Groovy集成

JBoss Seam的一个特性就是具有RAD(快速应用开发)能力。虽然动态语言与RAD并非同一个意思,但它却是该领域内的一个十分有趣的工具。 直到最近,选择一种动态语言就必须选择完全不同的开发平台(一个带有一系列API和运行环境的开发平台,如果你不想再使用旧的Java API, 这可能是一种幸运,因为你可能不得不被迫使用平台提供的私有API)。 Groovy 打破了这个约束,它是构建在Java虚拟机之上的动态语言。

现在,JBoss Seam通过静态语言和动态语言的无缝集成把动态语言世界和Java EE世界结合起来。 JBoss Seam让开发人员在任务中使用最佳的工具,而不需要关心上下文切换。 编写一个动态Seam组件和编写普通的Seam组件没什么两样,你使用相同的注释、相同的API,所有的一切都是相同的。

10.1. Groovy简介

Groovy是一个基于Java虚拟机的敏捷动态语言,它融合了从Python,Ruby和Smalltalk等语言中的诸多特性。Groovy的强大体现在两个方面:

  • Groovy支持Java语法:Java代码就是Groovy代码,使学习曲线非常平滑,即学习的难度变得非常低。

  • Groovy对象就是Java对象,Groovy类就是Java类:Groovy无缝集成所有已经存在的Java对象和类库。

TODO: 再写一个Groovy语法的快速入门

10.2. 用Groovy编写Seam应用

这个没有什么可多说的,Groovy对象就是Java对象,你可以使用Groovy编写任何Seam组件或者Java类并部署它们。 你也可以在一个应用中混合使用Groovy和Java类。

10.2.1. 编写Groovy组件

你可能已经注意到,Seam大量的使用注解(annotation)。要想Groovy支持注解必须确保其版本在1.1 Beta1以上。 下面是在Seam应用中使用Groovy代码的例子。

10.2.1.1. 实体

    @Entity
    @Name("hotel")
    class Hotel implements Serializable
    {
        @Id @GeneratedValue
        Long id

        @Length(max=50) @NotNull
        String name

        @Length(max=100) @NotNull
        String address

        @Length(max=40) @NotNull
        String city

        @Length(min=2, max=10) @NotNull
        String state

        @Length(min=4, max=6) @NotNull
        String zip

        @Length(min=2, max=40) @NotNull
        String country

        @Column(precision=6, scale=2)
        BigDecimal price

        @Override
        String toString()
        {
            return "Hotel(${name},${address},${city},${zip})"
        }
    }

Groovy本身就支持(getter/setter)方法特性,所以你不用显示的编写冗长的getter和setter方法:在前面的例子中, Hotel类可以通过 hotel.getCity() 这样的语法被Java访问到,getter和setter方法是在Groovy编译时生成的。 这样的语法让实体代码变得非常简洁。

Groovy1.1 Beta1暂时还不支持泛型(Generics)。一个负面影响是实体关系没有内置的类型信息。 这就必须适当的使用 @*ToMany 注解来代替简单的范型定义,就像 Collection<Entity>。 出于同样的原因,你也不能从非常有用的 Chapter 11, Seam应用程序框架 中受益。好消息是Groovy1.1正式版将支持范型(Groovy1.1 Beta2正在编写中)。

10.2.1.2. Seam组件

使用Groovy编写Seam组件与使用Java没有什么区别:使用注解将类标记为Seam组件。

@Scope(ScopeType.SESSION)
@Name("bookingList")
class BookingListAction implements Serializable
{
    @In EntityManager em
    @In User user
    @DataModel List<Booking> bookings
    @DataModelSelection Booking booking
    @Logger Log log

    @Factory public void getBookings()
    {
        bookings = em.createQuery('''
                select b from Booking b
                where b.user.username = :username
                order by b.checkinDate''')
            .setParameter("username", user.username)
            .getResultList()
    }

    public void cancel()
    {
        log.info("Cancel booking: #{bookingList.booking.id} for #{user.username}")
        Booking cancelled = em.find(Booking.class, booking.id)
        if (cancelled != null) em.remove( cancelled )
        getBookings()
        FacesMessages.instance().add("Booking cancelled for confirmation number #{bookingList.booking.id}", new Object[0])
    }
}

10.2.2. seam-gen

Seam gen透明地集成了Groovy。你可以在有seam-gen支持的项目中编写Groovy代码而不需要任何附加支持。 当你编写一个Groovy实体,只需要把 .groovy 文件放在 src/model 目录中即可。 同样的如果编写一个action,只要把 .groovy 文件放在 src/action 目录中就可以了。

10.3. 部署

部署Groovy类与部署Java类非常相像。(令人惊讶的是,不需要编写或者遵循某个3个字母的规范以支持多国语言组件框架)。

JBoss Seam拥有超越标准部署的能力,就是在开发时重新部署JavaBean Seam组件类而不必重启应用程序,这在开发/测试周期中节省了很多时间。 在 .groovy 文件被部署时,Seam对GroovyBean Seam 组件也提供了同样的热部署支持。

10.3.1. 部署Groovy代码

Groovy类就 Java类,和Java类有着同样的字节码。 部署一个Groovy实体、Groovy Session Bean或者Groovy Seam组件,编译步骤是不可缺少的。一个通用的方法是使用 groovyc Ant任务。 一旦编译,Groovy类和Java类不再有区别,应用服务器将它与Java类同样对待。这让Groovy与Java类无缝集成起来。

10.3.2. 开发时部署本地.groovy文件

JBoss Seam本身支持 .groovy 文件(不用编译)的增量热部署(必须是开发模式)。这让编辑/测试周期非常短。 为了设置.groovy部署,请按照 Section 2.7, “Seam与增量热部署” 进行配置,然后部署你的Groovy代码(.groovy文件)到 WEB-INF/dev 目录下。 不需要重启应用程序,GroovyBean组件将被增量启用(当然运行应用程序的服务器也不必重启)。

本地的.groovy文件的部署和其他的Seam热部署有同样的局限:

  • 组件必须是JavaBean或GroovyBean。不能是EJB3 Bean

  • 实体不能热部署

  • 被热部署的组件对部署在 WEB-INF/dev 以外的任何类都是不可见的

  • 必须用Seam Debug模式

10.3.3. seam-gen

Seam-gen透明地支持Groovy文件的部署和编译。包括本地 .groovy 文件在开发模式(未编译的)下的部署。 如果你构建了一个WAR类型的seam-gen项目,在 src/action 目录下的Java和Groovy类会自动参与增量热部署。 如果你在生产模式下,Groovy文件会在部署之前被编译。

你将在 examples/groovybooking 目录下的Booking Demo中找到一个支持增量热部署的完全用Groovy写的例子。

Chapter 11. Seam应用程序框架

Seam通过编写带有注解的简单Java类来让创建应用程序的工作变得非常简单,不需扩展任何特定接口和父类。但常见的编程任务还能进一步简化,这是通过一组预先创建的组件进行的,它们能够由 component.xml 文件配置(最简单的情况)或者类扩展而实现复用。

在一个Web应用程序中使用Hibernate或者JPA进行基本的数据库操作时,Seam Application Framework(Seam应用程序框架) 能够减少你需要书写的代码量。

我们需要强调的是,这个框架非常的简单,只是少量的易于理解和扩展的简单类。 “魔力”来自于Seam自身 — 即使没有用这个框架来创建任何Seam应用程序的时候,你也同样用到这一“魔力”。

11.1. 简介

有两种不同的方法使用Seam Application Framework所提供的组件。第一种方法是像处理其他种类的Seam内置组件一样,在 components.xml 中安装和配置组件的实例。 举例来说,下列 components.xml 中的片段安装了一个能够为 Person 实体执行基本的CRUD(创建(Create)、读取(Retrieve) 、更新(Update)和删除(Delete))操作的组件:

<framework:entity-home name="personHome"
                       entity-class="eg.Person"
                       entity-manager="#{personDatabase}">
    <framework:id>#{param.personId}</framework:id>
</framework:entity-home>

如果上面的代码按你的口味来说太像“用XML编程”,你可以改为使用扩展:

@Stateful
@Name("personHome")
public class PersonHome extends EntityHome<Person> implements LocalPersonHome {
    @RequestParameter String personId;
    @In EntityManager personDatabase;

    public Object getId() { return personId; }
    public EntityManager getEntityManager() { return personDatabase; }

}

第二种方法有一个很大的优点:你能够方便地添加额外的功能,覆盖内置的功能(框架的类都精心设计以便于扩展和定制)。

第二个优点是:如果你喜欢的话,你的类可以是有状态会话Bean(这不是必须的,也可以是普通的JavaBean组件,如果你喜欢的话)。如果你正在使用JBoss AS,你需要使用4.2.2.GA或更高的版本。

目前,Seam应用框架提供了四个内置的组件:用于CRUD操作的 EntityHomeHibernateEntityHome 以及用于查询的 EntityQueryHibernateEntityQuery

你得编写Home和Query组件,它们能在session、event或conversation作用范围中运行,至于选择哪个scope取决于你所希望在你的应用程序中使用的状态模型。

Seam应用框架仅在Seam管理的持久化上下文中工作。默认情况下,这些组件会寻找一个叫做 entityManager 的持久化上下文。

11.2. Home对象

Home对象对特定的实体类提供持久化操作,假设我们有个可靠的 Person 类:

@Entity
public class Person {
    @Id private Long id;
    private String firstName;
    private String lastName;
    private Country nationality;

    //getters and setters...
}

我们可以通过配置定义一个 personHome 组件:

<framework:entity-home name="personHome" entity-class="eg.Person" />

也可以通过类的扩展

@Name("personHome")
public class PersonHome extends EntityHome<Person> {}

Home对象提供了如下的操作:persist()remove()update()getInstance()。 在你能够调用 remove()update() 操作之前,你必须首先使用 setId() 方法定义你感兴趣的对象的标识符。

我们可以直接从一个JSF页面使用一个Home,如下例:

<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{personHome.instance.firstName}"/></div>
    <div>Last name: <h:inputText value="#{personHome.instance.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>

通常,只用person 指明person漂亮得多,所以在 components.xml 中添加一行语句来实现。

<factory name="person"
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome"
                       entity-class="eg.Person" />

(如果我们使用配置的方法。) 或者,我们可以通过向 PersonHome 中添加一个 @Factory 方法来实现:

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @Factory("person")
    public Person initPerson() { return getInstance(); }

}

(如果我们使用类扩展的方法) 这个修改使我们的JSF页面简化如下:

<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>

好,这就可以用来创建新的 Person 实体了。是的,这就是所需的全部代码!现在,如果我们想显示,更新,删除数据库中已经存在的 Person 实体,我们需要将实体标识符传递给 PersonHome。页面参数是一种非常好的实现方式:

<pages>
    <page view-id="/editPerson.jsp">
        <param name="personId" value="#{personHome.id}"/>
    </page>
</pages>

现在,我们可以向JSF页面中增加其他的操作:

<h1>
    <h:outputText rendered="#{!personHome.managed}" value="Create Person"/>
    <h:outputText rendered="#{personHome.managed}" value="Edit Person"/>
</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}" rendered="#{!personHome.managed}"/>
        <h:commandButton value="Update Person" action="#{personHome.update}" rendered="#{personHome.managed}"/>
        <h:commandButton value="Delete Person" action="#{personHome.remove}" rendered="#{personHome.managed}"/>
    </div>
</h:form>

当我们没有带任何请求参数链接到该页面时,会显示"Create Person"页面,当我们为 personId 这个请求参数设定一个值时,会显示“Edit Person”页面。

假设我们需要创建一些 Person 实体,并且初始化这些人的国籍。我们可以通过配置很轻松地完成:

<factory name="person"
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome"
                       entity-class="eg.Person"
                       new-instance="#{newPerson}"/>

<component name="newPerson"
           class="eg.Person">
    <property name="nationality">#{country}</property>
</component>

也可以通过扩展类

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @In Country country;

    @Factory("person")
    public Person initPerson() { return getInstance(); }

    protected Person createInstance() {
        return new Person(country);
    }

}

当然,Country 是一个被其它的Home对象管理的对象,比如说,CountryHome

为了增加更多复杂的操作(联合管理等等),我们可以向 PersonHome 中添加方法。

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @In Country country;

    @Factory("person")
    public Person initPerson() { return getInstance(); }

    protected Person createInstance() {
        return new Person(country);
    }

    public void migrate()
    {
        getInstance().setCountry(country);
        update();
    }

}

当事务成功之后(调用 persist()update()remove() 成功后),Home对象会发出一个 org.jboss.seam.afterTransactionSuccess 事件。 通过监听这一事件,我们可以在底层实体改变后,刷新查询。 如果我们只需要在特定的实体保存、修改或删除后刷新特定查询,我们可以监视 org.jboss.seam.afterTransactionSuccess.<name> 事件(<name> 是实体的名字)。

当一个操作成功时,Home对象可以自动地显示Faces信息,我们可以再一次通过配置来定制信息。

<factory name="person"
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome"
                       entity-class="eg.Person"
                       new-instance="#{newPerson}">
    <framework:created-message>New person #{person.firstName} #{person.lastName} created</framework:created-message>
    <framework:deleted-message>Person #{person.firstName} #{person.lastName} deleted</framework:deleted-message>
    <framework:updated-message>Person #{person.firstName} #{person.lastName} updated</framework:updated-message>
</framework:entity-home>

<component name="newPerson"
           class="eg.Person">
    <property name="nationality">#{country}</property>
</component>

或者扩展:

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @In Country country;

    @Factory("person")
    public Person initPerson() { return getInstance(); }

    protected Person createInstance() {
        return new Person(country);
    }

    protected String getCreatedMessage() { return "New person #{person.firstName} #{person.lastName} created"; }
    protected String getUpdatedMessage() { return "Person #{person.firstName} #{person.lastName} updated"; }
    protected String getDeletedMessage() { return "Person #{person.firstName} #{person.lastName} deleted"; }

}

但是指定信息最好的方法是把信息置于Seam所知的resource bundle中(在默认情况下,这个bundle叫做 messages )。

Person_created=New person #{person.firstName} #{person.lastName} created
Person_deleted=Person #{person.firstName} #{person.lastName} deleted
Person_updated=Person #{person.firstName} #{person.lastName} updated

这样方便进行国际化,从表现层的角度考虑也保持了代码和配置的整洁。

最后一步是使用 <s:validateAll><s:decorate> 向页面中添加验证功能,我会把这个留给你们自己去实现。

11.3. Query对象

如果我们需要数据库中所有 Person 实例的列表,我们可以使用Query对象,例如:

<framework:entity-query name="people"
                        ejbql="select p from Person p"/>

我们可以从一个JSF页面中使用它:

<h1>List of people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

我们可能需要支持分页:

<framework:entity-query name="people"
                        ejbql="select p from Person p"
                        order="lastName"
                        max-results="20"/>

我们可以使用page参数来决定被显示的页面

<pages>
    <page view-id="/searchPerson.jsp">
        <param name="firstResult" value="#{people.firstResult}"/>
    </page>
</pages>

用于分页的JSF代码可能有点冗长,但仍然是便于管理的:

<h1>Search for people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="First Page">
    <f:param name="firstResult" value="0"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="Previous Page">
    <f:param name="firstResult" value="#{people.previousFirstResult}"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Next Page">
    <f:param name="firstResult" value="#{people.nextFirstResult}"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Last Page">
    <f:param name="firstResult" value="#{people.lastFirstResult}"/>
</s:link>

真实的搜索界面能够通过让用户输入一系列的可选的搜索标准来缩小返回的结果列表。Query对象通过让你指定可选的“约束”来支持这个重要的用例。

<component name="examplePerson" class="Person"/>

<framework:entity-query name="people"
                        ejbql="select p from Person p"
                        order="lastName"
                        max-results="20">
    <framework:restrictions>
        <value>lower(firstName) like lower( concat(#{examplePerson.firstName},'%') )</value>
        <value>lower(lastName) like lower( concat(#{examplePerson.lastName},'%') )</value>
    </framework:restrictions>
</framework:entity-query>

注意“example”对象的使用。

<h1>Search for people</h1>
<h:form>
    <div>First name: <h:inputText value="#{examplePerson.firstName}"/></div>
    <div>Last name: <h:inputText value="#{examplePerson.lastName}"/></div>
    <div><h:commandButton value="Search" action="/search.jsp"/></div>
</h:form>

<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

在底层实体发生改变后,可以通过监听 org.jboss.seam.afterTransactionSuccess 事件来刷新查询:

<event type="org.jboss.seam.afterTransactionSuccess">
    <action execute="#{people.refresh}" />
</event>

或者,在发生持久化、更新或者删除时,通过 PersonHome 来刷新查询:

<event type="org.jboss.seam.afterTransactionSuccess.Person">
    <action execute="#{people.refresh}" />
</event>

这个部分所有的例子都是通过配置来体现重用的,但是,对Query对象通过扩展来进行重用也是可行的。

11.4. Controller对象

Controller 类以及它的子类 EntityControllerHibernateEntityControllerBusinessProcessController 是Seam Application Framework的可选部分。 这些类只是提供了一些访问常用内置组件及这些组件方法的便利手段,它们能够减少一些键盘输入量,也为探索Seam内置丰富功能的初学者提供了一个非常好的跳板。

例如,这就是Seam注册实例中的 RegisterAction

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

   @In private User user;

   public String register()
   {
      List existing = createQuery("select u.username from User u where u.username=:username")
         .setParameter("username", user.getUsername())
         .getResultList();

      if ( existing.size()==0 )
      {
         persist(user);
         info("Registered new user #{user.username}");
         return "/registered.jspx";
      }
      else
      {
         addFacesMessage("User #{user.username} already exists");
         return null;
      }
   }

}

正如你所看到的一样,这不是什么惊世骇俗的提高...

Chapter 12. Seam和JBoss规则

Seam简化了在Seam组件或jBPM过程定义中对JBoss Rules(Drools)规则库的调用。

12.1. 安装规则

第一步是使一个 org.drools.RuleBase 的实例在Seam的上下文变量中可用。 基于测试目的,Seam提供了一个内置组件用来编译来自class搜索路径的一组静态规则。你可以通过 components.xml 文件安装此组件:

<drools:rule-base name="policyPricingRules">
    <drools:rule-files>
        <value>policyPricingRules</value>
    </drools:rule-files>
</drools:rule-base>

这个组件编译来自一组 .drl 文件中的规则并在Seam的 APPLICATION 上下文 中缓存一个 org.drools.RuleBase 实例。需要注意的是这和在规则驱动的应用程序中需要安装多个规则库很相似。

如果你想要使用Drools DSL,你还需要指定DSL定义:

<drools:rule-base name="policyPricingRules" dsl-file="policyPricing.dsl">
    <drools:rule-files>
        <value>policyPricingRules</value>
    </drools:rule-files>
</drools:rule-base>

在大多数规则驱动的应用程序中,规则需要是可被动态部署的,所以一个生产环境应用程序会需要用Drools规则代理来管理规则库。 规则代理可以连接一个Drools规则服务器(BRMS)或者热部署来自本地文件仓库的规则包。规则代理管理的规则库也在 components.xml 中配置:

<drools:rule-agent name="insuranceRules"
                    configurationFile="/WEB-INF/deployedrules.properties" />

属性文件包含有规则代理RulesAgent所特有的属性。这里是一个来自Drools范例发型中的配置文件例子。

newInstance=true
url=http://localhost:8080/drools-jbrms/org.drools.brms.JBRMS/package/org.acme.insurance/fmeyer
localCacheDir=/Users/fernandomeyer/projects/jbossrules/drools-examples/drools-examples-brms/cache
poll=30
name=insuranceconfig

绕过配置文件,直接配置组件的选项也是可行的。

<drools:rule-agent name="insuranceRules"
                   url="http://localhost:8080/drools-jbrms/org.drools.brms.JBRMS/package/org.acme.insurance/fmeyer"
                   local-cache-dir="/Users/fernandomeyer/projects/jbossrules/drools-examples/drools-examples-brms/cache"
                   poll="30"
                   configuration-name="insuranceconfig" />

接下来,我们需要使 org.drools.WorkingMemory 实例对每个对话都可用。 (每个 WorkingMemory 累积与当前对话相关的fact。)

<drools:managed-working-memory name="policyPricingWorkingMemory" auto-create="true" rule-base="#{policyPricingRules}"/>

请注意,我们通过 ruleBase 配置属性给了 policyPricingWorkingMemory 一个指回规则库的引用。

12.2. 在Seam组件中使用规则

现在可以将我们的 WorkingMemory 注入进任意的Seam组件中了,进行判断并执行规则:

@In WorkingMemory policyPricingWorkingMemory;

@In Policy policy;
@In Customer customer;

public void pricePolicy() throws FactException
{
    policyPricingWorkingMemory.assertObject(policy);
    policyPricingWorkingMemory.assertObject(customer);
    policyPricingWorkingMemory.fireAllRules();
}

12.3. 在jBPM流程定义中使用规则

你甚至可以用一个规则库来充当jBPM动作处理器、决定处理器或者分配器—无论是在页面流或者业务流程定义中。

<decision name="approval">

    <handler class="org.jboss.seam.drools.DroolsDecisionHandler">
        <workingMemoryName>orderApprovalRulesWorkingMemory</workingMemoryName>
        <assertObjects>
            <element>#{customer}</element>
            <element>#{order}</element>
            <element>#{order.lineItems}</element>
        </assertObjects>
    </handler>

    <transition name="approved" to="ship">
        <action class="org.jboss.seam.drools.DroolsActionHandler">
            <workingMemoryName>shippingRulesWorkingMemory</workingMemoryName>
            <assertObjects>
                <element>#{customer}</element>
                <element>#{order}</element>
                <element>#{order.lineItems}</element>
            </assertObjects>
        </action>
    </transition>

    <transition name="rejected" to="cancelled"/>

</decision>

<assertObjects> 元素指定了用来返回要被作为fact设给 WorkingMemory 的对象或者对象集合的EL表达式。

除此之外,Seam还支持在jBPM任务分配中使用Drools:

<task-node name="review">
    <task name="review" description="Review Order">
        <assignment handler="org.jboss.seam.drools.DroolsAssignmentHandler">
            <workingMemoryName>orderApprovalRulesWorkingMemory</workingMemoryName>
            <assertObjects>
                <element>#{actor}</element>
                <element>#{customer}</element>
                <element>#{order}</element>
                <element>#{order.lineItems}</element>
            </assertObjects>
        </assignment>
    </task>
    <transition name="rejected" to="cancelled"/>
    <transition name="approved" to="approved"/>
</task-node>

这些对象作为Drools global提供给规则,即jBPM Assignable 对象(assignable)和Seam Decision对象(decision)。 处理决定的规则应该调用 decision.setOutcome("result") 来决定结果。执行分配的规则要调用 Assignable 设置参与者id。

package org.jboss.seam.examples.shop

import org.jboss.seam.drools.Decision

global Decision decision

rule "Approve Order For Loyal Customer"
  when
    Customer( loyaltyStatus == "GOLD" )
    Order( totalAmount <= 10000 )
  then
    decision.setOutcome("approved");
end
package org.jboss.seam.examples.shop

import org.jbpm.taskmgmt.exe.Assignable

global Assignable assignable

rule "Assign Review For Small Order"
  when
    Order( totalAmount <= 100 )
  then
    assignable.setPooledActors( new String[] {"reviewers"} );
end

Chapter 13. 安全

Seam Security API是个可选的Seam特性,它为保护您的Seam项目中的领域和页面资源提供验证和授权特性。

13.1. 概述

Seam Security提供两种不同的操作模式:

  • 简化模式 - 这个模式支持验证服务和简单的基于角色的安全性检查。

  • 高级模式 - 这个模式支持简化模式的所有特性,还利用JBoss Rules提供基于规则的安全性检查。

13.1.1. 哪种模式更适合我的应用程序呢?

这完全取决于应用程序的需求。 如果你的安全性需求不高,例如,如果只希望对登录的用户或者属于某个特定角色的用户限制某些页面和动作,那么简化模式可能就足够了。 这个模式的好处在于它是一种更简化的配置,要包括的库明显更少,占用的内存空间(memory footprint)也更小。

另一方面,如果应用程序需要根据上下文或者复杂的业务规则进行安全性检查,那就需要高级模式提供的特性了。

13.2. 需求

如果使用Seam Security的高级模式特性,下列jar文件就要配置成 application.xml 中的模块。 如果你正在简化模式下使用Seam Security,则 需要这些了:

  • drools-compiler-4.0.0.MR2.jar

  • drools-core-4.0.0.MR2.jar

  • janino-2.5.7.jar

  • antlr-runtime-3.0.jar

  • mvel14-1.2beta16.jar

对于基于Web的安全性来说,jboss-seam-ui.jar 也必须包括在应用程序的war文件中。

13.3. 取消安全

在某些场景下,可能需要取消Seam Security,例如在单元测试的过程中。 可以通过调用静态方法 Identity.setSecurityEnabled(false) 来取消安全检查。 这样一来,就可以阻止执行如下的任何安全检查:

  • Entity Security

  • Hibernate Security Interceptor

  • Seam Security Interceptor

  • Page restrictions

13.4. 验证

Seam Security提供的验证特性建立在JAAS(Java Authentication和Authorization Service)之上,给处理用户验证提供稳健的、非常容易配置的API。 然而,对于并不复杂的验证需求,Seam提供了一种更加简化的验证方法,隐藏了JAAS的复杂性。

13.4.1. 配置

简化的验证方法使用一种内置的JAAS登录模块:SeamLoginModule,把验证委托给你项目中的一个Seam组件。 这个登录模块已经在Seam内部配置为默认的应用策略的一部分,因此不需要任何额外的配置文件。它允许你利用你自己的应用程序提供的实体类编写验证方法。 配置这个简化的验证形式需要在 components.xml 中配置 identity 组件。

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core"
            xmlns:security="http://jboss.com/products/seam/security"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation=
                "http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.0.xsd
                 http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.0.xsd">

    <security:identity authenticate-method="#{authenticator.authenticate}"/>

</components>

如果你希望使用高级的安全性特性,如基于规则的许可检查,你所要做的就是把Drools(JBoss Rules)jars包含在你的classpath中,并添加一些额外的配置,后面会讲到。

EL表达式 #{authenticator.authenticate} 是一种方法绑定,表示 authenticator 组件的 authenticate 方法将用来验证该用户。

13.4.2. 编写验证方法

components.xml 中给 identity 指定的 authenticate-method 属性,规定了哪种方法将被SeamLoginModule用来验证用户。 这个方法没有参数,并将要返回一个布尔值,表示验证是否成功。用户的用户名和密码可以分别地通过 Identity.instance().getUsername()Identity.instance().getPassword() 获取,用户所属的任何角色都应该利用 Identity.instance().addRole() 分配。 下面是JavaBean组件内部一个验证方法的完整实例:

@Name("authenticator")
public class Authenticator {
   @In EntityManager entityManager;

   public boolean authenticate() {
      try
      {
         User user = (User) entityManager.createQuery(
            "from User where username = :username and password = :password")
            .setParameter("username", Identity.instance().getUsername())
            .setParameter("password", Identity.instance().getPassword())
            .getSingleResult();

         if (user.getRoles() != null)
         {
            for (UserRole mr : user.getRoles())
               Identity.instance().addRole(mr.getName());
         }

         return true;
      }
      catch (NoResultException ex)
      {
         return false;
      }

   }

}

在上面的例子中,UserUserRole 都是应用程序指定的实体Bean。 roles 参数以用户所属的角色填充,它应该添加到 Set 作为文字型字符串值,如“admin”、“user”。 在这个例子中,如果没有找到用户记录,并抛出一个 NoResultException,验证方法就返回false,表示验证失败。

13.4.2.1. Identity.addRole()

Identity.addRole() 方法表现的不同取决于当前会话是否已经验证过了。 如果会话没有验证过,那么 addRole()只能 在验证的过程中被调用。 当被调用到这里时,角色名称被放进一个预验证角色的临时列表中。 一当验证成功,预验证角色就变成“真实的”角色了,并且调用 Identity.hasRole() 就可以返回true了。 下面的序列图展示了预验证角色列表作为第一类对象,以更清楚地表明它如何适应在整个验证过程。

13.4.3. 编写登录表单

Identity 组件提供 usernamepassword 属性,适合最常见的验证场景。 这些属性可以直接绑定到登录表单上的用户名和密码字段。一旦设置了这些属性,调用 identity.login() 方法就可以验证使用所提供的证书的用户。 下面是一个简单的登录表单的例子:

<div>
    <h:outputLabel for="name" value="Username"/>
    <h:inputText id="name" value="#{identity.username}"/>
</div>

<div>
    <h:outputLabel for="password" value="Password"/>
    <h:inputSecret id="password" value="#{identity.password}"/>
</div>

<div>
    <h:commandButton value="Login" action="#{identity.login}"/>
</div>

类似地,注销用户通过调用 #{identity.logout} 来完成。调用这个动作将清除当前被验证用户的安全性状态。

13.4.4. 简化配置 - 概述

因此归结起来,配置验证有三个简单的步骤:

  • components.xml 中配置一种验证方法。

  • 编写一种验证方法。

  • 编写一个登录表单,以便用户可以验证。

13.4.5. 处理安全异常

为了防止用户随安全错误而收到默认的错误页面,建议 pages.xml 配置为把安全错误重定向到一个更“漂亮”的页面。由Security API抛出的两种主要的异常类型是:

  • NotLoggedInException - 如果用户试图在没有登录的情况下访问一个受限的动作或者页面,就会抛出这个异常。

  • AuthorizationException - 只有当用户已经登录,并且试图访问他们没有必要权限的受限动作或者页面时,才会抛出这个异常。

NotLoggedInException 抛出的情况下,建议把用户限制到一个登录页面或者注册页面,以便登录。 对于 AuthorizationException,把用户重定向到一个错误页面可能更合适。 以下是 pages.xml 文件的一个例子,它把这两种安全异常都处理了:

<pages>

    ...

    <exception class="org.jboss.seam.security.NotLoggedInException">
        <redirect view-id="/login.xhtml">
            <message>You must be logged in to perform this action 你必须登录执行这个动作</message>
        </redirect>
    </exception>

    <exception class="org.jboss.seam.security.AuthorizationException">
        <end-conversation/>
        <redirect view-id="/security_error.xhtml">
            <message>You do not have the necessary security privileges to perform this action.你没有执行这个动作的必要权限。</message>
        </redirect>
    </exception>

</pages>

大多数Web应用程序需要更复杂的登录重定向处理,因此Seam包含了处理这个问题的一些特殊功能。

13.4.6. 登录重定向

当未被验证的用户试图访问某个特定的视图(或者通配符id)时,可以让Seam把用户重定向到一个登录页面:

<pages login-view-id="/login.xhtml">

    <page view-id="/members/*" login-required="true"/>

    ...

</pages>

(这个不像上述的异常处理器那么简洁,但是可能要结合起来使用。)

用户登录以后,我们要自动返回到原来的地方(网址),以便可以重试要求登录之后才能进行的操作。 如果把下列事件监听器添加到 components.xml,如果用户在没有登录的情况下去访问受限的页面,那么页面信息就会被记录下来。 用户登录之后,就会被重定向到刚才的受限页面,并且把原来的请求参数也一起传过去。

<event type="org.jboss.seam.notLoggedIn">
    <action execute="#{redirect.captureCurrentView}"/>
</event>

<event type="org.jboss.seam.postAuthenticate">
    <action execute="#{redirect.returnToCapturedView}"/>
</event>

注意登录重定向被实现为一种对话范围的机制,因此不要在 authenticate() 方法中终止对话。

13.4.7. HTTP验证

虽然不建议使用,除非绝对必要,Seam提供HTTP Basic或HTTP Digest(RFC 2617 )方法的验证。 为使用表单的验证方式,authentication-filter 组件必须能够在components.xml中激活:

  <web:authentication-filter url-pattern="*.seam" auth-type="basic"/>
      

为了激活Basic验证的过滤器,设置 auth-typebasic 或者对于Digest验证 ,设置 auth-typedigest。 如果是使用Digest验证,keyrealm 也必须进行设置:

  <web:authentication-filter url-pattern="*.seam" auth-type="digest" key="AA3JK34aSDlkj" realm="My App"/>
      

key 可以是任何字符串值。 realm 就是进行用户验证时所用的验证Realm的名称。

13.4.7.1. 编写Digest验证者

如果是使用Digest认证,你的鉴定者类需要继承抽象类 org.jboss.seam.security.digest.digestauthenticator ,并使用 validatePassword() 方法,以根据Digest的要求来验证用户的纯文本密码。下面是一个例子:

   public boolean authenticate()
   {
      try
      {
         User user = (User) entityManager.createQuery(
            "from User where username = :username")
            .setParameter("username", identity.getUsername())
            .getSingleResult();

         return validatePassword(user.getPassword());
      }
      catch (NoResultException ex)
      {
         return false;
      }
   }
        

13.4.8. 高级验证特性

本节探讨Security API提供的部分高级特性,用来满足更复杂的安全需求。

13.4.8.1. 使用容器的JAAS配置

如果你宁可不用Seam Security API提供的简化的JAAS配置,可以委托给默认的系统JAAS配置,该配置是在 components.xml 中提供的一个 jaasConfigName。 例如,如果你正在使用JBoss AS,并希望使用 其它 策略(使用JBoss AS提供的 UsersRolesLoginModule 登录模块),那么 components.xml 中的项看起来就像这样:

<security:identity authenticate-method="#{authenticator.authenticate}"
                      jaas-config-name="other"/>

13.5. 错误消息

Security API给各种安全相关的事件生成许多默认的展现消息。下表列出了可以用来覆盖这些消息的Key,它们在 message.properties 资源文件中指定。为了禁止消息,只要在资源文件里给相应的Key赋予空值就行了。

Table 13.1. 安全消息Key

org.jboss.seam.loginSuccessful

这个消息在用户通过Security API成功登录时生成。

org.jboss.seam.loginFailed

这个消息在登录过程失败时生成。失败是由于用户提供了错误的用户名或者密码,或者由于其他问题造成的。

org.jboss.seam.NotLoggedIn

这个消息在用户试图执行一个动作,或者访问一个需要安全检查的页面,并且用户目前未被验证时生成。

13.6. 授权

Seam Security API提供了大量授权特性,用来保护对组件、组件方法和页面的访问。本节阐述这每一种授权特性。 要注意的一件重要的事是,如果你希望使用任何高级特性(如基于规则的许可),那么你的 components.xml 文件就必须配置成支持该特性 - 请见前面的配置小节。

13.6.1. 核心概念

Seam Security API提供的每种授权机制,都构建在用户被授与了角色和/或许可的概念之上。 角色是用户的一个 群组,或者一种 类型,该用户可能已经被授与某种特权,用来在应用程序内部执行一个或者更多的特定操作。 许可是执行单个指定动作的一种(有时是一次性的)特权。 只用许可而不用其它任何东西来构建应用程序是完全可能的,然而角色在授与用户群组特权时更灵活方便。

角色很简单,只由一个名称组成,如“admin”、“user”、“customer”等等。许可由一个名称和一个动作组成,在这个文档中以 name:action 的形式表示,例如 customer:delete,或者 customer:insert

13.6.2. 保护组件

我们从检验授权的最简单的形式开始:组件安全,从 @Restrict 注解开始。

13.6.2.1. @Restrict注解

Seam组件可以利用 @Restrict 注解在方法或者类级上得到保护。 如果方法和声明它的类都通过 @Restrict 注解,方法限制将优先(类限制是不起作用)。 如果方法调用在安全检查中失败,那么根据对 Identity.checkRestriction() 的约定就会抛出一个异常(请见行内限制)。 组件类自身上的 @Restrict 相当于把 @Restrict 添加到了它的每一个方法上。

空的 @Restrict 意味着 componentName:methodName 的一个许可检查。以下面的组件方法为例:

@Name("account")
public class AccountAction {
    @Restrict public void delete() {
      ...
    }
}

在这个例子中,调用 delete() 方法需要的隐含许可是 account:delete。 还有一种方式,就是写 @Restrict("#{s:hasPermission('account','delete',null)}")。 现在来看另一个例子:

@Restrict @Name("account")
public class AccountAction {
    public void insert() {
      ...
    }
    @Restrict("#{s:hasRole('admin')}")
    public void delete() {
      ...
    }
}

这一次,组件类本身通过 @Restrict 注解。这意味着任何没有覆盖 @Restrict 注解的方法都需要一个隐式的许可检查。在这个例子中,insert() 方法需要 account:insert 的一个许可,而 delete() 方法则要求用户必须是 admin 角色的一员。

在进一步探讨之前,先看看在前面的例子中见过的 #{s:hasRole()} 表达式。 s:hasRoles:hasPermission 都是EL方法,它们委托给 Identity 类相应的具名方法。 这些函数可以在所有Security API的任何EL表达式中使用。

作为一个EL表达式,@Restrict 注解的值可以引用Seam上下文中存在的任何对象。 这在给特定的对象实例执行许可检查时极为有用。看看这个例子:

@Name("account")
public class AccountAction {
    @In Account selectedAccount;
    @Restrict("#{s:hasPermission('account','modify',selectedAccount)}")
    public void modify() {
        selectedAccount.modify();
    }
}

这个例子中最值得关注的东西是,对在 hasPermission() 函数调用中见到的 selectedAccount 的引用。 这个变量的值将从Seam上下文内部查找,并传递到 Identity 中的 hasPermission() 方法, 在这个例子中,它随后可以确定用户是否具有修改指定 Account 对象所需的许可。

13.6.2.2. 行内限制

有时候,可能希望在代码中执行安全检查,而不用 @Restrict 注解。在这种情况下,只要用 Identity.checkRestriction() 来计算安全表达式,像这样:

public void deleteCustomer() {
    Identity.instance().checkRestriction("#{s:hasPermission('customer','delete',selectedCustomer)}");
}

如果指定的表达式没有取值为 true,或者

  • 如果用户没有登录,就抛出 NotLoggedInException 异常,或者

  • 如果用户登录了,就抛出 AuthorizationException 异常。

直接在Java代码中调用 hasRole()hasPermission() 方法也是可能的:

if (!Identity.instance().hasRole("admin"))
     throw new AuthorizationException("Must be admin to perform this action");

if (!Identity.instance().hasPermission("customer", "create", null))
     throw new AuthorizationException("You may not create new customers");

13.6.3. 用户界面中的安全

设计优良的用户界面的一种表现是,不会向用户展现他们没有必要的权限进行使用的选项。Seam Security允许条件性的渲染: 1)一个页面的几个部分,或者2)独立的控制,根据用户的权限,使用与用给组件安全的完全相同的EL表达式。

我们来看看界面安全的一些例子。首先,我们假设有一个登录表单,它应该只在用户还没有登录的情况下才被渲染。 我们可以利用 identity.isLoggedIn() 属性像这样编写:

<h:form class="loginForm" rendered="#{not identity.loggedIn}">

如果用户没有登录,登录表单就渲染 - 目前为止这还是非常直白易懂。 现在假设页面上有一个菜单,包含一些应该只对 manager 角色中的用户可用的操作。下面是一种编写方式:

<h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('manager')}">
    Manager Reports
</h:outputLink>

这也很容易理解。如果用户不是 manager 角色的一员,那么outputLink就不渲染。 rendered 属性本身一般来说可以用在控制,或者周围的 <s:div> 或者 <s:span> 控制上。

现在探讨一些更复杂的东西。我们假设你在页面上有一个 h:dataTable 控制,该页面罗列了可能希望或者不希望根据用户的权限,对其渲染动作链接的记录。 s:hasPermission EL函数允许我们传进一个对象参数,它可以用来确定用户是否具有对该对象的必要许可。 下面展现了带有受保护的链接的dataTable可能是什么样子:

<h:dataTable value="#{clients}" var="cl">
    <h:column>
        <f:facet name="header">Name</f:facet>
        #{cl.name}
    </h:column>
    <h:column>
        <f:facet name="header">City</f:facet>
        #{cl.city}
    </h:column>
    <h:column>
        <f:facet name="header">Action</f:facet>
        <s:link value="Modify Client" action="#{clientAction.modify}"
                rendered="#{s:hasPermission('client','modify',cl)"/>
        <s:link value="Delete Client" action="#{clientAction.delete}"
                rendered="#{s:hasPermission('client','delete',cl)"/>
    </h:column>
</h:dataTable>

13.6.4. 保护页面

页面安全要求应用程序使用 pages.xml 文件,但是配置极为简单。 只要在你希望保护的 page 元素内部包括一个 <restrict/> 元素。 如果没有通过 restrict 元素指定明确的限制,当网页通过non-faces(GET)的请求被访问时, /viewId.xhtml:render 隐含的许可将被检查, 并且当网页上任何的JSF postback(表单提交)都需要 /viewId.xhtml:restore 许可。 否则,特定的约束将作为一个标准的安全表达式进行取值。下面有几个例子:

<page view-id="/settings.xhtml">
    <restrict/>
</page>

本页面已经隐含了 /settings.xhtml:render 所需的non-faces请求的许可, 并隐含了 /settings.xhtml:restore 所需的faces请求的许可。

<page view-id="/reports.xhtml">
    <restrict>#{s:hasRole('admin')}</restrict>
</page>

这个页面的faces和non-faces请求,都需要用户是 admin 角色的一个成员。

13.6.5. 保护实体

Seam Security也使得对实体的读取、插入、更新和删除动作应用安全限制成为可能。

为了保护一个实体类的所有动作,在类自身上添加一个 @Restrict 注解:

@Entity
@Name("customer")
@Restrict
public class Customer {
  ...
}

如果没有在 @Restrict 注解中指定任何表达式,执行的默认安全检查就是 entityName:action 的许可检查, 在这里,entityName 是实体的Seam组件名(或者如果指定了@Name,则是完全匹配的类名),并且 action 可以是 readinsertupdate 或者 delete

也可能通过把@Restrict注解放在相关的实体生命周期的方法上(被注解如下),而只限制某些动作:

  • @PostLoad - 在实体实例从数据库中加载之后调用。用这个方法配置一个 read 许可。

  • @PrePersist - 在插入实体的一个新实例之前调用。用这个方法配置一个 insert 许可。

  • @PreUpdate - 在实体更新之前调用。用这个方法配置一个 update 许可。

  • @PreRemove - 在实体删除之前调用。用这个方法配置一个delete许可。

这里有一个例子,说明实体方法如何配置成给任何 insert 操作执行安全检查。 请注意不需要方法去做任何事情,有关安全的唯一重要的东西是它如何被注解:

  @PrePersist @Restrict
  public void prePersist() {}
   

这里还有一个例子,说明实体许可规则检查被验证的用户是否被允许(从seamspace例子中)插入新的 MemberBlog 记录。 正在进行安全检查的实体,被自动断言到工作内存中(在这个例子中是 MemberBlog):

rule InsertMemberBlog
  no-loop
  activation-group "permissions"
when
  check: PermissionCheck(name == "memberBlog", action == "insert", granted == false)
  Principal(principalName : name)
  MemberBlog(member : member -> (member.getUsername().equals(principalName)))
then
  check.grant();
end;

这个规则将授与许可 memberBlog:insert,如果当前被验证的用户(由 Principal fact表明)有着与正在为其创建Blog项的一样的成员。 可以在 Principal fact(和其它地方)中见到的“name : name”结构是一个变量绑定 - 它把 Principalname 属性绑定到一个具名 name 的变量上。 变量绑定允许值在其它地方被引用,例如下面的行把成员的用户名与 Principal 名称进行比较。 想了解更多细节,请参考JBoss Rules文档。

最后,需要安装一个监听器类,把Seam Security与你的JPA提供者整合起来。

13.6.5.1. 使用JPA的实体安全

对EJB3实体Bean的安全检查通过一个 EntityListener 执行来完成的。 你可以利用下列 META-INF/orm.xml 文件安装这个监听器:

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
                 version="1.0">

    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="org.jboss.seam.security.EntitySecurityListener"/>
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>

</entity-mappings>

13.6.5.2. 使用Hibernate的实体安全

如果你正在使用一个经由Seam配置的Hibernate SessionFactory,使用实体安全就不需要做任何特别的事情。

13.7. 编写安全规则

迄今为止,我们已经提到了许多许可,但是没有提及许可事实上如何定义或者授与。 本节将对此进行阐述,解释许可检查如何进行,以及如何给一个Seam应用程序实现许可检查。

13.7.1. 许可概述

Security API如何知道用户是否具有对一个特定客户的 customer:modify 许可? Seam Security提供一种新奇的方法,根据JBoss Rules确定用户许可。 使用规则引擎的两个好处在于:1)它是每个用户许可背后的业务逻辑的一个集中位置, 2)速度 - JBoss Rules使用非常有效的算法,用来给涉及多个条件的大量复杂规则取值。

13.7.2. 配置规则文件

Seam Security希望找到一个称作 securityRulesRuleBase 组件,Seam Security用它来给许可检查取值。 这在 components.xml 中配置如下:

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core"
            xmlns:security="http://jboss.com/products/seam/security"
            xmlns:drools="http://jboss.com/products/seam/drools"
            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
                 http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.0.xsd"
                 http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.0.xsd">

   <drools:rule-base name="securityRules">
       <drools:rule-files>
           <value>/META-INF/security.drl</value>
       </drools:rule-files>
   </drools:rule-base>

</components>

一旦配置了 RuleBase 组件,就可以编写安全规则了。

13.7.3. 创建安全规则文件

对于这个步骤,要在应用程序的jar文件的 /META-INF 目录中创建一个名为 security.drl 的文件。 事实上,这个文件可以命名为你喜欢的任何名字,并存在于任何位置,只要它在 components.xml 中进行了适当的配置。

那么安全规则文件应该包含什么呢?目前,至少浏览一下JBoss Rules文档可能是个好主意,但是这里介绍的是一个极为简单的例子:

package MyApplicationPermissions;

import org.jboss.seam.security.PermissionCheck;
import org.jboss.seam.security.Role;

rule CanUserDeleteCustomers
when
  c: PermissionCheck(name == "customer", action == "delete")
  Role(name == "admin")
then
  c.grant();
end;

我们来分解一下。我们见到的第一个东西是包声明。包在JBoss Rules中基本上是一个规则的集合。 包名可以是你喜欢的任何名称 - 它不影响规则基础范围之外的任何其它东西。

然后,我们可能注意到二个对 PermissionCheckRole 类的导入语句。 这些导入通知规则引擎我们将在规则中引用这些类。

最后是规则的代码。包中的每一个规则都应该有一个唯一的名称(一般描述规则的目的)。 在这个例子中,我们的规则称作 CanUserDeleteCustomers,用来检查用户是否被允许删除一个客户记录。

看看规则定义的主体,可以发现两个独特的部分。规则有称为LHS(左手边)和RHS(右手边)的东西。 LHS由规则的条件部分组成,如必须满足规则以便触发的一系列条件。 LHS由 when 部分表示。RHS是结果,或者是规则的动作部分,该规则只在满足LHS的所有条件时才触发。 RHS由 then 部分组成。规则的末端用 end; 线表示。

如果看看规则的LHS,就可见到那里列出了两个条件。我们先来检验第一个条件:

c: PermissionCheck(name == "customer", action == "delete")

说得通俗些,这个条件声明工作内存中必须存在一个 PermissionCheck 对象,它具有与"customer"相当的 name 属性,以及与"delete"相当的 action 属性。 什么是工作内存?它是个会话范围的对象,包含规则引擎进行有关许可检查决策时所需的上下文信息。 每次调用 hasPermission() 方法时,一个临时的 PermissionCheck 对象或者 Fact 就被断言到工作内存中。 这个 PermissionCheck 正好对应于正被检查的许可,因此,例如如果调用 hasPermission("account", "create", null),那么带有相当于"account"的 name 和相当于"create"的 actionPermissionCheck 对象,就将在许可检查持续期间被断言到工作内存。

工作内存中还有什么其它的东西? 除了在许可检查期间断言的短期临时fact被插入之外,工作内存中还有一些长期的对象,在用户验证的整个持续期间都保留在那。 这些包括作为验证过程一部分而创建的任何 java.security.Principal 对象, 还包括用户所属角色中的每一个角色的 org.jboss.seam.security.Role。 通过调用 ((RuleBasedIdentity) RuleBasedIdentity.instance()).getSecurityContext().insert(), 也可能断言额外的长期fact到工作内存中,把对象当作参数传递。

回到我们的简单例子上来,也会注意到我们LHS的第一行加上了 c: 的前缀。 这是个变量绑定,用来指回到符合条件的对象。移到LHS的第二行,会看到:

Role(name == "admin")

这个条件只声明工作内存中必须有 Role 对象带有"admin"的 name。 如前所述,用户角色被作为长期fact断言到工作内存中。 因此,把两个条件放在一起,这个规则基本上等于在说: “如果你检查 customer:delete 许可,并且用户是 admin 角色的一员时,我将会触发。”

那么规则触发的结果会怎样?我们来看看规则的RHS:

c.grant()

RHS由Java代码组成,在这个例子中是调用 c 对象的 grant() 方法, 如前面提到过的,它是个对 PermissionCheck 对象的变量绑定。 除了 PermissionCheck 对象的 nameaction 属性之外, 也有一个 granted 属性,它的初始值设置为 false。 在 PermissionCheck 上调用 grant() 方法, 设置 granted 属性为 true,这意味着许可检查成功了,允许用户执行许可检查预定的任何动作。

13.7.3.1. 通配符许可检查

通过在规则中给 PermissionCheck 删除 action 约束, 可能实现通配符许可检查(对一个指定的许可名称允许所有动作),像这样:

rule CanDoAnythingToCustomersIfYouAreAnAdmin
when
  c: PermissionCheck(name == "customer")
  Role(name == "admin")
then
  c.grant();
end;
        

这个规则允许带有 admin 角色的用户对任何 customer 许可检查执行任何操作。

13.8. SSL安全

Seam包括对通过HTTPS协议提供敏感的页面的基本支持。 这很容易通过在 pages.xml 中给页面指定 scheme 而配置。 下列例子说明视图 /login.xhtml 如何配置为使用HTTPS:

<page view-id="/login.xhtml" scheme="https">

这个配置自动扩展为 s:links:button JSF控制,它(在指定 view 时)还将渲染使用了正确协议的链接。在前一个例子的基础上,下列链接将使用HTTPS协议,因为 /login.xhtml 配置为使用HTTPS:

<s:link view="/login.xhtml" value="Login"/>

使用 错误 协议时,直接浏览视图将导致重定向到与使用 正确 协议一样的视图。 例如,浏览一个让 scheme="https" 使用HTTP的页面时,将重定向到与使用HTTPS一样的页面。

也可能给所有页面配置一个 默认的scheme。这事实上相当重要,就像你可能只希望给一些页面使用HTTPS一样。 如果没有指定默认的scheme,那么默认的行为就是继续使用当前的scheme。 这意味着一旦你通过HTTPS进入了一个页面,HTTPS就将持续使用,即使你导航到了另一个非HTTPS的页面(糟糕的事!)。 因此强烈建议包括一个默认的 scheme,通过在默认的 "*" 视图上配置它:

<page view-id="*" scheme="http" />

当然,如果你的应用程序中 没有 任何页面使用HTTPS,那就不需要指定默认的scheme。

你可以配置Seam来自动地在每次scheme改变时使当前的HTTP会话失效。只要在 components.xml 文件中加入这一行:

<core:servlet-session invalidate-on-scheme-change="true"/>

这个选项可以让你的系统减少被嗅探到Session ID或者因为页面从使用HTTPS转到HTTP时导致敏感数据的泄露。

13.9. 实现Captcha测试

虽然严格来说它不是Security API的一部分,但是它在某些环境下(例如新用户注册,发布到一个公共的blog或者论坛), 可以用来实现Captcha(Completely Automated Public Turing test to tell Computers and Humans Apart),以防止自动的机器人与应用程序进行交互。 Seam提供与JCaptcha的无缝整合,是产生Captcha challenge的一个极好的库。如果你希望在应用程序中使用Captcha特性,就要把来自Seam lib目录的jcaptcha-* jar文件包括在项目中,并在 application.xml 中把它注册为一个Java模块。

13.9.1. 配置Captcha Servlet

为了建立并运行起来,需要配置Seam Resource Servlet,这将给你的页面提供Captcha challenge映射。这在 web.xml 中需要下列项:

<servlet>
    <servlet-name>Seam Resource Servlet</servlet-name>
    <servlet-class>org.jboss.seam.servlet.SeamResourceServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Seam Resource Servlet</servlet-name>
    <url-pattern>/seam/resource/*</url-pattern>
</servlet-mapping>

13.9.2. 添加Captcha到页面

添加一个Captcha challenge到页面极为容易。 Seam提供一个page-scoped组件,captcha,它提供它所需要的一切,包括内置的captcha校验。这里有个例子:

<div>
    <h:graphicImage value="/seam/resource/captcha?#{captcha.id}"/>
</div>

<div>
    <h:outputLabel for="verifyCaptcha">Enter the above letters</h:outputLabel>
    <h:inputText id="verifyCaptcha" value="#{captcha.response}" required="true">
      <s:validate />
    </h:inputText>
    <div class="validationError"><h:message for="verifyCaptcha"/></div>
</div>

<div>
    <h:commandButton action="#{register.next}" value="Register"/>
</div>

这就是关于它的所有信息。graphicImage 控制显示Captcha challenge,inputText 接收用户的响应。 当表单被提交时这个响应会根据Captcha被自动地校验。

13.9.3. 定制Captcha图片

Captcha图片本身可以通过 ImageCaptchaService 和默认的(DefaultManageableImageCaptchaService)进行定制。 要配置不同的 ImageCaptchaService,在 components.xml 文件中添加以下条目:

  <component name="org.jboss.seam.captcha.captchaImage" service="#{customCaptcha.service}"/>

service 属性指明 ImageCaptchaService 实例用来生成Captcha图片。 更多关于配置一个 ImageCaptchaService 的信息,请参见JCaptcha文档。 这里有一个定制图片生成器的例子(可以在seamspace例子里找到):

@Name("customCaptcha")
public class CustomCaptcha
{
   public ImageCaptchaService getService()
   {
      BasicGimpyEngine customCaptcha = new BasicGimpyEngine();
      GimpyFactory factory = new GimpyFactory(
            new RandomWordGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789"),
            new ComposedWordToImage(new RandomFontGenerator(new Integer(15),
                  new Integer(15)), new UniColorBackgroundGenerator(new Integer(150),
                        new Integer(30)), new RandomTextPaster(new Integer(4),
                              new Integer(7), Color.BLACK)));
      GimpyFactory[] factories = {factory};
      customCaptcha.setFactories(factories);

      return new DefaultManageableImageCaptchaService(
                   new FastHashMapCaptchaStore(),
                   customCaptcha,
                   180,
                   120000,
                   75000);
   }
}

Chapter 14. 国际化和主题

Seam通过提供几个内置部件来为UI提供多语言支持,从而使构建国际化的应用程序变得十分容易。

14.1. 本地化

每一个用户登录会话都有一个相关的 java.util.Locale 实例(以名为 locale 的组件形式提供给应用程序)。 一般情况下,不需要做任何特别的配置设置locale,Seam 委托JSF来判断当前的活动locale:

  • 如果HTTP请求中有一个相关联的locale(浏览器的locale),并且这个locale在 faces-config.xml 的支持列表中,那么其他会话也使用该locale。
  • 否则,如果在 faces-config.xml 指定了默认的locale, 那么其他会话也使用该locale。
  • 否则,使用服务器的默认locale。

通过Seam的以下几个配置属性来手工设置locale也是你 可能的org.jboss.seam.international.localeSelector.languageorg.jboss.seam.international.localeSelector.countryorg.jboss.seam.international.localeSelector.variant,但是并不推荐这种做法。

然而,允许用户通过应用程序的用户界面来手工设置locale也是很有益处的。Seam提供了内置的功能来覆盖通过上述算法决定的locale。你所要做的只是 在JSP或Facelet的Form中增加以下代码段:

<h:selectOneMenu value="#{localeSelector.language}">
    <f:selectItem itemLabel="English" itemValue="en"/>
    <f:selectItem itemLabel="Deutsch" itemValue="de"/>
    <f:selectItem itemLabel="Francais" itemValue="fr"/>
</h:selectOneMenu>
<h:commandButton action="#{localeSelector.select}" value="#{messages['ChangeLanguage']}"/>

或者,如果你想要一个 faces-config.xml 支持的所有locale的列表,就用:

<h:selectOneMenu value="#{localeSelector.localeString}">
    <f:selectItems value="#{localeSelector.supportedLocales}"/>
</h:selectOneMenu>
<h:commandButton action="#{localeSelector.select}" value="#{messages['ChangeLanguage']}"/>

当在下拉列表中选择一项,并按下按钮后,随后会话中Seam和JSF的locale就被刷新了。

14.2. 标签

JSF 通过使用 <f:loadBundle /> 来支持用户界面标签和描述文本的国际化。这个方法同样可以用在Seam应用程序中。 或者,可以利用Seam的 messages 组件用内嵌的EL表达式来显示模板标签。

14.2.1. 定义标签

Seam提供了一个 java.util.ResourceBundle (以org.jboss.seam.core.resourceBundle 的名字提供给应用程序)。 你需要通过这个指定的资源包来使你的国际化标签可用。默认情况下,Seam 使用名为messages的资源包, 你需要在 messages.propertiesmessages_en.propertiesmessages_en_AU.properties 等文件中定义你的标签。这些文件通常在 WEB-INF/classes 目录下。

因此,在 messages_en.properties中:

Hello=Hello

和在 messages_en_AU.properties中:

Hello=G'day

你可以通过设置Seam的配置属性 org.jboss.seam.core.resourceLoader.bundleNames 为资源包选择一个不同的名字。 甚至可以指定一个资源包名称列表,以深度优先进行消息的搜索。

<core:resource-loader>
    <core:bundle-names>
        <value>mycompany_messages</value>
        <value>standard_messages</value>       
    </core:bundle-names>
</core:resource-loader>

如果想为一个特殊页定义消息,可在以一个和JSF View id同名的资源包中指定,去掉前置 / 和文件扩展名。 这样,如果我们只想在 /welcome/hello.jsp 中显示消息,就把它置于 welcome/hello_en.properties 中。

你还可以在 pages.xml 中指定一个显式的绑定名称:

<page view-id="/welcome/hello.jsp" bundle="HelloMessages"/>

这样,我们就可以在 /welcome/hello.jsp 中使用定义在 HelloMessages.properties 中的消息了。

14.2.2. 标签显示

如果使用Seam的资源包来定义标签,就不用每页再写 <f:loadBundle ... /> 了,可以使用这种简单的形式:

<h:outputText value="#{messages['Hello']}"/>

或者:

<h:outputText value="#{messages.Hello}"/>

更好的一点是,message自身可以包含EL表达式:

Hello=Hello, #{user.firstName} #{user.lastName}
Hello=G'day, #{user.firstName}

你也可以在代码中这样使用消息:

@In private Map<String, String> messages;
@In("#{messages['Hello']}") private String helloMessage;

14.2.3. Faces Messages

facesMessages 组件是一个向用户显示成功或者失败消息的非常方便的途径。 我们之前描述的功能对Faces Messages同样有效:

@Name("hello")
@Stateless
public class HelloBean implements Hello {
    @In FacesMessages facesMessages;
    
    public String sayIt() {
        facesMessages.addFromResourceBundle("Hello");
    }
}

这将根据用户的locale显示 Hello, Gavin King 或者 G'day, Gavin

14.3. 时区

Seam中还有一个session范围的 java.util.Timezone 实例,叫做 org.jboss.seam.international.timezone, 和一个名为 org.jboss.seam.international.timezoneSelector 的用于设置时区的组件。默认情况下,时区取服务器的默认时区。 不幸的是,JSF规范中讲所有的日期和时间都假设是UTC 的,并且显示为UTC,除非使用 <f:convertDateTime> 明确地为其指定时区。 这是一个非常不方便的默认行为。

Seam覆写了这个行为,默认所有的日期和时间都是Seam的时区。另外,Seam提供了 <s:convertDateTime> 标签,用来处理Seam 时区的转化。

14.4. 主题

Seam应用程序可以很方便地改变皮肤。Theme API和本地化API非常相似,但是它们二者的关注点截然不同,一些应用同时支持本地化和主题。

首先,配置所支持的主题集合:

<theme:theme-selector cookie-enabled="true">
    <theme:available-themes>
        <value>default</value>
        <value>accessible</value>
        <value>printable</value>
    </theme:available-themes>
</theme:theme-selector>

注意,第一个是默认的主题。

主题定义在一个和该主题同名的属性文件中。例如,default 主题定义在default.properties中。 default.properties可能是这样定义的:

css ../screen.css
template /template.xhtml

通常主题资源包的内容是CSS样式或图片的路径和facelet模板(不像本地化资源包那样通常是文本)。

现在我们可以在JSP或者Facelet页面中使用这些内容了。例如,一个Facelet页的风格可以这样:

<link href="#{theme.css}" rel="stylesheet" type="text/css" />

或者,当页面定义在一个子目录中时可以这样:

<link href="#{facesContext.externalContext.requestContextPath}#{theme.css}" 
    rel="stylesheet" type="text/css" />

最强大的是,Facelet让我们通过 <ui:composition> 把模板主题化:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    template="#{theme.template}">

正如locale选择器一样,有一个内置的主题选择器允许用户在各主题间自由地切换:

<h:selectOneMenu value="#{themeSelector.theme}">
    <f:selectItems value="#{themeSelector.themes}"/>
</h:selectOneMenu>
<h:commandButton action="#{themeSelector.select}" value="Select Theme"/>

14.5. 使用cookie保存locale和主题设置

locale选择器、主题选择器和时区选择器全都支持持久化,把参数保存到cookie中。仅需要在components.xml中设置 cookie-enabled 配置属性:

<theme:theme-selector cookie-enabled="true">
    <theme:available-themes>
        <value>default</value>
        <value>accessible</value>
        <value>printable</value>
    </theme:available-themes>
</theme:theme-selector>

<international:locale-selector cookie-enabled="true"/>

Chapter 15. Seam Text

面向协作的网站需要一个友好的标记语言来简化对论坛帖子、Wiki页面、博客(Blog)、评论等等格式化文本的处理。 Seam提供了 <s:formattedText/> 控件来显示符合 Seam Text 语言的格式化文本。 Seam Text是用一个基于ANTLR的语法分析器来实现的。但你不需要知道ANTLR就能方便地使用它了。

15.1. 基本格式化

下面是个简单的例子:

It's easy to make *bold text*, /italic text/, |monospace|,
~deleted text~, super^scripts^ or _underlines_.

如果我们用 <s:formattedText/> 来显示它,会产生下面的HTML:

<p>
It's easy to make <b>bold text</b>, <i>italic text</i>, <tt>monospace</tt>
<del>deleted text</del>, super<sup>scripts</sup> or <u>underlines</u>.
</p>

可以用一个空行来表示新的段落,用 + 来表示一个标题:

+This is a big heading
You /must/ have some text following a heading!

++This is a smaller heading
This is the first paragraph. We can split it across multiple
lines, but we must end it with a blank line.

This is the second paragraph.

(请注意简单的换行是被忽略的,你需要一个额外的空行把文本换行到一个新的段落中。)下面就是HTML的结果:

<h1>This is a big heading</h1>
<p>
You <i>must</i> have some text following a heading!
</p>

<h2>This is a smaller heading</h2>
<p>
This is the first paragraph. We can split it across multiple
lines, but we must end it with a blank line.
</p>

<p>
This is the second paragraph.
</p>

# 字符来建立有序的列表。至于无序的列表就用 = 字符:

An ordered list:

#first item
#second item
#and even the /third/ item

An unordered list:

=an item
=another item
<p>
An ordered list:
</p>

<ol>
<li>first item</li>
<li>second item</li>
<li>and even the <i>third</i> item</li>
</ol>

<p>
An unordered list:
</p>

<ul>
<li>an item</li>
<li>another item</li>
</ul>

引用的部分应该用双引号括起来:

The other guy said:

"Nyeah nyeah-nee
/nyeah/ nyeah!"

But what do you think he means by "nyeah-nee"?
<p>
The other guy said:
</p>

<q>Nyeah nyeah-nee
<i>nyeah</i> nyeah!</q>

<p>
But what do you think he means by <q>nyeah-nee</q>?
</p>

15.2. 输入代码和有特殊字符的文本

*|# 这样的特殊字符, 和诸如 <>& 之类的HTML字符可以用来 \ 来转义:

You can write down equations like 2\*3\=6 and HTML tags
like \<body\> using the escape character: \\.
<p>
You can write down equations like 2*3=6 and HTML tags
like &lt;body&gt; using the escape character: \.
</p>

代码段可以用倒单引号(`)括起来:

My code doesn't work:

`for (int i=0; i<100; i--)
{
    doSomething();
}`

Any ideas?
<p>
My code doesn't work:
</p>

<pre>for (int i=0; i&lt;100; i--)
{
    doSomething();
}</pre>

<p>
Any ideas?
</p>

请注意行间的空格会转码(大部分的空格都会被格式化成文本形式,实际上有很多的特定字符都是代码或者标签)。因此你可以写成:

This is a |<tag attribute="value"/>| example.

在这里面的空格没有转换成任何的字符。下面你不能通过任何方式来格式化行间的空格文本(斜体字,下划线等等)。

15.3. 链接

可以用下面的语法来建立一个链接:

Go to the Seam website at [=>http://jboss.com/products/seam].

如果你想指定链接的文本,也可以这样:

Go to [the Seam website=>http://jboss.com/products/seam].

对于高级用户,甚至可以自定义Seam Text解析器,让它诠释这个语法书写的Wiki词汇。

15.4. 输入HTML

文本可能会包含一个HTML的有限子集(不用担心,这个子集可以抵御跨站脚本攻击)。这在建立链接时很有用:

You might want to link to <a href="http://jboss.com/products/seam">something
cool</a>, or even include an image: <img src="/logo.jpg"/>

建立表格时也一样:

<table>
    <tr><td>First name:</td><td>Gavin</td></tr>
    <tr><td>Last name:</td><td>King</td></tr>
</table>

如果你想的话你还能做得更多!

Chapter 16. iText PDF生成

Seam目前包括了一个利用iText生成文档的组件集。Seam的iText文档支持主要关注于PDF文档的生成,但它也对生成RTF文档提供基本的支持。

16.1. 使用PDF支持

iText支持由 jboss-seam-pdf.jar 提供。 这个JAR包中含有用来构造可以渲染成PDF的视图的iText JSF控件和把把渲染好的文档提供给用户的DocumentStore组件。 为了在你的应用程序中包括PDF支持,要把 jboss-seam-pdf.jar 和iText JAR 文件一起放在你的 WEB-INF/lib 路径下。 要使用Seam的iText支持就只是这样,无需更多的配置了。

Seam iText模块需要使用Facelets作为视图技术。这个库的未来版本也可能支持使用JSP。此外,这个模块还需要用到seam-ui包。

工程 examples/itext 包含了一个PDF支持实践的例子。 这个例子示范了正确的部署包,它包含一些例子,用来示范目前支持PDF生成的关键特性。

16.1.1. 创建一个文档

<p:document>

描述

文档是由facelets文档利用命名空间 http://jboss.com/products/seam/pdf 中的标签生成的。 文档应该总是以 document 标签作为文件的根结点。 document 标签为Seam产生文档到文档库并渲染一个HTML重定向到存储内容做准备。

属性

  • type — 要生成文档的类型。 有效值为PDFRTFHTML 模式。 Seam默认为PDF生成,并且很多特性只有在生成PDF文档时才能正确工作。

  • 最常用的值是 LETTERA4。 被支持页面大小的全部列表可以在类 com.lowagie.text.PageSize 中找到。 也可以这样,pageSize可以直接指定宽度和高度。例如,值"612 792"与LETTER的页面大小是一样的。

  • orientation — 页面的定位。 有效值是 portraitlandscape,在前景模式下,页面大小的高度和宽度值是颠倒的。

  • margins — 有左边距,右边距,上边距和下边距值。

  • marginMirroring — 显示页边距设置应该反转一个交换页面。

Metadata属性

  • 标题

  • 主题

  • 关键字

  • 作者

  • 创建者

用法

<p:document xmlns:p="http://jboss.com/products/seam/pdf">
  The document goes here.文档指向这里。
</p:document>

16.1.2. 基本的文本元素

有用的文档需要包含的不仅仅是文本。但是标准的UI组件只适合生成HTML,而不适合生成PDF内容。 反之,Seam则提供了一种特殊的UI组件,用来生成适当的PDF内容。 像 <p:image><p:paragraph> 这样标签是简单文档的根本。 像 <p:font> 这样的标签给它们周围的所有内容都提供了样式信息。

<p:paragraph>

描述

为了使文本片段能够按照逻辑分组布局、格式化、修饰,段落标签中包含了文本的大部分用法。

属性

  • firstLineIndent 行首缩进

  • extraParagraphSpace 额外段落空间

  • leading 段首

  • multipliedLeading 复合段首

  • spacingBefore — 段落元素前要插入的空白

  • spacingAfter — 段落元素后要插入的空白

  • indentationLeft 左缩进

  • indentationRight 右缩进

  • keepTogether 保持对齐

用法

<p:paragraph alignment="justify">
    This is a simple document.  It isn't very fancy. 这是一个简单的文档,它不是很常用。
</p:paragraph>

<p:text>

描述

text 标签能够使用一般的JSF转换机制从应用程序数据中生成为文本片段。 在渲染HTML文档的时候,它的用法非常类似于 outputText 标签。

属性

  • value — 要显示的这个值,通常是一个值绑定表达式。

用法

<p:paragraph>
    The item costs 显示一件商品的价格<p:text value="#{product.price}">
        <f:convertNumber type="currency" currencySymbol="$"/>
    </p:text>
</p:paragraph>

<p:font>

描述

font标签为它内部的所有文本定义要使用的默认字体。

属性

  • name — 字体名称,例如: COURIERHELVETICATIMES-ROMANSYMBOL 或者 ZAPFDINGBATS

  • size — 字体大小

  • style — 字体样式,下面这些的任意组合: NORMALBOLDITALICOBLIQUEUNDERLINELINE-THROUGH

  • encoding — 字符设置编码

用法

<p:font family="courier" style="bold" size="24">
    <p:paragraph>My Title</p:paragraph>
</p:font>

<p:newPage>

描述

p:newPage 插入一个新页面。

用法

<p:newPage />

<p:image>

描述

p:image 将一张图片插入到文档中。 利用value 属性从classpath或者Web应用程序上下文加载图片。

资源也可以由应用程序代码动态地生成。 imageData 属性可以指定一个值为 java.awt.Image 对象的值绑定表达式。

属性

  • value —一个资源名称或者是一个应用程序生成的图片的方法表达式绑定。

  • rotation — 图片旋转角度。

  • height — 图片高度。

  • width — 图片宽度。

  • alignment— 图片对齐方式。(可能的值请见 Section 16.1.7.2, “对齐方式值”

  • alt — 替换图片的文本。

  • indentationLeft 左缩进

  • indentationRight 右缩进

  • spacingBefore — 元素前要插入的空白。

  • spacingAfter — 元素后要插入的空白

  • widthPercentage 宽度百分比

  • initialRotation 初始旋转

  • dpi 像素

  • scalePercent(图片放缩比例) — 图片放缩比例因子(百分比)。 可以是一个百分比值,也可以两个分别代表X方向和Y方向的百分比值。

  • wrap

  • underlying 下划线

用法

<p:image value="/jboss.jpg" />
<p:image value="#{images.chart}" />

<p:anchor>

描述

p:anchor 定义文档中的活链接. 它支持下面的属性:

属性

  • name — 文档中目标锚点的名称。

  • reference — 链接指向的目标。文档中其他点的链接应该以一个"#"开头。 例如,"#link1" 用 link1 的一外名称指向 name 另一个锚点位置。 链接也可以是指向文档之外的一个资源的完整URL路径。

用法

<p:listItem><p:anchor reference="#reason1">Reason 1</p:anchor></p:listItem>
...
<p:paragraph>
    <p:anchor name="reason1">It's the quickest way to get "rich"</p:anchor>
    ...
</p:paragraph>

16.1.3. 页眉和页脚

<p:header>

<p:footer>

描述

p:headerp:footer 组件提供了将页眉和页脚文本放在生成文档的每个页面上的能力,除了第一个页面之外。页眉和页脚声明应该出现在文档的顶部。

属性

  • alignment — 页眉/页脚框的对齐方式。 (对齐方式的取值请见 Section 16.1.7.2, “对齐方式值”

  • backgroundColor — 页眉/页脚框的背景色。(颜色值请见 Section 16.1.7.1, “颜色值”

  • borderColor — 页眉/页脚框的边框颜色。 单独设置各边框颜色用 borderColorLeftborderColorRightborderColorTopborderColorBottom。(颜色值请见 Section 16.1.7.1, “颜色值”

  • borderWidth — 边框的宽度。 每一条单独的边可以利用 borderWidthLeftborderWidthRightborderWidthTopborderWidthBottom 来指定。

用法

<p:facet name="header">
  <p:font size="12">
    <p:footer borderWidthTop="1" borderColorTop="blue"
              borderWidthBottom="0" alignment="center">
        Why Seam? [<p:pageNumber />]
    </p:footer>
  </p:font>
</f:facet>

<p:pageNumber>

描述

通过 p:pageNumber 标签能够将当前页码放到页眉或者页脚的位置。 该标签只能用在页眉或者页脚的上下文中,并且只能使用一次。

用法

<p:footer borderWidthTop="1" borderColorTop="blue"
          borderWidthBottom="0" alignment="center">
    Why Seam? [<p:pageNumber />]
</p:footer>

16.1.4. 章节

<p:chapter>

<p:section>

描述

如果按照书籍/文章的结构生成文档,p:chapterp:section 标签就可以用来提供必要的结构。 小节标签只能在章的内部使用,但是它们之间却可以任意地嵌套。 大多数的PDF浏览工具都提供可以在文档内部章节之间自由切换的功能。

属性

  • alignment — 页眉/页脚框的对齐方式。 (对齐方式取值请见 Section 16.1.7.2, “对齐方式值”

  • number — 章序号。每章都应该有一个章序号。

  • numberDepth — 文档中章节的层次数。所有的小节都有一个相对于它们周围章/节的序号。 如果文章的默认显示层次数是3,那么3.1.4就表示第三章第一节的第四小节。 如果忽略章序号,那么层数就是2,上述小节的序号就应该显示为1.4。

用法

<p:document xmlns:p="http://jboss.com/products/seam/pdf"
            title="Hello">

   <p:chapter number="1">
      <p:title><p:paragraph>Hello</p:paragraph></p:title>
      <p:paragraph>Hello #{user.name}!</p:paragraph>
   </p:chapter>

   <p:chapter number="2">
      <p:title><p:paragraph>Goodbye</p:paragraph></p:title>
      <p:paragraph>Goodbye #{user.name}.</p:paragraph>
   </p:chapter>

</p:document> 

<p:header>

描述

任何一个章或节都可以包含一个 p:title 标签,用于显示下一章节的序号。 标签的主体可以包含原始文本,或者是一个 p:paragraph 标签。

16.1.5. 列表

标签 p:listp:listItem 可以显示列表结构。 列表里可以包含任意嵌套的子列表。列表中的项不能在列表之外使用。 在文档中通过以下文档使用 ui:repeat 标签显示从Seam组件获取到的值的列表。

<p:document xmlns:p="http://jboss.com/products/seam/pdf"
            xmlns:ui="http://java.sun.com/jsf/facelets"
            title="Hello">
   <p:list style="numbered">
      <ui:repeat value="#{documents}" var="doc">
         <p:listItem>#{doc.name}</p:listItem>
      </ui:repeat>
   </p:list>
</p:document>

<p:list>

属性

  • style — 列表的有序/无序排列方式。 可选项:NUMBEREDLETTEREDGREEKROMANZAPFDINGBATSZAPFDINGBATS_NUMBER。 如果没有指定样式,列表项就以无序排列。

  • listSymbol — 针对无序列表,指定列表符号。

  • indent — 列表的缩进程度。

  • lowerCase — 针对以字母方式排序的列表,表明字母是否应该为小写。

  • charNumber — 针对ZAPFDINGBATS排序,指明无序字符的字符编码。

  • numberType — 针对ZAPFDINGBATS_NUMBER排序,指明编号方式。

用法

<p:list style="numbered">
  <ui:repeat value="#{documents}" var="doc">
    <p:listItem>#{doc.name}</p:listItem>
  </ui:repeat>
</p:list>

<p:listItem>

描述

p:listItem 支持以下属性:

属性

用法

...

16.1.6. 表格

可以使用标签 p:tablep:cell 创建表格结构。 和许多表格结构不同,这里的的表格结构没有明确的行声明。如果一个表格有3列,那么每3个单元格会自动组成一行。 可以声明标题行和注脚行,并且当一个表格结构跨越多个页面的时候,标题行和注脚行就会重复地出现在每个页面上。

<p:table>

描述

p:table 支持以下属性。

属性

  • columns — 组成一个表行的列(单元格)的数量。

  • widths — 每个列的相对宽度。每个列应该都要有一个值。 例如widths="2 1 1"表示这个表格有3列,第一列的宽度是第二列和第三列的两倍。

  • headerRows — 初始行数量, 可以认为是标题或者注脚行的数量,在表格跨多个页面的时候应该重复的行数。

  • footerRows — 被认为是注脚行的行数。 这个值应该减去 headerRows 值。 如果文档有2行构成标题行、1行构成注脚行,那么 headerRows 应该设置为3,footerRows 应该设置为1

  • widthPercentage — 表格占页面宽度的百分比。

  • horizontalAlignment — 表格的水平对齐方式。 (可能取值请见 Section 16.1.7.2, “对齐方式值”

  • skipFirstHeader

  • runDirection

  • lockedWidth

  • splitRows

  • spacingBefore — 元素前要插入的空白。

  • spacingAfter — 元素后要插入的空白。

  • extendLastRow

  • headersInEvent

  • splitLate

  • keepTogether

用法

<p:table columns="3" headerRows="1">
  <p:cell>name</p:cell>
  <p:cell>owner</p:cell>
  <p:cell>size</p:cell>
  <ui:repeat value="#{documents}" var="doc">
    <p:cell>#{doc.name}</p:cell>
    <p:cell>#{doc.user.name}</p:cell>
    <p:cell>#{doc.size}</p:cell>
  </ui:repeat>
</p:table>

<p:cell>

描述

p:cell 支持下面的属性。

属性

  • colspan — 通过声明 colspan 值大于1,单元格可以跨多个列。表格不具备跨多行的能力。

  • horizontalAlignment — 单元格的水平对齐方式。 (可能取值请见 Section 16.1.7.2, “对齐方式值”

  • verticalAlignment — 单元格的垂直对齐方式。 (可能取值请见 Section 16.1.7.2, “对齐方式值”

  • padding — 填充指定的边还可以通过以下属性来指定: paddingLeftpaddingRightpaddingToppaddingBottom

  • useBorderPadding

  • leading

  • multipliedLeading

  • indent 缩进

  • verticalAlignment 垂直对齐

  • extraParagraphSpace 额外段落空间

  • fixedHeight 固定高度

  • noWrap

  • minimumHeight 最小高度

  • followingIndent 底部缩进

  • rightIndent 右缩进

  • spaceCharRatio 字符间距比

  • runDirection 排列方向

  • arabicOptions 阿拉伯语选项

  • useAscender 递增

  • grayFill 灰色填充

  • rotation 旋转度

用法

<p:cell>...</p:cell>

16.1.7. 文档常量

本节例举了一些被属性在多个页面共享的常量。

16.1.7.1. 颜色值

Seam 尚未支持全部颜色的定义。目前只支持下面的颜色:whitegraylightgraydarkgrayblackredpinkyellowgreenmagentacyanblue

16.1.7.2. 对齐方式值

在用到对齐方式值的地方,Seam PDF支持下列水平对齐值: leftrightcenterjustifyjustifyall。 垂直对齐值为:topmiddlebottombaseline

16.1.8. iText配置

生成的文档无需其它的额外配置就可以使用。但是更加严格的应用程序则需要一些配置的要点。

默认的实现能够从一般的URL得到PDF文档,/seam-doc.seam。 许多浏览器(和用户)更倾向于见到包含实际PDF文档名诸如 /myDocument.pdf的链接。 这个功能需要一些配置。为了在浏览器中浏览PDF文件,所有的*.pdf资源都应该被映射到Seam Servlet过滤器以及DocumentStoreServlet:

<filter>
    <filter-name>Seam Servlet Filter</filter-name>
    <filter-class>org.jboss.seam.servlet.SeamServletFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>Seam Servlet Filter</filter-name>
    <url-pattern>*.pdf</url-pattern>
</filter-mapping>

<servlet>
    <servlet-name>Document Store Servlet</servlet-name>
    <servlet-class>org.jboss.seam.pdf.DocumentStoreServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Document Store Servlet</servlet-name>
    <url-pattern>*.pdf</url-pattern>
</servlet-mapping>

文档存储组件中的 useExtensions 选项保证了文档存储时的扩展名和生成时的一致。

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:pdf="http://jboss.com/products/seam/pdf">
    <pdf:documentStore useExtensions="true" />
</components>

生成的文档保存在当前对话范围中直到对话结束。此时,文档的引用就无效了。 当对文档不存在时,你可以利用documentStore的 errorPage 属性指定要显示的默认视图。

<pdf:documentStore useExtensions="true" errorPage="/pdfMissing.seam" />

16.2. 图表

<p:barchart>

描述

显示柱状图。

属性

  • borderVisible — 控制图表边框是否要显示。

  • borderPaint — 边框可见时的边框颜色。

  • borderBackgroundPaint — 图表的默认背景色。

  • borderStroke

  • domainAxisLabel — 域坐标轴的文本标签。

  • domainAxisPaint — 域坐标轴标签的颜色。

  • domainGridlinesVisible— 是否在图表上显示域坐标上的网格。

  • domainGridlinePaint— 域坐标上网格可见时的颜色。

  • domainGridlineStroke — 域坐标轴上网格线可见时的画笔风格。

  • height — 图表的高度。

  • width — 图表的宽度。

  • is3D — 表示图表应该以3D而不是2D渲染的一个布尔值。

  • legend — 表示图表中是否应该显示图例的一个布尔值。

  • legendItemPaint— 图例中文本标签的默认颜色。

  • legendItemBackgoundPaint— 图例的背景色与图表的背景色不一致时的颜色。

  • orientation — 绘图方向,可以是 vertical(默认),也可以是 horizontal

  • plotBackgroundPaint— 绘图背景的颜色。

  • plotBackgroundAlpha— 绘图区域背景的alpha(透明度)级别。 它应该是0(完全透明)到1(完全不透明)之间的一个数字。

  • plotForegroundAlpha— 绘图区域的alpha(透明度)级别。 它应该是0(完全透明)到1(完全不透明)之间的一个数字。

  • plotOutlinePaint— 绘图区域边线可见时的颜色。

  • plotOutlineStroke — 绘图区域边线可见时的画笔风格。

  • rangeAxisLabel — 值坐标轴上的文本标签。

  • rangeAxisPaint — 值坐标轴上的标签颜色。

  • rangeGridlinesVisible— 是否显示值坐标轴上的网格。

  • rangeGridlinePaint— 值坐标轴上网格可见时的颜色。

  • rangeGridlineStroke — 值坐标轴上网格线可见时的画笔风格。

  • title — 图表标题文字。

  • titlePaint— 图表标题文字的颜色。

  • titleBackgroundPaint— 图表标题文字的背景色。

  • width — 图表的宽度。

用法

<p:barchart title="Bar Chart" legend="true"
            width="500" height="500">
    <p:series key="Last Year">
        <p:data columnKey="Joe" value="100" />
        <p:data columnKey="Bob" value="120" />
    </p:series>        <p:series key="This Year">
        <p:data columnKey="Joe" value="125" />
        <p:data columnKey="Bob" value="115" />
    </p:series>
</p:barchart>
                            

<p:linechart>

描述

显示一个折线图。

属性

  • borderVisible — 控制是否显示整个图表的边框。

  • borderPaint — 图表边框可见时的颜色。

  • borderBackgroundPaint — 图表的默认背景色

  • borderStroke

  • domainAxisLabel — 域坐标轴的文本标签。

  • domainAxisPaint — 域坐标轴标签的颜色。

  • domainGridlinesVisible— 控制是否在图表上显示域坐标的网格。

  • domainGridlinePaint— 域坐标上网格可见时的颜色。

  • domainGridlineStroke — 域坐标轴上网格线可见时的画笔风格。

  • height — 图表的高度。

  • width — 图表的宽度。

  • is3D — 表示应该以3D而不是2D图表渲染的一个布尔值。

  • legend — 表示是否应该在图表中包括图例的一个布尔值。

  • legendItemPaint— 图例中文本标签的默认颜色。

  • legendItemBackgoundPaint— 图例的背景色与图表的背景色不一致时的颜色。

  • orientation — 绘图区的方向,可以是vertical(默认), 也可以是 horizontal

  • plotBackgroundPaint— 绘图区域的背景色。

  • plotBackgroundAlpha— 绘图区域背景的alpha(透明度)级别。 它应该是0(完全透明)到1(完全不透明)之间的一个数字。

  • plotForegroundAlpha— 绘图区域的alpha(透明度)级别。 它应该是0(完全透明)到1(完全不透明)之间的一个数字。

  • plotOutlinePaint— 绘图区域边线可见时的颜色。

  • plotOutlineStroke — 绘图区域边线可见时的画笔风格。

  • rangeAxisLabel — 值坐标轴上的文本标签。

  • rangeAxisPaint — 值坐标轴上的标签颜色。

  • rangeGridlinesVisible— 控制是否在图表上显示值坐标轴的网格。

  • rangeGridlinePaint— 值坐标轴上网格可见时的颜色。

  • rangeGridlineStroke — 值坐标轴上网格线可见时的画笔风格。

  • title — 图表的标题文字。

  • titlePaint— 图表标题文字的颜色。

  • titleBackgroundPaint— 图表标题的背景色。

  • width — 图表的宽度。

用法

<p:linechart title="Line Chart"
            width="500" height="500">
    <p:series key="Prices">
        <p:data columnKey="2003" value="7.36" />
        <p:data columnKey="2004" value="11.50" />
        <p:data columnKey="2005" value="34.625" />
        <p:data columnKey="2006" value="76.30" />
        <p:data columnKey="2007" value="85.05" />
    </p:series>
</p:linechart>

<p:piechart>

描述

显示一个饼状图。

属性

  • title 标题

  • label 标签

  • legend 图例

  • is3D 是否3D

  • labelLinkMargin 标签链接边距

  • labelLinkPaint 标签链接颜色

  • labelLinkStroke 标签链接画笔风格

  • labelLinksVisible 标签链接是否可见

  • labelOutlinePaint 标签外边线颜色

  • labelOutlineStroke 标签外边线画笔风格

  • labelShadowPaint 标签阴影颜色

  • labelPaint 标签颜色

  • labelGap 标签间隔

  • labelBackgroundPaint 标签背景色

  • startAngle 起始角度

  • circular 圆形

  • direction 方向

  • sectionOutlinePaint 截面外边线颜色

  • sectionOutlineStroke 截面外边线画笔风格

  • sectionOutlinesVisible 截面外边线是否可见

  • baseSectionOutlinePaint 基本截面外边线颜色

  • baseSectionPaint 基本界面颜色

  • baseSectionOutlineStroke 基本界面外边线画笔风格

用法

<p:series>

描述

类数据可以分解成系列。series标签用于按照系列给一组数据分类,并且将样式应用到所有系列。

属性

  • key — 系列名。

  • seriesPaint — 系列中每个项的颜色。

  • seriesOutlinePaint — 系列中每个项的外边线颜色。

  • seriesOutlineStroke — 系列中每个项所使用的画笔风格

  • seriesVisible — 表示系列是否显示的一个布尔值。

  • seriesVisibleInLegend — 表示是否在图例中列出系列的一个布尔值。

用法

<p:series key="data1">
    <ui:repeat value="#{data.pieData1}" var="item">
        <p:data columnKey="#{item.name}" value="#{item.value}" />
    </ui:repeat>
</p:series>

<p:data>

描述

该数据标签描述要在图表中显示的每个数据点。

属性

  • key — 数据项的名称。

  • series — 系列名,当该标签没有内嵌在<p:series>中的时候。

  • value — 数字化的数据值。

  • explodedPercent — 对于饼状图,表示分离块的百分比大小。

  • sectionOutlinePaint — 对于柱状图,表示截面外边线的颜色。

  • sectionOutlineStroke — 对于柱状图,表示截面外边线的画笔风格。

  • sectionPaint — 对于柱状图,表示截面的颜色。

用法

<p:data key="foo" value="20" sectionPaint="#111111"
        explodedPercent=".2" />
<p:data key="bar" value="30" sectionPaint="#333333" />
<p:data key="baz" value="40" sectionPaint="#555555"
        sectionOutlineStroke="my-dot-style" />

<p:color>

描述

颜色组件声明一种颜色或者一组渐变的颜色,可以在绘制填充图形时使用。

属性

  • color — 颜色值,对于渐变的颜色,这表示起始的颜色值。 Section 16.1.7.1, “颜色值”

  • color2 — 对于渐变的颜色,这表示结束渐变的颜色值。

  • point — 渐变颜色的起始坐标

  • point2 — 渐变颜色的结束坐标

用法

<p:color id="foo" color="#0ff00f"/>
<p:color id="bar" color="#ff00ff" color2="#00ff00"
                  point="50 50" point2="300 300"/>  

<p:stroke>

描述

描述一种用来在图表中画线的画笔风格。

属性

  • width — 画笔的宽度。

  • cap — 线端类型,有效值为 buttroundsquare

  • join — 线交汇点的类型,有效值为 miter, roundbevel

  • miterLimit — 线边缘交汇,这个值限制交汇点的大小。

  • dash — 设置要用来画线的虚线模式的虚线值。整数代表交替画实线和虚线的长度。

  • dashPhase — 表示虚线模式中实线的偏移量。

用法

<p:stroke id="dot2" width="2" cap="round" join="bevel" dash="2 3" />
                            

16.3. 柱状图编码

...

<p:barcode>

描述

属性

  • type 类型

  • code 编码

  • xpos 横坐标

  • ypos 纵坐标

  • rotDegrees 旋转角度

  • barHeight 高度

  • textSize 文本大小

  • minBarWidth 最小柱状图宽度

  • barMultiplier

用法

16.4. 更详细的文档

关于iText的更多信息,请见:

Chapter 17. 电子邮件

Seam现在包含了一个用于模板和发送邮件的可选组件。

邮件支持是由 jboss-seam-mail.jar 提供的。 这个JAR包包括用于创建邮件的mail JSF控件,以及 mailSession 管理组件。

examples/mail项目包括一份实用的email支持示例。该例子示范了恰当的打包方式,并且包含了一些当前支持的关键特性。

你也可以使用Seam的集成测试环境来测试你的mail程序,参见 Section 31.3.2, “Seam Mail集成测试”

17.1. 创建一条消息

为了使用Seam Mail,你并不需要完整的学习一门模板语言—一封邮件仅仅是一个facelet!【Facelet是用来建立JSF应用程序时的一个可供选择的表现层技术】

<m:message xmlns="http://www.w3.org/1999/xhtml"
    xmlns:m="http://jboss.com/products/seam/mail"
    xmlns:h="http://java.sun.com/jsf/html">

    <m:from name="Peter" address="peter@example.com" />
    <m:to name="#{person.firstname} #{person.lastname}">#{person.address}</m:to>
    <m:subject>Try out Seam!</m:subject>

    <m:body>
        <p><h:outputText value="Dear #{person.firstname}" />,</p>
        <p>You can try out Seam by visiting
        <a href="http://labs.jboss.com/jbossseam">http://labs.jboss.com/jbossseam</a>.</p>
        <p>Regards,</p>
        <p>Pete</p>
    </m:body>

</m:message>

<m:message> 标签包装整个消息,并且通知Seam开始渲染一封邮件。 在这个 <m:message> 标签内,我们使用标签 <m:from> 来设置这个消息是来自谁, 使用标签 <m:to> 来标识发送者(注意我们就和在普通的facelet里一样使用EL),和 <m:subject> 标签。

<m:body> 标签包装邮件的主体。你可以像JSF组件那样将正规的HTML标签用在邮件主体内。

好,现在你已经有了email的模板,你将如何发送它呢? 在 m:message 的结尾,mailSession 将被调用,用于发送邮件。 所以,你所有要做的仅仅是请求Seam渲染这个视图:

@In(create=true)
private Renderer renderer;

public void send() {
    try {
       renderer.render("/simple.xhtml");
       facesMessages.add("Email sent successfully");
   }
   catch (Exception e) {
       facesMessages.add("Email sending failed: " + e.getMessage());
   }
}

假如:你输入了一个无效的email地址,将会抛出一个异常。该异常将被捕捉并显示给用户。

17.1.1. 附件

Seam中邮件添加附件的操作变得轻而易举。在处理文件时,它支持绝大多数的标准Java类型。

如果你想通过邮件发送 jboss-seam-mail.jar

<m:attachment value="/WEB-INF/lib/jboss-seam-mail.jar"/>

Seam将通过classpath加载文件,并将其附件加入到到邮件中。 默认情况下,它将被像 jboss-seam-mail.jar 一样加载; 如果你想为它添加别名,只需要添加 fileName 属性即可。

<m:attachment value="/WEB-INF/lib/jboss-seam-mail.jar" fileName="this-is-so-cool.jar"/>

你同样可以附加 java.io.Filejava.net.URL

<m:attachment value="#{numbers}"/>

也可以是 byte[] 或是 java.io.InputStream

<m:attachment value="#{person.photo}" contentType="image/png"/>

你会注意到对于 byte[]java.io.InputStream, 你需要指定附件的MIME类型(因为这两种文件不带此类信息。)

更好的是,你可以附Seam产生的PDF或任意标准的JSF视图,只需将你使用的普通标签用 <m:attachment> 封装起来即可:

<m:attachment fileName="tiny.pdf">
    <p:document>
        A very tiny PDF
    </p:document>
</m:attachment>

如果你想将多个文件添加到附件中(例如从数据库加载的一套照片),你只需要使用 <ui:repeat>

<ui:repeat value="#{people}" var="person">
    <m:attachment value="#{person.photo}" contentType="image/jpeg" fileName="#{person.firstname}_#{person.lastname}.jpg"/>
</ui:repeat>

如果你想直接显示一个附上的图片:

<m:attachment
    value="#{person.photo}"
    contentType="image/jpeg"
    fileName="#{person.firstname}_#{person.lastname}.jpg"
    status="personPhoto"
    disposition="inline" />
<img src="cid:#{personPhoto.contentId}" />

你可能会问 cid:#{...} 的作用是什么。 是这样的,IETF明确规定将这个标签加入作为你图片的src(源文件),当试着定位图片(Content-ID必须匹配)时,它就能够被查找到。— 多么神奇!

在访问状态对象之前你必须声明附件。

17.1.2. HTML/Text 交替部分

尽管现在绝大多数的邮件查看器都支持HTML格式的邮件,但还是有一些不支持,所以你可以在邮件体里添加一个无格式的文本作为替换。

<m:body>
    <f:facet name="alternative">Sorry, your email reader can't show our fancy email,
please go to http://labs.jboss.com/jbossseam to explore Seam.</f:facet>
</m:body>

17.1.3. 多个收件人

很多时候你希望向一个收件组(比如你的用户们)发送邮件。 所有的收件人标签可以被放在一个 <ui:repeat> 标签中:

<ui:repeat value="#{allUsers} var="user">
    <m:to name="#{user.firstname} #{user.lastname}" address="#{user.emailAddress}" />
</ui:repeat>

17.1.4. 多条信息

有时候,你需要向每一个收件人发送一条稍微有差别的信(例如:重设密码)。 最好的方法就是将整个信息放在 <ui:repeat> 标签中:

<ui:repeat value="#{people}" var="p">
    <m:message>
        <m:from name="#{person.firstname} #{person.lastname}">#{person.address}</m:from>
        <m:to name="#{p.firstname}">#{p.address}</m:to>
            ...
    </m:message>
</ui:repeat>

17.1.5. 模板

邮件模板示例显示(facelets模板)可以和Seam的mail标签很好的结合。

我们的 template.xhtml 包括:

<m:message>
   <m:from name="Seam" address="do-not-reply@jboss.com" />
   <m:to name="#{person.firstname} #{person.lastname}">#{person.address}</m:to>
   <m:subject>#{subject}</m:subject>
   <m:body>
       <html>
           <body>
               <ui:insert name="body">This is the default body, specified by the template.</ui:insert>
           </body>
       </html>
   </m:body>
</m:message>

我们的 templating.xhtml 包括:

<ui:param name="subject" value="Templating with Seam Mail"/>
<ui:define name="body">
    <p>This example demonstrates that you can easily use <i>facelets templating</i> in email!</p>
</ui:define>

你也可以在你的邮件中使用facelet的源标签,但你必须将它们置于一个jar包中并放在 WEB-INF/lib 目录下 - 当使用Seam Mail从 web.xml 引用 .taglib.xml 并不可靠。 (因为如果你异步的发送你的邮件,Seam Mail无法访问到完整的JSF或Servlet上下文,所以并不知道 web.xml 的配置参数)

发送邮件时,如果你需要更多的配置Facelets或JSF,你需要重载Renderer组件,并且编程式地做配置工作 - 仅限于高级用户。

17.1.6. 国际化

Seam支持发送国际化的信息。默认情况下,使用JSF提供的编码,但也可以由如下的模板重写:

<m:message charset="UTF-8">
   ...
</m:message>

邮件内容、主题和收件人(和发件人)的名称都会被编码。通过设置模板的编码,你需要确认facelets是否使用了正确的编码方式来解析你的页面。

<?xml version="1.0" encoding="UTF-8"?>

17.1.7. 其它的标识头

有时候你会想在邮件上添加其他的头信息。Seam提供了一部分支持(请看 Section 17.5, “标签”)。 例如:我们可以设置邮件的重要程度,或着请求一个阅读回执。

<m:message xmlns:m="http://jboss.com/products/seam/mail"
    importance="low"
    requestReadReceipt="true"/>

另外你也可以通过使用 <m:header> 标签,为消息添加其它任意的头信息。

<m:header name="X-Sent-From" value="JBoss Seam"/>

17.2. 接收邮件

如果你正在使用EJB,你可以使用MDB(消息驱动Bean:Message Driven Bean)来接收消息。 JBoss提供JCA适配器 —mail-ra.rar 但是跟随JBoss发布的版本有一定的限制(某些版本没有做捆绑)。 因此我们建议采用跟随推荐的Seam发布的 mail-ra.rar(不在Seam包的 mail 目录)。 mail-ra.rar 应该被放置在 $JBOSS_HOME/server/default/deploy 目录下; 如果你正在使用的JBoss版本已经有了这个文件,就替换了它。

您可以向这样配置:

@MessageDriven(activationConfig={
    @ActivationConfigProperty(propertyName="mailServer", propertyValue="localhost"),
    @ActivationConfigProperty(propertyName="mailFolder", propertyValue="INBOX"),
    @ActivationConfigProperty(propertyName="storeProtocol", propertyValue="pop3"),
    @ActivationConfigProperty(propertyName="userName", propertyValue="seam"),
    @ActivationConfigProperty(propertyName="password", propertyValue="seam")
})
@ResourceAdapter("mail-ra.rar")
@Name("mailListener")
public class MailListenerMDB implements MailListener {

    @In(create=true)
    private OrderProcessor orderProcessor;

    public void onMessage(Message message) {
       // Process the message
       orderProcessor.process(message.getSubject());
    }

}

每一个接收到的消息都将导致 onMessage(Message message) 被调用。 大多数Seam的注释将会在MDB内部运行,但你不可以访问持久上下文。

在链接 http://wiki.jboss.org/wiki/Wiki.jsp?page=InboundJavaMailmail-ra.rar 上你可以找到更多的信息。

如果你没有使用JBoss,你依然可以使用 mail-ra.rar,或许你可以在你的程序服务器上找到类似的适配器。

17.3. 配置

为了在你的应用程序中能够使用电子邮件,要确保 jboss-seam-mail.jar 包含在 WEB-INF/lib 目录中。 如果你在使用JBoss AS,则使用Seam的邮件支持不需要做更多的配置工作了。 否则你可能需要确认你是否有JavaMail的API,一个可供使用的JavaMail API的实现(JBoss AS中使用的API和实现正如作为 lib/mail.jar 跟随Seam发布的包),和一份Java Activation Framework的拷贝(作为 lib/activation.jar 跟随Seam发布)。

Seam的Email模块需要Facelets作为视图技术。将来库的版本可能会添加对JSP的支持。另外,它需要用到seam-ui包。

mailSession 组件使用JavaMail就像与'真实的'SMTP服务器通讯。

17.3.1.  mailSession

如果你在使用JEE环境工作,可以通过JNDI查找可用的JavaMail Session,你也可以使用Seam配置好的Session。

Section 28.8, “与邮件相关的组件” 中有关于邮件会话组件属性的详细介绍。

17.3.1.1. 在JBoss AS中查找JNDI

JBossAS deploy/mail-service.xml 配置JavaMail会话捆绑到JNDI。 你需要修改默认的服务配置再应用到你的网络中。这里描述了更加详细的服务 http://wiki.jboss.org/wiki/Wiki.jsp?page=JavaMail

<components xmlns="http://jboss.com/products/seam/components"
    xmlns:core="http://jboss.com/products/seam/core"
    xmlns:mail="http://jboss.com/products/seam/mail">

    <mail:mail-session session-jndi-name="java:/Mail"/>

</components>

这里我们告诉Seam在JNDI中是通过 java:/Mail 来获得邮件Session的。

17.3.1.2. Seam配置会话

邮件会话可以通过 components.xml 配置来访问的。 这里我们告诉Seam使用 smtp.example.com 作为SMTP服务器。

<components xmlns="http://jboss.com/products/seam/components"
    xmlns:core="http://jboss.com/products/seam/core"
    xmlns:mail="http://jboss.com/products/seam/mail">

    <mail:mail-session host="smtp.example.com"/>

</components>

17.4. Meldware

Seam的邮件示例采用Meldware(来自 buni.org)作为邮件服务器。 Meldware是提供 SMTPPOP3IMAP、WebMail、共享日历和图形化的管理工具的于一身的软件; 它是作为一个JEE应用程序编写的,因此可以和你的Seam程序一起部署到JBoss上。

和Seam一起分发的Meldware的版本(在文件夹mail/buni-meldware)为了开发都被特别修改过 - 邮箱、用户和别名(邮件地址)在每次程序部署的时候创建。 如果你希望在产品中不仅仅使用Meldware发送邮件,建议你使用vanilla拷贝。 你也可以使用 meldware 组件来创建邮箱,用户和别名等。

<components xmlns="http://jboss.com/products/seam/components"
    xmlns:core="http://jboss.com/products/seam/core"
    xmlns:mail="http://jboss.com/products/seam/mail">

    <mail:mail-session host="smtp.example.com"/>

    <mail:meldware>
        <mail:users>
            <value>#{duke}</value>
            <value>#{root}</value>
	    </mail:users>
    </mail:meldware>

   	<mail:meldware-user name="duke" username="duke" password="duke">
   	    <mail:aliases>
   	        <value>duke@jboss.org</value>
   	        <value>duke@jboss.com</value>
   	    </mail:aliases>
   	<mail:meldware-user name="root" username="root" password="root" administrator="true" />
</components>

这里我们创建了两个用户,拥有两个邮件地址的 duke 和名为 root 的管理员。

17.5. 标签

邮件通过使用命名空间 http://jboss.com/products/seam/mail 的标签生成。 文档中在消息的根部通常应该有 message 标签,message标签使Seam准备生成一封邮件。

标准的facelets的模板标签可以如同往常一样地来使用。 你可以在主体内部使用任何JSF标签;如果需要访问外部资源(stylesheets、javascript),那么就要确认是否设置了 urlBase

<m:message>

邮件消息的根标签

  • importance — 低、正常或是高。默认是正常,这是设置邮件消息重要程度的标签。

  • precedence — 设置消息的优先级(例如:突出)

  • requestReadReceipt —默认是false,如果设置,将会添加阅读回执,阅读回执将会被发给 From: 地址。

  • urlBase — 如果设置,预设的 requestContextPath 将允许你在邮件中使用形如 <h:graphicImage> 的组件。

<m:from>

设置邮件的发件地址。每封邮件只允许有一个这样的值。

  • name — 邮件应该来自的名称。

  • address — 邮件应该来自的地址。

<m:replyTo>

设置回复地址给邮件。每封邮件同样只能有一个这样的值。

  • address — 邮件来源的地址。

<m:to>

添加一个收件人到邮件。有多个收件人时使用复合的<m:to>标签。 这个标签可以被安全的放置在重复标签<ui:repeat>之类中。

  • name — 收件人的名字。

  • address — 收件人的地址。

<m:cc>

添加抄送地址到邮件。有多个抄送地址时使用复合的<m:cc>标签。这个标签可以被安全的放置在重复标签<ui:repeat>之类中。

  • name — 收件人的名字。

  • address — 收件人的邮件地址。

<m:bcc>

添加一个秘文抄送人到邮件。有多个秘密抄送地址时使用复合的<m:bcc>标签。 这个标签可以被安全的放置在重复标签<ui:repeat>之类中。

  • name — 收件人的名字。

  • address — 收件人的邮件地址。

<m:header>

向邮件添加一个头(例如:X-Sent-From: JBoss Seam)。

  • name — 要添加的头的名字(例如:X-Sent-From)。

  • value — 要添加的头的值(例如:JBoss Seam)。

<m:attachment>

添加一个附件到邮件。

  • value — 要添加的附件:

    • String — 在classpath中一个 String 作为到文件的路径被解析。

    • java.io.File —一个指向 File 对象的EL表达式。

    • java.net.URL — 一个指向URL对象的EL表达式。

    • java.io.InputStream — 一个指向 InputStream 类型的EL表达式。 这种情况下,fileNamecontentType 都必须指定。

    • byte[] — 一个指向 byte[] 类型的EL表达式。 这种情况下,fileNamecontentType 都必须指定。

    如果值属性被省略:

    • 如果这个标签包含一个 <p:document> 标签,这个被描述的文档将会被生成并且附加到邮件上。 fileName 应该被指定。

    • 如果这个标签包含其它的JSF标签,将会通过它们生成HTML文档并附加到邮件。fileName应该被指定。

  • fileName — 指定可供使用的已经被附上的文件。

  • contentType —指定已附上的文件的MIME类型。

<m:subject>

设置邮件主题。

<m:body>

设置邮件主体。支持 alternative facet。 比如生成的一个HTML邮件可能包含针对不支持html的阅读器的备选的文本。

  • type — 如果设为 plain,将会生成一份简单文本邮件,否则将会生成一份HTML邮件。

Chapter 18. 异步和消息

Seam使得异步执行一个来自Web请求的工作变得非常容易。当大多数人在Java EE里考虑异步时,他们想到用JMS。 在Seam中,这确实是一种解决方案,当你有严格和明确定义的QoS服务需求时,这是正确的。Seam利用Seam组件让发送和接收JMS消息更容易进行。

但是对于多数用例来说,用JMS无异于杀鸡用牛刀。Seam将简单的异步方法和事件应用分层,置于你选择的 dispatchers 之上。

  • java.util.concurrent.ScheduledThreadPoolExecutor (默认)

  • EJB Timer Service (针对 EJB 3.0 环境)

  • Quartz

18.1. 异步

异步的事件和方法调用与底层的分配机制有着相同的服务期待质量。 基于 ScheduledThreadPoolExecutor 的默认dispatcher执行得很好,但不提供对持久化异步任务的支持,因此不保证一项任务真正会被执行。 如果你在一个支持EJB 3.0的环境中工作,并将下面这一行添加到 components.xml 中:

<async:timer-service-dispatcher/>

那么,你的异步任务将由容器的EJB定时服务处理。 如果你不熟悉Timer服务,也不必担心,如果你想要在Seam中使用异步方法,并不需要与它直接交互。 要了解一件重要的事情:任何好的EJB 3.0实现都将有使用持久化定时器的选择,它为任务最终得到处理提供了一些保证。

另一种选择是使用开源的Quartz库来管理异步的方法。 你要将Quartz库JAR(在 lib 路径中)绑定在你的EAR中,并在 application.xml 中将它声明成一个Java模块。 另外,你还需要将下面的行添加到 components.xml 中来安装Quartz Dispatcher。

<async:quartz-dispatcher/>

Seam的API对于默认的 ScheduledThreadPoolExecutor 的Seam API,及EJB3 Timer与Quartz Scheduler 大体相同。 它们可以只是通过在 components.xml 中添加一行来进行”即插即用(plug and play)“。

18.1.1. 异步方法

最简单的形式,一个异步的调用只是异步地处理来自访问者的方法调用(在不同的线程中)。 当我们要返回一个即时响应给客户端时,通常使用一个异步调用,并让一些费时的工作在后台处理。 此模式在使用AJAX的应用程序中运行良好,在AJAX应用中客户端能够自动地从服务器上获得工作结果。

对于EJB组件,我们在本地接口上进行注解,来指定某个方法要被异步地处理。

@Local
public interface PaymentHandler
{
    @Asynchronous
    public void processPayment(Payment payment);
}

(对于JavaBean组件,如果喜欢的话,我们可以注解组件实现类。)

异步的使用对于Bean类来说是透明的:

@Stateless
@Name("paymentHandler")
public class PaymentHandlerBean implements PaymentHandler
{
    public void processPayment(Payment payment)
    {
        //do some work!
    }
}

并且对客户端也是透明的:

@Stateful
@Name("paymentAction")
public class CreatePaymentAction
{
    @In(create=true) PaymentHandler paymentHandler;
    @In Bill bill;

    public String pay()
    {
        paymentHandler.processPayment( new Payment(bill) );
        return "success";
    }
}

异步方法在一个全新的事件上下文中处理,而且无法访问调用者的会话或对话上下文状态。 然而,业务流程上下文 得到了 传播。

异步方法调用可以利用 @Duration@Expiration@IntervalDuration注解为后续的执行定时。

@Local
public interface PaymentHandler
{
    @Asynchronous
    public void processScheduledPayment(Payment payment, @Expiration Date date);

    @Asynchronous
    public void processRecurringPayment(Payment payment, @Expiration Date date, @IntervalDuration Long interval)'
}
@Stateful
@Name("paymentAction")
public class CreatePaymentAction
{
    @In(create=true) PaymentHandler paymentHandler;
    @In Bill bill;

    public String schedulePayment()
    {
        paymentHandler.processScheduledPayment( new Payment(bill), bill.getDueDate() );
        return "success";
    }

    public String scheduleRecurringPayment()
    {
        paymentHandler.processRecurringPayment( new Payment(bill), bill.getDueDate(), ONE_MONTH );
        return "success";
    }
}

客户端和服务端两者都可以访问与调用相关联的 Timer 对象。当使用EJB3 Dispatcher时, The Timer 对象会显示在下面。对于默认的ScheduledThreadPoolExecutor,返回的是JDK的对象Future。对于Quartz Dispatcher,返回QuartzTriggerHandle,我们会在下部分对此进行讨论。

@Local
public interface PaymentHandler
{
    @Asynchronous
    public Timer processScheduledPayment(Payment payment, @Expiration Date date);
}
@Stateless
@Name("paymentHandler")
public class PaymentHandlerBean implements PaymentHandler
{
    @In Timer timer;

    public Timer processScheduledPayment(Payment payment, @Expiration Date date)
    {
        //do some work!

        return timer; // 注意返回值被完全忽略
    }

}
@Stateful
@Name("paymentAction")
public class CreatePaymentAction
{
    @In(create=true) PaymentHandler paymentHandler;
    @In Bill bill;

    public String schedulePayment()
    {
        Timer timer = paymentHandler.processScheduledPayment( new Payment(bill), bill.getDueDate() );
        return "success";
    }
}

异步方法不能返回任何其它值给调用者。

18.1.2. 包含Quartz Dispatcher的异步方法

Quartz dispatcher(它的安装方法请见前文)允许你使用 @Asynchronous@Duration@Expiration@IntervalDuration 注解。但它还有一些其他的强大功能。Quartz dispatcher还支持三种新注解。

@FinalExpiration 注解指定一个重现任务的终止日期。

    // Defines the method in the "processor" component
    @Asynchronous
    public QuartzTriggerHandle schedulePayment(@Expiration Date when,
                                 @IntervalDuration Long interval,
                                 @FinalExpiration Date endDate,
                                 Payment payment)
    {
        // do the repeating or long running task until endDate
    }

    ... ...

    // Schedule the task in the business logic processing code
    // Starts now, repeats every hour, and ends on May 10th, 2010
    Calendar cal = Calendar.getInstance ();
    cal.set (2010, Calendar.MAY, 10);
    processor.schedulePayment(new Date(), 60*60*1000, cal.getTime(), payment);

注意该方法返回 QuartzTriggerHandle 对象,你以后可以用它来中止、暂停和恢复定时器。 QuartzTriggerHandle 对象是可序列化的,因此,如果你需要保留更久一点,可以把它存到数据库中。

QuartzTriggerHandle handle =
         processor.schedulePayment(payment.getPaymentDate(),
                                   payment.getPaymentCron(),
                                   payment);
        payment.setQuartzTriggerHandle( handle );
        // Save payment to DB

        // later ...

        // Retrieve payment from DB
        // Cancel the remaining scheduled tasks
        payment.getQuartzTriggerHandle().cancel();

@IntervalCron 注解支持Unix cron语法的任务调度。例如,下面的异步方法在三月份每周三的2:10pm和2:44pm运行。

    // Define the method
    @Asynchronous
    public QuartzTriggerHandle schedulePayment(@Expiration Date when,
                                 @IntervalCron String cron,
                                 Payment payment)
    {
        // do the repeating or long running task
    }

    ... ...

    // Schedule the task in the business logic processing code
    QuartzTriggerHandle handle =
      processor.schedulePayment(new Date(), "0 10,44 14 ? 3 WED", payment);

@IntervalBusinessDay 注解支持在”第n个Business Day“调用。 例如,下面的异步方法在每个月的第2个business day的14:00运行。 默认时,它从business day中排除了2010年之前的所有周末和米国联邦假期。

    // Define the method
    @Asynchronous
    public QuartzTriggerHandle schedulePayment(@Expiration Date when,
                                 @IntervalBusinessDay NthBusinessDay nth,
                                 Payment payment)
    {
        // do the repeating or long running task
    }

    ... ...

    // Schedule the task in the business logic processing code
    QuartzTriggerHandle handle =
      processor.schedulePayment(new Date(),
          new NthBusinessDay(2, "14:00", WEEKLY), payment);

NthBusinessDay 对象包含调用触发器的配置。 你可以通过 additionalHolidays 属性指定更多的假期(例如,公司假期、非美国的假期等等。)

public class NthBusinessDay implements Serializable
{
      int n;
      String fireAtTime;
      List <Date> additionalHolidays;
      BusinessDayIntervalType interval;
      boolean excludeWeekends;
      boolean excludeUsFederalHolidays;

      public enum BusinessDayIntervalType { WEEKLY, MONTHLY, YEARLY }

      public NthBusinessDay ()
      {
        n = 1;
        fireAtTime = "12:00";
        additionalHolidays = new ArrayList <Date> ();
        interval = BusinessDayIntervalType.WEEKLY;
        excludeWeekends = true;
        excludeUsFederalHolidays = true;
      }
      ... ...
}

@IntervalDuration@IntervalCron@IntervalNthBusinessDay 注解相互排斥。 如果把它们用在同一个方法中,就会抛出 RuntimeException

18.1.3. 异步事件

组件驱动的事件也可以是异步的。 为了给异步处理提出事件,只要调用 Events 类的 raiseAsynchronousEvent() 方法就可以了。 要安排一个定时的事件,要调用 raiseTimedEvent() 的一个方法,并传递一个 schedule 对象 (对于默认的dispatcher或者定时服务dispatcher,要使用 TimerSchedule)。 组件可以用正常方式观察异步事件,但是要记住,只有业务处理上下文才被传播到异步线程上。

18.2. Seam中的消息

Seam让JMS消息发送到Seam组件和从Seam组件接收变得很容易。

18.2.1. 配置

为了给发送JMS消息配置Seam的基础结构,你需要告诉Seam关于任何你想发送消息到的主题(Topic)和队列(Queue),并且也要告诉Seam到哪里寻找 QueueConnectionFactory 和/或 TopicConnectionFactory

Seam默认使用 UIL2ConnectionFactory,它是使用JBossMQ时常用的连接工厂。 如果你正使用其他的JMS提供者,就需要在 seam.propertiesweb.xmlcomponents.xml 文件中设置一个或两个 queueConnection.queueConnectionFactoryJndiNametopicConnection.topicConnectionFactoryJndiName

你也需要在 components.xml 文件中列出主题(Topic)和队列(Queue),来安装Seam受控的 TopicPublisherQueueSender

<jms:managed-topic-publisher name="stockTickerPublisher" auto-create="true" topic-jndi-name="topic/stockTickerTopic"/>

<jms:managed-queue-sender name="paymentQueueSender" auto-create="true" queue-jndi-name="queue/paymentQueue"/>

18.2.2. 发送消息

现在,你可以注入一个JMS TopicPublisherTopicSession 到任何组件里:

@In
private TopicPublisher stockTickerPublisher;
@In
private TopicSession topicSession;

public void publish(StockPrice price) {
      try
      {
         stockTickerPublisher.publish( topicSession.createObjectMessage(price) );
      }
      catch (Exception ex)
      {
         throw new RuntimeException(ex);
      }
}

或用来同Queue一起使用

@In
private QueueSender paymentQueueSender;
@In
private QueueSession queueSession;

public void publish(Payment payment) {
      try
      {
         paymentQueueSender.send( queueSession.createObjectMessage(payment) );
      }
      catch (Exception ex)
      {
         throw new RuntimeException(ex);
      }
}

18.2.3. 利用消息驱动Bean接收消息

你可以利用任何EJB3消息驱动Bean来处理消息。 消息驱动Bean甚至可以是Seam组件,在这种情况下,它可能注入其他事件和应用程序作用域的Seam组件。

18.2.4. 在客户端接收消息

Seam Remoting允许你在客户端的JavaScript代码中订阅JMS主题(Topic)。这个在下一章里讲述。

Chapter 19. 缓存

数据库成为了大多数企业应用的主要瓶颈,也成为了运行环境中最不具伸缩性的层。PHP/Ruby的用户会说什么都不共享(share nothing)的架构照样具有很好的伸缩性。从表面上看也许是对的,可惜我不知道是否存在这样的多用户应用,其实现是能够在集群的不同结点间不共享资源。这些傻瓜真正想的是“除了数据库以外什么都不共享(Share nothing except for the database)”的架构。当然,共享数据库是多用户应用伸缩性的主要问题——因此声称这样的架构具有高伸缩性是荒谬的,你可要知道它们花费了这些人的大部分时间。

通常,几乎所有通过共享数据库做的事情并不值得这样去做。

这就是缓存(Cache)产生的原因。嗯,当然并不只是一个缓存。一个设计良好的Seam应用将具有丰富的多层缓存策略,这也影响着应用的每一层:

  • 当然,数据库有它自己的缓存,这是超级重要的,但是它不能像应用层的缓存一样具有伸缩性。

  • 对从数据库提取出的数据,你的ORM解决方案(Hibernate,或者别的JPA实现)具有两级缓存。这是一种很强大的能力,但是经常被误用。在一个集群环境里,保持缓存中的数据在整个集群中具有事务一致性,并且和数据库一致,其代价是相当昂贵的。这对于共享在多个用户间,且很少被更新的数据最有意义。在传统的无状态架构里,人们经常使用二级缓存来保存会话状态。这种做法总是糟糕的,在Seam中更是大错特错的。

  • Seam会话上下文是会话状态的缓存。存储于会话上下文中的组件可以保持并缓存与当前用户交互相关的状态。

  • 特别的,Seam管理的持久化上下文(或者一个扩展受管EJB容器持久化上下文,它与会话范围的无状态会话Bean相关)成为了当前会话中数据的缓存。这种缓存趋向于拥有一个相当高的命中率!Seam优化了集群环境中受管Seam持久化上下文的复制,也不需要保证数据库事务的一致性(乐观锁已足够),因此你不必担心这种缓存的性能问题,除非你把成千上万个对象读取到一个单独的持久化上下文中。

  • 应用可以在Seam应用上下文中缓存非事务性状态。相应的,保存在应用上下文中的状态不能被集群中其它结点访问。

  • 应用通过Seam的 pojoCache 组件可以缓存事务性状态,这个组件把JBossCache集成到了Seam环境中。如果你在集群模式下运行了JBossCache,那么这个状态是可以被别的结点访问的。

  • 最后,Seam让你能够缓存生成的JSF页面的部分内容(rendered fragments)。与ORM的二级缓存不一样的是,当数据发生变化时,这种缓存不能自动的失效,因此你需要写应用代码来使它显式的失效,或者设置适当的过期策略。

如要获得更多关于二级缓存的信息,你可以参考你的ORM解决方案的文档,因为这是个极为复杂的话题。在这节中我们会直接讨论通过 pojoCache 组件使用JBossCache,或者通过 <s:cache>控制充当页片段(page fragment)缓存。

19.1. 在Seam中使用JBossCache

内建的 pojoCache 组件用来管理 org.jboss.cache.aop.PojoCache 实例。你可以安全的把任何不可变的Java对象保存在缓存里,然后它将通过集群被复制(假设打开了复制)。如果你想在缓存里保存可变的Java对象,你需要运行JBossCache字节码预处理器来确保这些对象的变化能够自动的被探测到并被复制。

为了使用 pojoCache,你需要做的就是把JBossCache jar文件放在classpath中,并提供一个名为 treecache.xml 的文件,它应该有一个合适的cache配置。JBossCache有很多让人觉得恐怖和迷惑的配置,因此我们不在这里讨论它们。请参考JBossCache文档来获得更多信息。

examples/blog/resources/treecache.xml 里有一个 treecache.xml 的范例。

对Seam的EAR部署,我们推荐把JBossCache jar文件和配置文件直接放到EAR里。确保你在EAR的lib目录里放置了 jboss-cache.jarjgroups.jar

现在你可以把缓存注入到任何Seam组件:

@Name("chatroom")
public class Chatroom {
    @In PojoCache pojoCache;

    public void join(String username) {
      try
      {
         Set<String> userList = (Set<String>) pojoCache.get("chatroom", "userList");
         if (userList==null)
         {
            userList = new HashSet<String>();
            pojoCache.put("chatroom", "userList", userList);
         }
         userList.put(username);
      }
      catch (CacheException ce)
      {
         throw new RuntimeException(ce);
      }
    }
}

如果你想在应用中使用多个JBossCache配置,需要使用 components.xml:

<core:pojo-cache name="myCache" cfg-resource-name="myown/cache.xml"/>

19.2. 页片段缓存

使用JBossCache最有趣的是 <s:cache> 标签,Seam用它来解决JSF的页片段缓存问题。<s:cache> 内部使用了 pojoCache,因此你在使用它时需要遵循上面列出的步骤。(把jar文件放到EAR里,并进行令人恐怖的配置选项等。)

<s:cache>被用来缓存一些不太变化的内容。例如我们blog欢迎页面显示的近期blog:

<s:cache key="recentEntries-#{blog.id}" region="welcomePageFragments">
   <h:dataTable value="#{blog.recentEntries}" var="blogEntry">
      <h:column>
         <h3>#{blogEntry.title}</h3>
         <div>
            <s:formattedText value="#{blogEntry.body}"/>
         </div>
      </h:column>
   </h:dataTable>
</s:cache>

key使得每个页片段拥有多个缓存版本。在这种情形下,每个blog拥有一个缓存版本。region 决定了所有版本都将保存在哪个JBossCache结点里。不同结点可能有不同的过期策略。(这是你设置上述令人恐怖的配置选项做的事情。)

当然,<s:cache>的一个重要问题是它无法知道数据的变化(例如当博客作者发布一个新实体)。因此你需要自己管理被缓存的片段。

public void post() {
    ...
    entityManager.persist(blogEntry);
    pojoCache.remove("welcomePageFragments", "recentEntries-" + blog.getId() );
}

另一种方法是,如果不必把数据的变化立刻反馈给用户,那么你可以在JbossCache结点上设置一个短的过期时间。

Chapter 20. Web Services

Seam与JBossWS整合,有助于标准的JEE Web Services完全利用Seam上下文框架的优势,包括支持对话的Web Services。 本章概述帮助Web Services在Sema环境内部运行所需要的步骤。

20.1. 配置和打包

为了允许Seam拦截Web Service请求,以便能够为该请求创建必要的Seam上下文,必须配置一种特殊的SOAP处理器; org.jboss.seam.webservice.SOAPRequesHandler 是一种 SOAPHandler 实现,它完成在一个Web Service请求范围期间管理Seam生命周期的工作。 将一个项目配置为使用这个处理器的最容易方法是,将一个称作 standard-jaxws-endpoint-config.xml 的文件放到包含Web Service类的 jar 文件的 META-INF 路径下。 这个文件包含下列SOAP处理器配置:

<jaxws-config xmlns="urn:jboss:jaxws-config:2.0" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xmlns:javaee="http://java.sun.com/xml/ns/javaee"
              xsi:schemaLocation="urn:jboss:jaxws-config:2.0 jaxws-config_2_0.xsd">
   <endpoint-config>
      <config-name>Seam WebService Endpoint</config-name>
      <pre-handler-chains>
         <javaee:handler-chain>
            <javaee:protocol-bindings>##SOAP11_HTTP</javaee:protocol-bindings>
            <javaee:handler>
               <javaee:handler-name>SOAP Request Handler</javaee:handler-name>
               <javaee:handler-class>org.jboss.seam.webservice.SOAPRequestHandler</javaee:handler-class>
            </javaee:handler>
         </javaee:handler-chain>
      </pre-handler-chains>
   </endpoint-config>
</jaxws-config>

20.2. 对话的Web Services

那么对话在Web Service请求之间如何传播呢? Seam使用一个在SOAP请求和响应消息中都有的SOAP标头元素,将对话ID从消费者传输到服务,并再传回来。 下面是一个包含对话ID的Web Service请求的例子:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:seam="http://seambay.example.seam.jboss.org/">
  <soapenv:Header>
    <seam:conversationId xmlns:seam='http://www.jboss.org/seam/webservice'>2</seam:conversationId>
  </soapenv:Header>
  <soapenv:Body>
    <seam:confirmAuction/>
  </soapenv:Body>
</soapenv:Envelope>    
    

就如你在上面的SOAP消息中所见,在包含请求的对话ID(在这个例子中是2)的SOAP头内部,有一个 conversationId 元素,遗憾的是,由于Web Services可能被以不同语言编写的不同Web Service客户端消费,因此在准备用于单个对话范围之内的单独Web Services之间实现对话ID传播,由开发人员决定。

要注意的一件重要事情是,conversationId 头元素必须满足 http://www.jboss.org/seam/webservice 的命名空间条件,否则Seam将无法从请求中读取对话ID。以下是对上述请求消息响应的一个例子:

<env:Envelope xmlns:env='http://schemas.xmlsoap.org/soap/envelope/'>
  <env:Header>
    <seam:conversationId xmlns:seam='http://www.jboss.org/seam/webservice'>2</seam:conversationId>
  </env:Header>
  <env:Body>
    <confirmAuctionResponse xmlns="http://seambay.example.seam.jboss.org/"/>
  </env:Body>
</env:Envelope>    
    

如你所见,响应消息包含与请求相同的 conversationId 元素。

20.2.1. 建议策略

因为Web Services必须被实现为一个无状态的会话Bean或者POJO,对于对话的Web Services,建议用Web Service充当一个对话Seam组件的外观(facade)。

如果Web Service是作为无状态会话Bean编写的,那它也可能通过提供一个 @Name,把它变成一个Seam组件。 这么做允许Seam的双向注入(和其它)特性在Web Service类自身中使用。

20.3. Web Servic范例

让我们浏览一个Web Service的例子。 本节中的代码全部来自Seam的 /examples 目录下的seamBay范例应用程序,并遵循前一节中所述的建议策略。 让我们先来看看Web Service类和它的其中一个Web Service方法:

@Stateless
@WebService(name = "AuctionService", serviceName = "AuctionService")
public class AuctionService implements AuctionServiceRemote
{           
   @WebMethod
   public boolean login(String username, String password)
   {
      Identity.instance().setUsername(username);
      Identity.instance().setPassword(password);
      Identity.instance().login();
      return Identity.instance().isLoggedIn();
   }
   
   // snip
}

如你所见,我们的Web Service是一个无状态的会话Bean,并如JSR-181所规定的,利用 javax.jws 包中的JWS注解进行注解。 @WebService 注解告诉容器,这个类实现一个Web Service,且 login() 方法中的 @WebMethod 注解将该方法当成是一个Web Service方法。 @WebService 注解中的 nameserviceName 属性是可选的。

根据规范要求,要暴露作为Web Service方法的每个方法也都必须在Web Service类的远程接口进行声明(当Web Service是一个无状态会话Bean的时候)。 在上述例子中,AuctionServiceRemote 接口必须声明 login() 方法,实际上就是注解 @WebMethod

就如你可以在上述代码中所见,Web Service实现一个委托给Seam内建的 Identity 组件的 login() 方法。 为了与我们的建议策略一致,Web Service是作为一个简单的外观编写的,将实际的工作委托给Seam组件。 这样有助于最好地在Web Services和其他客户端之间重用业务逻辑。

让我们看另一个例子。这个Web Service方法通过委托给 AuctionAction.createAuction() 方法,开始了一个新对话。

   @WebMethod
   public void createAuction(String title, String description, int categoryId)
   {
      AuctionAction action = (AuctionAction) Component.getInstance(AuctionAction.class, true);
      action.createAuction();
      action.setDetails(title, description, categoryId);
   }

下面是来自 AuctionAction 的代码:

   @Begin
   public void createAuction()
   {
      auction = new Auction();
      auction.setAccount(authenticatedAccount);
      auction.setStatus(Auction.STATUS_UNLISTED);        
      durationDays = DEFAULT_AUCTION_DURATION;
   }

从中我们可以看到,Web Services是如何通过充当外观,并将实际的工作委托给一个对话的Seam组件,来参与长运行对话的。

Chapter 21. Remoting

Seam使用AJAX来为Web页面远程访问组件提供便捷方法。使用该框架几乎不需要预先的开发准备 —— 你只需要在组件中增加简单的注解,就可以通过AJAX来访问你的组件了。本章描述了建立一个支持AJAX的Web页面所必须的步骤,然后用更多细节继续解释Seam Remoting框架的特性。

21.1. 配置

要使用Remoting,必须先在 web.xml 文件中配置Seam Resource Servlet。

<servlet>
  <servlet-name>Seam Resource Servlet</servlet-name>
  <servlet-class>org.jboss.seam.servlet.SeamResourceServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>Seam Resource Servlet</servlet-name>
  <url-pattern>/seam/resource/*</url-pattern>
</servlet-mapping>

接下来是在Web页面引入必要的JavaScript。至少有两段脚本必须被引入。第一段脚本包含了支持远程功能的所有客户端框架代码。

<script type="text/javascript" src="seam/resource/remoting/resource/remote.js"></script>

第二段脚本包含了你希望调用的组件的存根和类型定义。它是根据组件的本地接口动态生成的,同时也包含了调用远程接口的方法时所用到的全部类的类型定义。 脚本中的名称反射到组件的名称。例如,如果有一个标有 @Name("customerAction") 的无状态会话Bean,那么脚本标签应该类似于此:

<script type="text/javascript"
          src="seam/resource/remoting/interface.js?customerAction"></script>

如果想在一个页面上访问多个组件,则需要把它们全部作为脚本标签的参数:

<script type="text/javascript"
        src="seam/resource/remoting/interface.js?customerAction&accountAction"></script>

或者,你也可以使用 s:remote 标签来引入这些必要的JavaScript,注意要通过逗号来分隔每个组件或者类的名称。

  <s:remote include="customerAction,accountAction"/>
    

21.2. Seam对象

客户端通过 Seam JavaScript对象与你的组件进行交互。 这个JavaScript对象在 remote.js 中定义,你将一直使用它来异步调用你的组件。 它被划分成两个功能域:Seam.Component 包含了与组件一起工作的方法,Seam.Remoting 包含了执行远程请求的方法。 熟悉这个对象的最简单方法是从一个简单的例子开始。

21.2.1. Hello World示例

让我们从这个简单的示例中逐步弄清楚 Seam 对象是怎样工作的。首先,我们创建一个名为 helloAction 的新的Seam组件。

@Stateless
@Name("helloAction")
public class HelloAction implements HelloLocal {
    public String sayHello(String name) {
        return "Hello, " + name;
    }
}

同时需要为这个新组件创建一个本地接口 —— 特别要注意 @WebRemote 注解,因为它是能远程访问我们方法所必须的。

@Local
public interface HelloLocal {
  @WebRemote
  public String sayHello(String name);
}

这是我们所编写的所有服务器端代码。接下来就是我们的Web页面 —— 创建一个新的页面然后引入以下脚本:

<s:remote include="helloAction"/>

为了完成一个完整的用户交互体验,我们增加一个按钮:

<button onclick="javascript:sayHello()">Say Hello</button>

同时,我们还需要增加更多脚本以使得按钮在被点击后真正能够做出相应的反应:

<script type="text/javascript">
  //<![CDATA[

  function sayHello() {
    var name = prompt("What is your name?");
    Seam.Component.getInstance("helloAction").sayHello(name, sayHelloCallback);
  }

  function sayHelloCallback(result) {
    alert(result);
  }

   // ]]>
</script>

到此已经全部完成了!部署这个应用程序并且浏览页面,点击按钮,按照提示输入一个名字,会出现一个hello消息框,这个消息框的出现表明了这次调用是成功的。 如果你想节省时间,你可以从Seam的 /examples/remoting/helloworld 目录中找到这个Hello World示例的所有源代码。

那我们的脚本到底做了什么呢?我们把它分解成更小的部分。 开始你可以从列出的JavaScript代码中看到我们已经实现了两个方法 —— 第一个方法负责提示用户输入姓名,然后产生一个远程请求。看一下下面这行:

Seam.Component.getInstance("helloAction").sayHello(name, sayHelloCallback);

这一行的第一部分 Seam.Component.getInstance("helloAction"),返回了一个代理,或者 helloAction 组件的存根。 我们可以通过这个存根调用组件的方法,这也是剩余部分即 sayHello(name, sayHelloCallback); 所做的事情。

这整行的代码是在调用我们组件的 sayHello 方法,并且传进来 name 作为参数。 第二个参数 sayHelloCallback 并不是我们组件的 sayHello 方法的参数, 相反它告诉Seam Remoting框架一旦它收到与请求对应的响应,则要把这个响应传递到JavaScript脚本的 sayHelloCallback 方法。 这个回调参数是完全可选的,因此当你调用一个返回值为 void 方法或者你不关心结果时,你可以不使用这个参数。

一旦 sayHelloCallback 方法收到了远程请求的响应,就会弹出一个警告消息,以显示方法调用的结果。

21.2.2. Seam.Component

Seam.Component JavaScript对象提供了许多客户端方法来与Seam组件一起工作, 它的两个主要方法是 newInstance()getInstance(),将在下面的小节中讲解, 它们的主要区别是 newInstance() 总是创建一个组件类型的新实例,而 getInstance() 却返回一个单例的实例。

21.2.2.1. Seam.Component.newInstance()

使用newInstance()方法来创建一个实体或者JavaBean组件的一个新实例。 这个方法返回的对象将含有相同的获取/设置方法作为服务器端组成部分,或者你可以直接访问它的字段。以下面的Seam实体组件为例:

@Name("customer")
@Entity
public class Customer implements Serializable
{
  private Integer customerId;
  private String firstName;
  private String lastName;

  @Column public Integer getCustomerId() {
    return customerId;
  }

  public void setCustomerId(Integer customerId} {
    this.customerId = customerId;
  }

  @Column public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  @Column public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

为了创建一个客户端Customer对象,你要编写如下代码:

var customer = Seam.Component.newInstance("customer");

然后从这里起,你可以设置customer对象的字段:

customer.setFirstName("John");
// Or you can set the fields directly
customer.lastName = "Smith";

21.2.2.2. Seam.Component.getInstance()

使用 getInstance() 方法来获得一个Seam会话Bean组件的存根的引用,这个存根可以用来远程执行组件的方法。 这个方法返回特定组件的单例,因此使用同样的组件名调用它两次也将返回该组件的同一个实例。

接着看我们先前的例子,如果我们创建了一个新的 customer,同时我们想保存它, 我们需要把它传递给 customerAction 组件的 saveCustomer() 方法:

Seam.Component.getInstance("customerAction").saveCustomer(customer);

21.2.2.3. Seam.Component.getComponentName()

把一个对象传递到该方法,如果对象是组件则将返回它的组件名,否则将返回 null

if (Seam.Component.getComponentName(instance) == "customer")
  alert("Customer");
else if (Seam.Component.getComponentName(instance) == "staff")
  alert("Staff member");

21.2.3. Seam.Remoting

Seam Remoting相关的大多数客户端功能都包含在 Seam.Remoting 对象中。 你不必直接调用它的大多数方法,但是非常有必要解释一下两个非常重要的方法。

21.2.3.1. Seam.Remoting.createType()

如果你的应用程序包含或者使用了不是Seam组件的JavaBean类,那么你就需要在客户端创建这些类型并把它们作为参数传递到组件的方法中。 使用 createType() 方法来创建你所需类型的实例,并以完整限定的Java类名传进来作为参数:

var widget = Seam.Remoting.createType("com.acme.widgets.MyWidget");

21.2.3.2. Seam.Remoting.getTypeName()

Seam.Remoting.getTypeName()Seam.Component.getComponentName() 等价,但它是针对非组件类型的。 它将返回对象实例的类型名,如果不知道类型名,则返回 null。这个名称是完整限定的Java类名。

21.3. EL表达式求值

Seam Remoting也对EL表达式求值提供了支持,即提供了另外一种便利的方法来从服务器端获取数据。 通过使用 Seam.Remoting.eval(),一个EL表达式可以在服务器端被远程求值,并且将其结果返回给客户端的回调方法。 此方法接受两个参数,第一个是要被求值的EL表达式,第二个是调用表达式值的回掉方法。示例如下:

  function customersCallback(customers) {
    for (var i = 0; i < customers.length; i++) {
      alert("Got customer: " + customers[i].getName());
    }
  }

  Seam.Remoting.eval("#{customers}", customersCallback);
    

在这个例子中,表达式 #{customers} 将被Seam求值, 并且表达式的值(此处是Customer对象列表)被返回给 customersCallback() 方法。 一定要记住,以这种方式返回的对象必须导入他们的类型(通过 s:remote)这样才能与JavaScript一起工作。 因此,为了与 customer 对象列表一起工作,它需要导入 customer 类型。

<s:remote include="customer"/>

21.4. 客户端接口

在上节的配置中,接口或组件的存根通过 seam/resource/remoting/interface.js 被导入到我们页面里:

或者使用s:remote标签

<script type="text/javascript"
        src="seam/resource/remoting/interface.js?customerAction"></script>
<s:remote include="customerAction"/>

通过在页面中包含这段脚本,组件的接口定义,及执行组件的方法所需要的任何别的组件或者类型都将被生成并且可被Seam Remoting框架所使用。

这会生成两种客户存根:可执行的存根和类型存根。可执行的存根具有一定的行为,能被用来执行会话bean组件的方法。 相反,类型存根包含状态,也表示参数或者返回值的类型。

客户存根类型的生成依赖于Seam组件的类型。如果组件是会话Bean,那么将生成可执行的存根,否则如果组件是实体或者JavaBean,那么将生成类型存根。 这里也有一个例外;如果你的组件是JavaBean(例如它既不是会话Bean也不是实体Bean)并且任何它的方法被注解为@WebRemote, 那么将生成一个可执行的存根而不是一个类型存根。这允许你在非EJB环境中使用Remoting来调用JavaBean组件而不需要访问会话Bean。

21.5. 上下文

Seam Remoting上下文包含了附加的信息,并在发送和接收中作为远程请求/响应周期的一部分。目前它只包含了对话ID,但是将来它可能被扩展。

21.5.1. 设置和读取对话ID

如果你打算在对话范围内使用远程调用,那么你需要能从Seam Remoting上下文中读取或者设置对话ID。 在发起远程请求调用 Seam.Remoting.getContext().getConversationId() 后读取对话ID。 在发起请求前通过调用 Seam.Remoting.getContext().setConversationId() 来设置对话ID。

如果对话ID没有通过 Seam.Remoting.getContext().setConversationId() 显式地进行设置, 那么它将自动地被赋值为任意远程调用返回的第一个有效的对话ID。如果你的页面有多个对话,那么你需要在每次调用之前显式地设置对话ID。 如果你只是工作于单个对话中,那么你不需要额外做任何事情。

21.5.2. 当前对话范围内的远程调用

在某些情况下,可能会要求在当前视图的对话范围内发起一个远程调用,为此,你必须要在调用之前显式地设置对话ID。 以下一小段JavaScript代码将在当前视图会话ID远程调用的时候,设置会话ID。

Seam.Remoting.getContext().setConversationId( #{conversation.id} );

21.6. 批量请求

Seam Remoting允许在单个请求中执行多个组件的调用。只要能减少网络流量,那么极力推荐使用这个特性。

Seam.Remoting.startBatch() 方法将启动一个新的批处理,启动批处理后任何组件的调用都将进入队列,而不是立刻的发送。 当所有的组件调用都被加到批处理以后,Seam.Remoting.executeBatch() 方法将发送一个包含所有调用队列的请求到服务器,服务器将顺序地执行这些调用。 当这些调用被执行之后,单个响应将返回客户端,它包含了所有的返回值,同时回调函数(如果提供的话)也将按与执行相同的顺序被触发。

如果你通过 startBatch() 方法启动了一个新的批处理方法,然后你决定不发送它, 那么你需要调用 Seam.Remoting.cancelBatch() 方法,它将丢弃任何队队中的调用并退出批处理模式。

使用批处理的例子请见 /examples/remoting/chatroom

21.7. 使用数据类型

21.7.1. 原生 / 基本 类型

这部分描述了基本数据类型的支持。一般来说,在服务器端这些值是与它们的原生类型或者相应的包装类相兼容的。

21.7.1.1. String

当设置字符串参数值时可以简单地使用Javascript字符串对象。

21.7.1.2. Number

支持Java语言支持的所有数字类型。在客户端数字值总是被序列化为字符串,在服务器端他们被转化成正确的目标类型。 进行转化时 ByteDoubleFloatIntegerLongShort 的原生类型或者包装类型都被支持。

21.7.1.3. Boolean

Boolean在客户端表示为JavaScript Boolean值,在服务器端表示为Java Boolean。

21.7.2. JavaBeans

一般来说,JavaBean一般是Seam实体或者JavaBean组件,或者其他别的非组件类。 需要使用适当的方法(Seam组件使用 Seam.Component.newInstance(),其它使用 Seam.Remoting.createType())来创建对象的新实例。

注意只有这两个方法创建的对象才能被用作参数值,这些参数值不是本节所提到的有效类型之一。在不能确定参数类型的情况下,你可以使用如下的组件方法:

@Name("myAction")
public class MyAction implements MyActionLocal {
  public void doSomethingWithObject(Object obj) {
    // code
  }
}

在这个示例中你可能要传进 myWidget 组件的一个实例,然而 myAction 接口并没有包含 myWidget,因为它并没有被它的任何方法直接引用。 为了能够把参数传进来,需要显式地引入 MyWidget

<s:remote include="myAction,myWidget"/>

这允许使用 Seam.Component.newInstance("myWidget") 创建 myWidget 对象,并允许创建后的对象传递到 myAction.doSomethingWithObject() 中。

21.7.3. Date和Time

日期值被序列化成字符串表示,并且精确到毫秒。在客户端,使用JavaScript日期对象来使用日期值。 在服务器端,使用 java.util.Date 类(或者派生类,如 java.sql.Datejava.sql.Timestamp)。

21.7.4. Enums 枚举类型

在客户端,Enum也被作为String处理。当为Enum参数设置值时,简单地使用Enum的字符串表示就行了。以下面的组件为例:

@Name("paintAction")
public class paintAction implements paintLocal {
  public enum Color {red, green, blue, yellow, orange, purple};

  public void paint(Color color) {
    // code
  }
} 

为了调用 paint() 方法,并且传递给参数color的值是 red,只要把red作为字符串传入就可以了:

Seam.Component.getInstance("paintAction").paint("red");

反过来也是成立的 —— 也就是说,如果一个组件方法返回Enum型参数(或者返回的对象图里包含一个Enum字段),那么在客户端仍将被表示为一个字符串。

21.7.5. Collections 集合

21.7.5.1. Bags

Bags囊括了所有的集合类型,包含arrays、collections、lists、sets,(但不包含Maps —— 见下一章),它在客户端的实现是JavaScript array。 当调用一个接收上述类型为参数的组件方法时,你的参数应该是JavaScript array。如果一个组件方法返回上述类型之一,那么返回值将是JavaScript array。 发生组件方法调用时,在服务器端Seam Remoting框架能够非常聪明地把Bag类型转化为适当的类型。

21.7.5.2. Maps

JavaScript并没有对Map的本地支持,Seam Remoting框架支持简单的Map实现。 通过创建 Seam.Remoting.Map对象以支持把Map用做远程调用的参数。

var map = new Seam.Remoting.Map();

这个Javascript实现提供了处理Map的基本方法:size()isEmpty()keySet()values()get(key)put(key, value)remove(key)contains(key)。 每一个方法等同于Java中对应的方法。在Java中一些方法将返回集合对象,例如 keySet()values(), 相应地,在JavaScript中包含key或者value对象的JavaScript Array对象也将被返回。

21.8. 调试

为了能够跟踪Bug,可以启用调试模式,在调试模式下,所有在客户端和服务器端发出和返回的数据包的内容都被显示在一个弹出窗口中。 为了启用调试模式,或者在JavaScript脚本中执行 setDebug() 方法:

Seam.Remoting.setDebug(true);

或者通过components.xml配置它:

<remoting:remoting debug="true"/>

如果要关闭调试模式,则需要调用 setDebug(false)。 如果你要在调试日志中记录一些自己定义的信息,那需要调用 Seam.Remoting.log(message)

21.9. 加载消息

默认加载消息显示在屏幕的右上角,并且是可以修改的,它的表现形式可以自定义甚至可以关掉。

21.9.1. 修改信息

如果要把默认的“Please Wait...”消息改成其它内容,则需要设置 Seam.Remoting.loadingMessage 的值:

Seam.Remoting.loadingMessage = "Loading..."; 

21.9.2. 隐藏加载信息

如果要尽可能少的显示加载消息,可以通过覆写 displayLoadingMessage()hideLoadingMessage() 的实现为反之不显示任何消息的函数:

// don't display the loading indicator
Seam.Remoting.displayLoadingMessage = function() {};
Seam.Remoting.hideLoadingMessage = function() {};

21.9.3. 自定义加载指示器

如果你需要覆写加载指示器以显示一个动画图标或者其他东西,那么你需要覆写 displayLoadingMessage()hideLoadingMessage()

  Seam.Remoting.displayLoadingMessage = function() {
    // Write code here to display the indicator
  };

  Seam.Remoting.hideLoadingMessage = function() {
    // Write code here to hide the indicator
  };

21.10. 控制返回数据

当远程方法被执行后,执行结果被序列化成一个XML响应并返回客户端。这个响应被客户端反射成一个JavaScript对象。 对于复杂的类型(例如Javabean),它们包含了其他对象的引用,所有这些被引用的对象也将被序列化为这个响应的一部分。 这些被引用的对象可能又引用了其他对象,同时还可能存在更深层次的引用关系。 如果不检查,这个对象图可能是巨大的,具体取决于对象间的关系。 作为一个次要的问题(除了响应可能很冗长之外),你可能也不希望把敏感的信息暴露给客户端。

Seam Remoting提供了一个简单的方式来限制对象图,即指定远程方法的 @WebRemote 注解中的 exclude 字段。 这个字段接受包含用.号指定的一个或多个路径的字符串数组。当调用一个远程方法时,执行结果的对象图中的对象如果与这些路径匹配,就将被从序列化结果包中去掉。

我们使用下面的 Widget 类来说明整个示例:

@Name("widget")
public class Widget
{
  private String value;
  private String secret;
  private Widget child;
  private Map<String,Widget> widgetMap;
  private List<Widget> widgetList;

  // getters and setters for all fields
}

21.10.1. 一般字段的约束

如果远程方法返回 Widget 实例,但你不想暴露 secret 字段,因为它包含一些敏感信息,你可以用如下的方式限制它: