本教程假定你已下载JBoss AS 4.0.5并安装了EJB 3.0 profile(请使用JBoss AS安装器)。你也得下载一份Seam并解压到工作目录上。 各示例的目录结构仿效以下形式:
第一步,确保已安装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 (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; } }
这个例子中最值得注意的是 @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 (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; } } }
这次我们并没有显式指定 @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:///products/seam/components" xmlns:core="http:///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./xml/ns/javaee" xmlns:xsi="http://www./2001/XMLSchema-instance" xsi:schemaLocation="http://java./xml/ns/javaee http://java./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./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./xml/ns/javaee" xmlns:xsi="http://www./2001/XMLSchema-instance" xsi:schemaLocation="http://java./xml/ns/javaee http://java./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./xml/ns/persistence" xmlns:xsi="http://www./2001/XMLSchema-instance" xsi:schemaLocation="http://java./xml/ns/persistence http://java./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./jsf/html" prefix="h" %> <%@ taglib uri="http://java./jsf/core" prefix="f" %> <%@ taglib uri="http:///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./jsf/html" prefix="h" %> <%@ taglib uri="http://java./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./xml/ns/javaee" xmlns:xsi="http://www./2001/XMLSchema-instance" xsi:schemaLocation="http://java./xml/ns/javaee http://java./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 (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() {} }
请注意,这是个会话作用域的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./jsf/html" prefix="h" %> <%@ taglib uri="http://java./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"> (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>
如果我们用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./jsf/html" prefix="h"%> <%@ taglib uri="http://java./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() {} }
在实际的应用中,@StartTask 及 @EndTask 不会出现在同一个方法中,因为为了完成任务,通常用应用中有许多工作要做。 最后,该应用的主要内容在 todo.jsp 中: Example 1.17. <%@ taglib uri="http://java./jsf/html" prefix="h" %> <%@ taglib uri="http://java./jsf/core" prefix="f" %> <%@ taglib uri="http:///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> (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>
这个页面流在JBossIDE页面流编辑器里看上去是这个样子的: ![]() 看过了页面流,现在再来理解剩下的程序就变得十分简单了! 这是应用程序的主页面numberGuess.jspx: Example 1.19. <%@ taglib uri="http://java./jsf/html" prefix="h"%> <%@ taglib uri="http://java./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./jsf/html" prefix="h"%> <%@ taglib uri="http://java./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; } }
如你所见,这个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 (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) }
应用程序的主页面是一个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> <a:commandButton value="Find Hotels" action="#{hotelSearch.find}" styleClass="button" reRender="searchResults"/> <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> <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>
这个页面根据我们的键入动态地显示搜索结果,让我们选择一家宾馆并将它传给 HotelBookingAction 的 selectHotel() 方法,这个对象才是 真正 有趣的地方。 现在让我们来看看宾馆预定范例程序是如何使用一个对话域的有状态的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() {} }
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 (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(); } }
这些看起来已经很不错了,那如何来收藏诸如搜索结果页这样的表单提交结果页面呢? 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> 你可以根据自己的喜好来选择实现。 |
|