本教程假定你已下载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 文件中。
第一步,确保已安装Ant,并正确设定了 $ANT_HOME 及 $JAVA_HOME 的环境变量。接着在Seam的根目录下的 build.properties 文件中正确设定JBoss AS 4.0.5的安装路径。 若一切就绪,就可在JBoss的安装根目录下敲入 bin/run.sh 或 bin/run.bat 命令来启动JBoss AS。(译注:此外,请安装JDK1.5以上以便能直接运行示例代码)
现在只要在Seam安装目录 examples/registration 下输入 ant deploy 就可构建和部署示例了。
试着在浏览器中访问此链接:http://localhost:8080/seam-registration/。
首先,确保已安装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容器环境中运行。
注册示例是个极其普通的应用,它可让新用户在数据库中保存自己的用户名,真实的姓名及密码。 此示例并不想一下子就把Seam的所有的酷功能全部秀出。然而, 它演示了EJB3 会话Bean作为JSF动作监听器及Seam的基本配置的使用方法。
或许你对EJB 3.0还不太熟悉,因此我们会对示例的慢慢深入说明。
此示例的首页显示了一个非常简单的表单,它有三个输入字段。试着在表单上填写内容并提交,一旦输入数据被提交后就会在数据库中保存一个user对象。
本示例由两个JSP页面,一个实体Bean及无状态的会话Bean来实现。
让我们看一下代码,就从最“底层”的实体Bean开始吧。
我们需要EJB 实体Bean来保存用户数据。这个类通过注解声明性地定义了 persistence 及 validation 属性。它也需要一些额外的注解来将这个类定义为Seam的组件。
Example 1.1.
@Entity @Name("user") @Scope(SESSION) @Table(name="users") public class User implements Serializable { private static final long serialVersionUID = 1881413500711441951L; private String username; 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() {} @NotNull @Length(min=5, max=15) 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) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
EJB3标准注解 @Entity 表明了 User 类是个实体Bean. | |
Seam组件需要一个 组件名称,此名称由注解 @Name来指定。此名称必须在Seam应用内唯一。当JSF用一个与组件同名的名称去请求Seam来解析上下文变量, 且该上下文变量尚未定义(null)时,Seam就将实例化那个组件,并将新实例绑定给上下文变量。 在此例中,Seam将在JSF第一次遇到名为 user 的变量时实例化 User。 | |
每当Seam实例化一个组件时,它就将始化后的实例绑定给组件中 默认上下文 的上下文变量。默认的上下文由 @Scope注解指定。 User Bean是个会话作用域的组件。 | |
EJB标准注解@Table 表明了将 User 类映射到 users 表上。 | |
name、 password 及 username 都是实体Bean的持久化属性。所有的持久化属性都定义了访问方法。当JSF渲染输出及更新模型值阶段时需要调用该组件的这些方法。 | |
EJB和Seam都要求有空的构造器。 | |
@NotNull 和 @Length 注解是Hibernate Validator框架的组成部份, Seam集成了Hibernate Validator并让你用它来作为数据校验(尽管你可能并不使用Hibernate作为持久化层)。 | |
标准EJB注解 @Id 表明了实体Bean的主键属性。 |
这个例子中最值得注意的是 @Name 和 @Scope 注解,它们确立了这个类是Seam的组件。
接下来我们将看到 User 类字段在更新模型值阶段时直接被绑定给JSF组件并由JSF操作, 在此并不需要冗余的胶水代码来在JSP页面与实体Bean域模型间来回拷贝数据。
然而,实体Bean不应该进行事务管理或数据库访问。故此,我们无法将此组件作为JSF动作监听器,因而需要会话Bean。
在Seam应用中大都采用会话Bean来作为JSF动作监听器(当然我们也可选择JavaBean)。
在我们的应用程序中确实存在一个JSF动作和一个会话Bean方法。在此示例中,只有一个JSF动作,并且我们使用会话Bean方法与之相关联并使用无状态Bean,这是由于所有与动作相关的状态都保存在 User Bean中。
这是示例中比较有趣的代码部份:
Example 1.2.
@Stateless @Name("register") public class RegisterAction implements Register { @In private User user; @PersistenceContext private EntityManager em; @Logger private Log log; public String register() { List existing = em.createQuery( "select username from User where username=#{user.username}") .getResultList(); if (existing.size()==0) { em.persist(user); log.info("Registered new user #{user.username}"); return "/registered.jsp"; } else { FacesMessages.instance().add("User #{user.username} already exists"); return null; } } }
EJB标准注解 @Stateless 将这个类标记为无状态的会话Bean。 | |
注解 @In将Bean的一个属性标记为由Seam来注入。 在此例中,此属性由名为 user 的上下文变量注入(实例的变量名)。 | |
EJB标准注解 @PersistenceContext 用来注入EJB实体管理器。 | |
Seam的 @Logger 注解用来注入组件的 Log 实例。 | |
动作监听器方法使用标准的EJB3 EntityManager API来与数据库交互,并返回JSF的输出结果。 请注意,由于这是个会话Bean,因此当 register() 方法被调用时事务就会自动开始,并在结束时提交(commit)。 | |
请注意Seam让你在EJB-QL中使用JSF EL表达式。因此可在标准JPA Query 对象上调用普通的JPA setParameter() 方法,这样岂不妙哉? | |
Log API为显示模板化的日志消息提供了便利。 | |
多个JSF动作监听器方法返回一个字符串值的输出,它决定了接下来应显示的页面内容。 空输出(或返回值为空的动作监听器方法)重新显示上一页的内容。 在普通的JSF中,用JSF的导航规则(navigation rule) 来决定输出结果的JSF视图id是很常用的。 这种间接性对于复杂的应用是非常有用的,值得去实践。但是,对于象示例这样简单的的应用,Seam让你使用JSF视图id作为输出结果,以减少对导航规则的需求。请注意,当你用视图id作为输出结果时,Seam总会执行一次浏览器的重定向。 | |
Seam提供了大量的 内置组件(built-in components) 来协助解决那些经常遇到的问题。 用 FacesMessages 组件就可很容易地来显示模板化的错误或成功的消息。 内置的Seam组件还可由注入或通过调用 instance() 方法来获取。 |
这次我们并没有显式指定 @Scope,若没有显式指定时,每个Seam 组件类型就使用其默认的作用域。对于无状态的会话Bean, 其默认的作用域就是无状态的上下文。实际上 所有的 无状态的会话Bean都属于无状态的上下文。
会话Bean的动作监听器在此小应用中履行了业务和持久化逻辑。在更复杂的应用中,我们可能要将代码分层并重构持久化逻辑层成 专用数据存取组件,这很容易做到。但请注意Sean并不强制你在应用分层时使用某种特定的分层策略。
此外,也请注意我们的SessionBean会同步访问与web请求相关联的上下文(比如在 User 对象中的表单的值),状态会被保持在事务型的资源里(EntityManager 对象)。 这是对传统J2EE的体系结构的突破。再次说明,如果你习惯于传统J2EE的分层,也可以在你的Seam应用实行。但是对于许多的应用,这是明显的没有必要 。
很自然,我们的会话Bean需要一个本地接口。
所有的Java代码就这些了,现在去看一下部署描述文件。
如果你此前曾接触过许多的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.init 的 jndiPattern 属性。这里需要奇怪的@符号是因为ANT脚本会在部署应用时将正确的JNDI语法在标记处自动填补
我们将以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应用中的配置与此处的配置基本相同。
绝大多数的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代码中,所以我们甚至都不需要定义导航规则。
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>
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>
对于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毫无相干。
最后,因为我们的应用是要部署成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。
至此,我们了解了整个应用中 所有的 部署描述文件!
当提交表单时,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 实体的属性值。
在几乎所有的在线应用中都免不了将搜索结果显示成可点击的列表。 因此Sean在JSF层之上提供了特殊的功能,使得我们很容易用EJB-QL或HQL来查询数据并用JSF <h:dataTable> 将查询结果显示成可点击的列表。我们将在接下的例子中演示这一功能。
此消息示例中有一个实体Bean,Message,一个会话Bean MessageListBean 及一个JSP页面。
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; } }
如此前的例子,会话Bean MessageManagerBean 用来给表单中的两个按钮定义个动作监听器方法, 其中的一个按钮用来从列表中选择消息,并显示该消息。而另一个按钮则用来删除一条消息,除此之外,就没什么特别之处了。
在用户第一次浏览消息页面时,MessageManagerBean 会话Bean也负责抓取消息列表,考虑到用户可能以多种方式来浏览该页面,他们也有可能不是由JSF动作来完成,比如用户可能将该页加入收藏夹。 因此抓取消息列表发生在Seam的工厂方法中,而不是在动作监听器方法中。
之所以将此会话Bean设为有状态的,是因为我们想在不同的服务器请求间缓存此消息列表。
Example 1.11.
@Stateful @Scope(SESSION) @Name("messageManager") public class MessageManagerBean implements Serializable, MessageManager { @DataModel private List<Message> messageList; @DataModelSelection @Out(required=false) private Message message; @PersistenceContext(type=EXTENDED) private EntityManager em; @Factory("messageList") public void findMessages() { messageList = em.createQuery("from Message msg order by msg.datetime desc").getResultList(); } public void select() { message.setRead(true); } public void delete() { messageList.remove(message); em.remove(message); message=null; } @Remove @Destroy public void destroy() {} }
注解 @DataModel 暴露了 java.util.List 类型的属性给JSF页面来作为 javax.faces.model.DataModel 的实例。 这允许我们在JSF <h:dataTable>的每一行中能使用可点击列表。在此例中,DataModel 可在变量名为 messageList 的会话上下文中被使用。 | |
@DataModelSelection 注解告诉了Seam来注入 List 元素到相应的被点击链接。 | |
注解 @Out 直接暴露了被选中的值给页面。 这样一来,每次可点击列表一旦被选中,Message 就被会注入给有状态Bean的属性,紧接着 向外注入(outjected)给变量名为 message 的事件上下文的属性。 | |
此有状态Bean有个EJB3的 扩展持久化上下文(extended persistence context)。只要Bean存在,查询中获取的消息就会保留在受管理的状态中。 这样一来,此后对有状态Bean的所有方法调用勿需显式调用 EntityManager 就可更新这些消息了。 | |
当我们第一次浏览JSP页面时,messageList 上下文变量尚未被初始化,@Factory 注解告诉Seam来创建 MessageManagerBean 的实例并调用 findMessages() 方法来初始化上下文变量。 我们把 findMessages() 当作 messages 的 工厂方法。 | |
select() 将选中的 Message 标为已读,并同时更新数据库。 | |
delete() 动作监听器方法将选中的 Message 从数据库中删除。 | |
对于每个有状态的会话Bean,Seam组件的所有方法中 必须 有一不带参数的方法被标为 @Remove @Destroy 以确保在Seam的上下文结束时删除有状态Bean,并同时清除所有服务器端的状态。 |
请注意,这是个会话作用域的Seam组件。它与用户登入会话相关联,并且登入会话的所有请求共享同一个组件的实例。 (在Seam的应用中,我们通常使用会话作用域的组件。)
当然,每个会话Bean都有个业务接口。
@Local public interface MessageManager { public void findMessages(); public void select(); public void delete(); public void destroy(); }
从现在起,我们在示例代码中将不再对本地接口作特别的说明。
由于XML文件与此前的示例几乎都一样,因此我们略过了 components.xml、persistence.xml、 web.xml、ejb-jar.xml、faces-config.xml 及application.xml 的细节,直接来看一下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>
当我们首次浏览 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 从数据库中删除。最后,该网页重新渲染,再次显示消息列表。
jBPM提供了先进的工作流程和任务管理的功能。为了体验一下jBPM是如何与Seam集成在一起工作的,在此将给你一个简单的管理“待办事项列表”的应用。由于管理任务列表等功能是jBPM的核心功能,所以在此例中只用了很少的Java代码。
这个例子的核心是jBPM的流程定义(process definition)。此外,还有两个JSP页面和两个简单的JavaBeans(由于他们不用访问数据库,或有其它事务相关的行为,因此并没有用会话Bean)。让我们先从流程定义开始:
Example 1.13.
<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>
节点 <start-state> 代表流程的逻辑开始。一旦流程开始时,它就立即转入 todo节点。 | |
<task-node> 节点代表 等待状态,就是在执行业务流程暂停时,等待一个或多个未完成的任务。 | |
<task> 元素定义了用户需要完成的任务。 由于在这个节点只有定义了一个任务,当它完成,或恢复执行时我们就转入结束状态。 此任务从Seam中名为 todolist 的组件(JavaBeans之一)获得任务description。 | |
任务在创建时就会被分配给一个用户或一组用户时。在此示例中,任务是分配给当前用户,该用户从一个内置的名为 actor 的Seam组件中获得。任何Seam组件都可用来执行任务指派。 | |
<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() { return description; } public void setDescription(String description) { this.description = description; } @CreateProcess(definition="todo") public void createTodo() {} @StartTask @EndTask public void done() {} }
description属性从JSP页接受用户输入,并将它暴露给流程定义,这样就可让Seam来设定任务的descrption。 | |
Seam的 @CreateProcess 注解为指定名称的流程定义创建了一个新的jBPM流程实例。 | |
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配置并不是很有趣。
对有相对自由(特别)导航的Seam应用程序而言,JSF/Seam导航规则是定义页面流的一个完美的方法。 而对于那些带有更多约束的导航,特别是带状态的用户界面而言,导航规则反而使得系统流程变得难以理解。 要理解整个流程,你需要从视图页面、动作和导航规则里一点点把它拼出来。
Seam允许你使用一个jPDL流程定义来定义页面流。下面这个简单的猜数字范例将演示这一切是如何实现的。
这个例子由一个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> </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>
<page>元素定义了一个等待状态,在该状态中系统显示一个JSF视图等待用户输入。 view-id与简单JSF导航规则中的view id一样。 redirect属性告诉Seam在导航到页面时使用post-then-redirect。(这会带来友好的浏览器URL。) | |
<transition> 元素命名了一个JSF输出。当一个JSF动作导致那个输出时会触发转换。 在任何jBPM转换动作调用后,执行会进行到页面流程图的下一个节点。 | |
一个转换动作 <action> 就像JSF动作,不同的就是它只发生在一个jBPM转换发生时。 转换动作能调用任何Seam组件。 | |
<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 @Begin(pageflow="numberGuess") 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; } }
一开始,JSP页面请求一个 numberGuess 组件,Seam会为该组件创建一个新的实例,并调用 @Create 方法,允许组件初始化自己。 | |
@Begin 注解启动了一个Seam 业务会话(conversation) (稍后详细说明),并指定业务会话页面流所要使用的页面流定义。 |
如你所见,这个Seam组件是纯业务逻辑的!它不需要知道任何关于用户交互的东西。这点使得组件更易被复用。
该系统是一个完整的宾馆客房预订系统,它由下列功能组成:
用户注册
登录
注销
设置密码
搜索宾馆
选择宾馆
客房预订
预订确认
当前预订列表
应用程序中使用了JSF、EJB 3.0和Seam,视图部分结合了Facelets。也可以选择使用JSF、Facelets、Seam、JavaBeans和Hibernate3。
在使用过一段时间后你会发现该应用程序非常 健壮。你能使用回退按钮、刷新浏览器、打开多个窗口, 或者键入各种无意义的数据,会发现都很难让它崩溃。你也许会想我们花了几个星期测试修复该系统才达到了这个目标。 事实却不是这样的,Seam的设计使你能够用它方便地构建健壮的web应用程序,而且Seam还提供了很多以前需要通过编码才能实现的健壮性。
在你浏览范例程序代码研究它是如何运行时,注意观察声明式的状态管理和集成的验证是如何被用来实现这种健壮性的。
这个项目的结构和上一个一样,要安装部署该应用程序请参考Section 1.1, “试试看”。 当应用程序启动后,可以通过 http://localhost:8080/seam-booking/ 进行访问。
只需要用9个类(加上6个Session Bean的本地接口)就能实现这个应用程序。6个Session Bean动作监听器包括了以下功能的所有业务逻辑。
应用程序的持久化模型由三个实体bean实现。
我们鼓励您随意浏览源代码。在这个教程里我们将关注功能中的某一特定部分:宾馆搜索、选择、预订和确认。 从用户的角度来看,从选择宾馆到确认的每一步都是工作中的一个连续单元,属于一个 业务对话。 然而搜索却 不 是该对话的一部分。用户能在不同浏览器标签页中的相同搜索结果页面中选择多个宾馆。
大多数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 @Name("hotelSearch") @Scope(ScopeType.SESSION) @Restrict("#{identity.loggedIn}") public class HotelSearchingAction implements HotelSearching { @PersistenceContext private EntityManager em; private String searchString; private int pageSize = 10; private int page; @DataModel private List<Hotel> hotels; 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() {} }
EJB标准中的 @Stateful 注解表明这个类是一个有状态的Session Bean。它们的默认作用域是业务对话上下文。 | |
@Restrict注解给组件加上了一个安全限制。只有登录过的用户才能访问该组件。安全章节中更详细地讨论了Seam的安全问题。 | |
@DataModel 注解将一个 List 作为JSF ListDataModel 暴露出去。 这简化了搜索界面的可单击列表的实现。在这个例子中,宾馆的列表是以名为 hotels 的 ListDataModel 业务对话变量暴露给页面的。 | |
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}" reRender="searchResults" /> </h:inputText>   <a:commandButton value="Find Hotels" action="#{hotelSearch.find}" styleClass="button" reRender="searchResults"/>   <a:status> <f:facet name="start"> <h:graphicImage value="/img/spinner.gif"/> </f:facet> </a:status> <br/> <h:outputLabel for="pageSize">Maximum results:</h:outputLabel>  <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"> <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)}"/> </h:column> </h:dataTable> <s:link value="More results" action="#{hotelSearch.nextPage}" rendered="#{hotelSearch.nextPageAvailable}"/> </div> </a:outputPanel>
Ajax4JSF的 <a:support> 标签允许一个JSF动作事件监听器在类似 onkeyup 这样的JavaScript事件发生时被异步的 XMLHttpRequest 调用。 更棒的是,reRender 属性让我们可以在收到异步响应时渲染一个JSF页面的片段并执行一个页面的局部修改。 | |
Ajax4JSF的 <a:status> 标签使我们能在等待异步请求返回时显示一个简单的动画。 | |
Ajax4JSF的 <a:outputPanel> 标签定义了一块能被异步请求修改的页面区域。 | |
Seam的<s:link> 标签使我们能将一个JSF动作监听器附加在一个普通的(非JavaScript)HTML链接上。 用它取代标准JSF的 <h:commandLink> 的好处就是它在“在新窗口中打开”和“在新标签页中打开”时仍然有效。 值得注意的另一点就是我们用了一个绑定了参数的方法:#{hotelBooking.selectHotel(hot)}。 在标准的统一EL中这是不允许的,但Seam对EL的扩展进行了扩展,使表达式能够支持带参数的方法。 |
这个页面根据我们的键入动态地显示搜索结果,让我们选择一家宾馆并将它传给 HotelBookingAction 的 selectHotel() 方法,这个对象才是 真正 有趣的地方。
现在让我们来看看宾馆预定范例程序是如何使用一个对话域的有状态的Session Bean的,这个Session Bean实现了业务会话相关持久化数据的天然缓存。 下面的代码很长。但如果你把它理解为实现业务会话的多个步骤的一系列动作的话,它是不难理解的。我们把这个类当作故事一样从头开始阅读。
Example 1.24.
@Stateful @Name("hotelBooking") @Restrict("#{identity.loggedIn}") public class HotelBookingAction implements HotelBooking { @PersistenceContext(type=EXTENDED) private EntityManager em; @In 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 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 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 public void destroy() {} }
这个bean使用EJB3的 扩展持久化上下文,所以任意实体实例在整个有状态Session Bean的生命周期中一直受到管理。 | |
@Out 注解声明了一个属性值在方法调用后会 向外注入 到一个上下文变量中的。 在这个例子中,名为 hotel 的上下文变量会在每个动作监听器调用完成后被设置为 hotel 实例变量的值。 | |
@Begin 注解表明被注解的方法开始一个 长期业务对话,因此当前业务对话上下文在请求结束后不会被销毁。相反,它会被关联给当前窗口的每次请求,在业务对话超时时或者一个 @End 方法被调用后销毁。 | |
@End 注解表明被注解的方法被用来结束一个长期业务对话,所以当前业务对话上下文会在请求结束后被销毁。 | |
这个EJB删除方法会在Seam销毁业务对话上下文时被调用。不要忘记定义该方法! |
HotelBookingAction 包含了实现选择、预订和预订确认的所有动作监听器方法,并在它的实例变量中保存与之相关的状态。 我们认为你一定会同意这个代码比起获取和设置 HttpSession 的属性来说要简洁的多。
而且,一个用户能在每个登录Session中拥有多个独立的业务对话。试试吧!登录系统,执行搜索,在多个浏览器标签页中导航到不同的宾馆页面。 你能在同一时间建立两个不同的宾馆预约。如果某个业务对话被闲置太长时间,Seam最终会判其超时并销毁它的状态。如果在结束业务对话后, 你按了退回按钮回到那个会话的某一页,尝试执行一个动作,Seam会检测到那个业务对话已经被结束了,并将你重定向到搜索页面。
如果你查看下预订系统的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表单验证功能的章节。
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 即可。
DVD商店程序演示了如何在任务管理和页面流中使用jBPM。
用户界面应用jPDL页面流实现了搜索和购物车功能。
管理员界面使用jBPM来管理订单的审批和送货周期。业务流程可以通过选择不同的流程定义实现动态改变。
TODO
见dvdstore目录。
Hibernate预订系统是之前客房预订系统的另一个版本,它使用Hibernate和JavaBeans代替了会话Bean实现持久化。
TODO
见hibernate目录。
Seam可以很方便地实现在服务器端保存状态的应用程序。 然而,服务器端状态在有些情况下并不合适,特别是对那些用来提供内容的功能。 针对这类问题,我们常需要让用户能够收藏页面,有一个相对无状态的服务器,这样一来能够在任何时间通过书签来访问那些被收藏的页面。 Blog范例演示了如何用Seam来实现一个RESTful的应用程序。应用程序中的每个页面都能被收藏,包括搜索结果页面。
Blog范例演示了“拉”风格("pull"-style)的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>]   <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 private EntityManager entityManager; @Unwrap public Blog getBlog() { return (Blog) entityManager.createQuery("from Blog b left join fetch b.blogEntries") .setHint("org.hibernate.cacheable", true) .getSingleResult(); } }
这个组件使用了一个 受Seam管理的持久化上下文(seam-managed persistence context)。 与我们看过的其他例子不同,这个持久化上下文是由Seam管理的,而不是EJB3容器。 持久化上下文贯穿于整个Web请求中,这使得在视图里访问未抓取的关联数据时可以避免发生任何异常。 | |
@Unwrap 注解告诉Seam将 Blog 而不是 BlogService 组件作为方法的返回值提供给客户端。 这是Seam的 管理员组件模式(manager component pattern) |
这些看起来已经很不错了,那如何来收藏诸如搜索结果页这样的表单提交结果页面呢?
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; } }
有些时候,用“推”风格的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  <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>
你可以根据自己的喜好来选择实现。