本教程假定你已下載JBoss AS 4.0.5并安裝了EJB 3.0 profile(請使用JBoss AS安裝器)。你也得下載一份Seam并解壓到工作目錄上。
各示例的目錄結構仿效以下形式:
網頁、圖片及樣式表可在 examples/registration/view 目錄中找到。
諸如部署描述文件及數(shù)據導入腳本之類的資源可在目錄 examples/registration/resources 中找到。
Java源代碼保存在 examples/registration/src 中。
Ant構建腳本放在 examples/registration/build.xml 文件中。
第一步,確保已安裝Ant,并正確設定了 $ANT_HOME 及 $JAVA_HOME 的環(huán)境變量。接著在Seam的根目錄下的 build.properties 文件中正確設定JBoss AS 4.0.5的安裝路徑。 若一切就緒,就可在JBoss的安裝根目錄下敲入 bin/run.sh 或 bin/run.bat 命令來啟動JBoss AS。(譯注:此外,請安裝JDK1.5以上以便能直接運行示例代碼)
現(xiàn)在只要在Seam安裝目錄 examples/registration 下輸入 ant deploy 就可構建和部署示例了。
試著在瀏覽器中訪問此鏈接:http://localhost:8080/seam-registration/。
首先,確保已安裝Ant,并正確設定了 $ANT_HOME 及 $JAVA_HOME 的環(huán)境變量。接著在Seam的根目錄下的 build.properties 文件中正確設定Tomcat 6.0的安裝路徑。你需要按照25.5.1章節(jié)“安裝嵌入式的Jboss”中的指導配置 (當然, SEAM也可以脫離Jboss在TOMCAT上直接運行)。
至此,就可在Seam安裝目錄 examples/registration 中輸入 ant deploy.tomcat 構建和部署示例了。
最后啟動Tomcat。
試著在瀏覽器中訪問此鏈接:http://localhost:8080/jboss-seam-registration/。
當你部署示例到Tomcat時,任何的EJB3組件將在JBoss的可嵌入式的容器,也就是完全獨立的EJB3容器環(huán)境中運行。
注冊示例是個極其普通的應用,它可讓新用戶在數(shù)據庫中保存自己的用戶名,真實的姓名及密碼。 此示例并不想一下子就把Seam的所有的酷功能全部秀出。然而, 它演示了EJB3 會話Bean作為JSF動作監(jiān)聽器及Seam的基本配置的使用方法。
或許你對EJB 3.0還不太熟悉,因此我們會對示例的慢慢深入說明。
此示例的首頁顯示了一個非常簡單的表單,它有三個輸入字段。試著在表單上填寫內容并提交,一旦輸入數(shù)據被提交后就會在數(shù)據庫中保存一個user對象。
本示例由兩個JSP頁面,一個實體Bean及無狀態(tài)的會話Bean來實現(xiàn)。
讓我們看一下代碼,就從最“底層”的實體Bean開始吧。
我們需要EJB 實體Bean來保存用戶數(shù)據。這個類通過注解聲明性地定義了 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; }}
(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) | name、 password 及 username 都是實體Bean的持久化屬性。所有的持久化屬性都定義了訪問方法。當JSF渲染輸出及更新模型值階段時需要調用該組件的這些方法。 |
(6) | EJB和Seam都要求有空的構造器。 |
(7) | @NotNull 和 @Length 注解是Hibernate Validator框架的組成部份, Seam集成了Hibernate Validator并讓你用它來作為數(shù)據校驗(盡管你可能并不使用Hibernate作為持久化層)。 |
(8) | 標準EJB注解 @Id 表明了實體Bean的主鍵屬性。 |
這個例子中最值得注意的是 @Name 和 @Scope 注解,它們確立了這個類是Seam的組件。
接下來我們將看到 User 類字段在更新模型值階段時直接被綁定給JSF組件并由JSF操作, 在此并不需要冗余的膠水代碼來在JSP頁面與實體Bean域模型間來回拷貝數(shù)據。
然而,實體Bean不應該進行事務管理或數(shù)據庫訪問。故此,我們無法將此組件作為JSF動作監(jiān)聽器,因而需要會話Bean。
在Seam應用中大都采用會話Bean來作為JSF動作監(jiān)聽器(當然我們也可選擇JavaBean)。
在我們的應用程序中確實存在一個JSF動作和一個會話Bean方法。在此示例中,只有一個JSF動作,并且我們使用會話Bean方法與之相關聯(lián)并使用無狀態(tài)Bean,這是由于所有與動作相關的狀態(tài)都保存在 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 將這個類標記為無狀態(tài)的會話Bean。 |
(2) | 注解 @In將Bean的一個屬性標記為由Seam來注入。 在此例中,此屬性由名為 user 的上下文變量注入(實例的變量名)。 |
(3) | EJB標準注解 @PersistenceContext 用來注入EJB實體管理器。 |
(4) | Seam的 @Logger 注解用來注入組件的 Log 實例。 |
(5) | 動作監(jiān)聽器方法使用標準的EJB3 EntityManager API來與數(shù)據庫交互,并返回JSF的輸出結果。 請注意,由于這是個會話Bean,因此當 register() 方法被調用時事務就會自動開始,并在結束時提交(commit)。 |
(6) | 請注意Seam讓你在EJB-QL中使用JSF EL表達式。因此可在標準JPA Query 對象上調用普通的JPA setParameter() 方法,這樣豈不妙哉? |
(7) | Log API為顯示模板化的日志消息提供了便利。 |
(8) | 多個JSF動作監(jiān)聽器方法返回一個字符串值的輸出,它決定了接下來應顯示的頁面內容。 空輸出(或返回值為空的動作監(jiān)聽器方法)重新顯示上一頁的內容。 在普通的JSF中,用JSF的導航規(guī)則(navigation rule) 來決定輸出結果的JSF視圖id是很常用的。 這種間接性對于復雜的應用是非常有用的,值得去實踐。但是,對于象示例這樣簡單的的應用,Seam讓你使用JSF視圖id作為輸出結果,以減少對導航規(guī)則的需求。請注意,當你用視圖id作為輸出結果時,Seam總會執(zhí)行一次瀏覽器的重定向。 |
(9) | Seam提供了大量的 內置組件(built-in components) 來協(xié)助解決那些經常遇到的問題。 用 FacesMessages 組件就可很容易地來顯示模板化的錯誤或成功的消息。 內置的Seam組件還可由注入或通過調用 instance() 方法來獲取。 |
這次我們并沒有顯式指定 @Scope,若沒有顯式指定時,每個Seam 組件類型就使用其默認的作用域。對于無狀態(tài)的會話Bean, 其默認的作用域就是無狀態(tài)的上下文。實際上 所有的 無狀態(tài)的會話Bean都屬于無狀態(tài)的上下文。
會話Bean的動作監(jiān)聽器在此小應用中履行了業(yè)務和持久化邏輯。在更復雜的應用中,我們可能要將代碼分層并重構持久化邏輯層成 專用數(shù)據存取組件,這很容易做到。但請注意Sean并不強制你在應用分層時使用某種特定的分層策略。
此外,也請注意我們的SessionBean會同步訪問與web請求相關聯(lián)的上下文(比如在 User 對象中的表單的值),狀態(tài)會被保持在事務型的資源里(EntityManager 對象)。 這是對傳統(tǒng)J2EE的體系結構的突破。再次說明,如果你習慣于傳統(tǒng)J2EE的分層,也可以在你的Seam應用實行。但是對于許多的應用,這是明顯的沒有必要 。
很自然,我們的會話Bean需要一個本地接口。
所有的Java代碼就這些了,現(xiàn)在去看一下部署描述文件。
如果你此前曾接觸過許多的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應用中的配置與此處的配置基本相同。
絕大多數(shù)的Seam應用將JSF來作為表示層。因而我們通常需要 faces-config.xml。SEAM將用Facelet定義視圖表現(xiàn)層,所以我們需要告訴JSF用Facelet作為它的模板引擎。
Example 1.6.
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE faces-configPUBLIC "-//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文件就是導航規(guī)則及可能的jBPM流程定義。對于Seam而言, 流程(process flow) 及 配置數(shù)據 是唯一真正屬于需要XML定義的。
在此簡單的示例中,因為我們將視圖頁面的ID嵌入到Action代碼中,所以我們甚至都不需要定義導航規(guī)則。
ejb-jar.xml 文件將 SeamInterceptor 綁定到壓縮包中所有的會話Bean上,以此實現(xiàn)了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的持久化層在哪找到數(shù)據源,該文件也含有一些廠商特定的設定。此例在程序啟動時自動創(chuàng)建數(shù)據庫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的技術來實現(xiàn)。在此例中,我們使用了JSP,因為大多數(shù)的開發(fā)人員都很熟悉, 且這里并沒有其它太多的要求。(我們建議你在實際開發(fā)中使用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>
此部署描述文件聯(lián)接了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在無狀態(tài)上下文中找到 RegisterAction 無狀態(tài)的會話Bean并把它返回。JSF隨之調用 register() 動作監(jiān)聽器方法。
Seam攔截方法調用并在繼續(xù)調用之前從Seam會話上下文注入 User 實體。
register() 方法檢查所輸入用戶名的用戶是否已存在。 若存在該用戶名,則錯誤消息進入 facesmessages 組件隊列,返回無效結果并觸發(fā)瀏覽器重顯頁面。facesmessages 組件嵌在消息字符串的JSF表達式,并將JSF facesmessage 添加到視圖中。
若輸入的用戶不存在,"/registered.jsp" 輸出就會將瀏覽器重定向到 registered.jsp 頁。 當JSF來渲染頁面時,它請求Seam來解析名為 user 的變量,并使用從Seam會話作用域返回的User 實體的屬性值。
在幾乎所有的在線應用中都免不了將搜索結果顯示成可點擊的列表。 因此Sean在JSF層之上提供了特殊的功能,使得我們很容易用EJB-QL或HQL來查詢數(shù)據并用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 用來給表單中的兩個按鈕定義個動作監(jiān)聽器方法, 其中的一個按鈕用來從列表中選擇消息,并顯示該消息。而另一個按鈕則用來刪除一條消息,除此之外,就沒什么特別之處了。
在用戶第一次瀏覽消息頁面時,MessageManagerBean 會話Bean也負責抓取消息列表,考慮到用戶可能以多種方式來瀏覽該頁面,他們也有可能不是由JSF動作來完成,比如用戶可能將該頁加入收藏夾。 因此抓取消息列表發(fā)生在Seam的工廠方法中,而不是在動作監(jiān)聽器方法中。
之所以將此會話Bean設為有狀態(tài)的,是因為我們想在不同的服務器請求間緩存此消息列表。
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 就被會注入給有狀態(tài)Bean的屬性,緊接著 向外注入(outjected)給變量名為message 的事件上下文的屬性。 |
(4) | 此有狀態(tài)Bean有個EJB3的 擴展持久化上下文(extended persistence context)。只要Bean存在,查詢中獲取的消息就會保留在受管理的狀態(tài)中。 這樣一來,此后對有狀態(tài)Bean的所有方法調用勿需顯式調用 EntityManager 就可更新這些消息了。 |
(5) | 當我們第一次瀏覽JSP頁面時,messageList 上下文變量尚未被初始化,@Factory 注解告訴Seam來創(chuàng)建 MessageManagerBean 的實例并調用 findMessages() 方法來初始化上下文變量。 我們把 findMessages() 當作 messages 的 工廠方法。 |
(6) | select() 將選中的 Message 標為已讀,并同時更新數(shù)據庫。 |
(7) | delete() 動作監(jiān)聽器方法將選中的 Message 從數(shù)據庫中刪除。 |
(8) | 對于每個有狀態(tài)的會話Bean,Seam組件的所有方法中 必須 有一不帶參數(shù)的方法被標為 @Remove @Destroy 以確保在Seam的上下文結束時刪除有狀態(tài)Bean,并同時清除所有服務器端的狀態(tài)。 |
請注意,這是個會話作用域的Seam組件。它與用戶登入會話相關聯(lián),并且登入會話的所有請求共享同一個組件的實例。 (在Seam的應用中,我們通常使用會話作用域的組件。)
當然,每個會話Bean都有個業(yè)務接口。
@Localpublic interface MessageManager{ public void findMessages(); public void select(); public void delete(); public void destroy();}
從現(xiàn)在起,我們在示例代碼中將不再對本地接口作特別的說明。
由于XML文件與此前的示例幾乎都一樣,因此我們略過了 components.xml、persistence.xml、 web.xml、ejb-jar.xml、faces-config.xml 及application.xml 的細節(jié),直接來看一下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(),該方法執(zhí)行了一次數(shù)據庫查詢并導致 DataModel 被向外注入。 DataModel 提供了渲染 <h:dataTable> 所需的行數(shù)據。
當用戶點擊 <h:commandLink> 時,JSF就調用 Select() 動作監(jiān)聽器。 Seam攔截此調用并將所選行的數(shù)據注入給 messageManager 組件的 message 屬性。 而動作監(jiān)聽器將所選定的 Message標為已讀。在此調用結束時,Seam向外注入所選定的 Message 給名為 message 的變量。 接著,EJB容器提交事務,將 Message 的已讀標記寫入數(shù)據庫。 最后,該網頁重新渲染,再次顯示消息列表,并在列表下方顯示所選消息的內容。
如果用戶點擊了 <h:commandButton>,JSF就調用 delete() 動作監(jiān)聽器。 Seam攔截此調用并將所選行的數(shù)據注入給 messageManager 組件的 message 屬性。 觸發(fā)動作監(jiān)聽器,將選定的Message 從列表中刪除并同時在 EntityManager 中調用 remove() 方法。在此調用的最后,Seam刷新 messageList 上下文變量并清除名為 message 的上下文變量。 接著,EJB容器提交事務,將 Message 從數(shù)據庫中刪除。最后,該網頁重新渲染,再次顯示消息列表。
jBPM提供了先進的工作流程和任務管理的功能。為了體驗一下jBPM是如何與Seam集成在一起工作的,在此將給你一個簡單的管理“待辦事項列表”的應用。由于管理任務列表等功能是jBPM的核心功能,所以在此例中只用了很少的Java代碼。
這個例子的核心是jBPM的流程定義(process definition)。此外,還有兩個JSP頁面和兩個簡單的JavaBeans(由于他們不用訪問數(shù)據庫,或有其它事務相關的行為,因此并沒有用會話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) | 節(jié)點 <start-state> 代表流程的邏輯開始。一旦流程開始時,它就立即轉入 todo節(jié)點。 |
(2) | <task-node> 節(jié)點代表 等待狀態(tài),就是在執(zhí)行業(yè)務流程暫停時,等待一個或多個未完成的任務。 |
(3) | <task> 元素定義了用戶需要完成的任務。 由于在這個節(jié)點只有定義了一個任務,當它完成,或恢復執(zhí)行時我們就轉入結束狀態(tài)。 此任務從Seam中名為 todolist 的組件(JavaBeans之一)獲得任務description。 |
(4) | 任務在創(chuàng)建時就會被分配給一個用戶或一組用戶時。在此示例中,任務是分配給當前用戶,該用戶從一個內置的名為 actor 的Seam組件中獲得。任何Seam組件都可用來執(zhí)行任務指派。 |
(5) | <end-state>節(jié)點定義業(yè)務流程的邏輯結束。當執(zhí)行到達這個節(jié)點時,流程實例就要被銷毀。 |
如果我們用jBossIDE所提供的流程定義編輯器來查看此流程定義,那它就會是這樣:
這個文檔將我們的 業(yè)務流程 定義成節(jié)點圖。 這可能是最常見的業(yè)務流程:只有一個 任務 被執(zhí)行,當這項任務完成之后,業(yè)務流程就結束了。
第一個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負責啟動業(yè)務流程實例及結束任務。
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 注解為指定名稱的流程定義創(chuàng)建了一個新的jBPM流程實例。 |
(3) | Seam的 @StartTask 注解用來啟動任務,@EndTask 用來結束任務,并允許恢復執(zhí)行業(yè)務流程。 |
在實際的應用中,@StartTask 及 @EndTask 不會出現(xiàn)在同一個方法中,因為為了完成任務,通常用應用中有許多工作要做。
最后,該應用的主要內容在 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作為請求參數(shù)傳給Seam:
<h:column> <s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/></h:column>
(請注意,這是在使用Seam seam-ui.jar 包中的JSF <s:button> 控件。)
這個按鈕是用來更新任務屬性。當提交表單時,Seam和jBPM將直接更改任務的持久化,不需要任何的動作監(jiān)聽器方法:
<h:commandButton value="Update Items" action="update"/>
第二個表單通過調用注解為 @CreateProcess的動作方法來創(chuàng)建新的項目(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導航規(guī)則是定義頁面流的一個完美的方法。 而對于那些帶有更多約束的導航,特別是帶狀態(tài)的用戶界面而言,導航規(guī)則反而使得系統(tǒng)流程變得難以理解。 要理解整個流程,你需要從視圖頁面、動作和導航規(guī)則里一點點把它拼出來。
Seam允許你使用一個jPDL流程定義來定義頁面流。下面這個簡單的猜數(shù)字范例將演示這一切是如何實現(xiàn)的。
這個例子由一個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>元素定義了一個等待狀態(tài),在該狀態(tài)中系統(tǒng)顯示一個JSF視圖等待用戶輸入。 view-id與簡單JSF導航規(guī)則中的view id一樣。 redirect屬性告訴Seam在導航到頁面時使用post-then-redirect。(這會帶來友好的瀏覽器URL。) |
(2) | <transition> 元素命名了一個JSF輸出。當一個JSF動作導致那個輸出時會觸發(fā)轉換。 在任何jBPM轉換動作調用后,執(zhí)行會進行到頁面流程圖的下一個節(jié)點。 |
(3) | 一個轉換動作 <action> 就像JSF動作,不同的就是它只發(fā)生在一個jBPM轉換發(fā)生時。 轉換動作能調用任何Seam組件。 |
(4) | <decision> 節(jié)點用來劃分頁面流,通過計算JSF EL表達式決定要執(zhí)行的下一個節(jié)點。 |
這個頁面流在JBossIDE頁面流編輯器里看上去是這個樣子的:
看過了頁面流,現(xiàn)在再來理解剩下的程序就變得十分簡單了!
這是應用程序的主頁面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會為該組件創(chuàng)建一個新的實例,并調用 @Create 方法,允許組件初始化自己。 |
(2) | @Begin 注解啟動了一個Seam 業(yè)務會話(conversation) (稍后詳細說明),并指定業(yè)務會話頁面流所要使用的頁面流定義。 |
如你所見,這個Seam組件是純業(yè)務邏輯的!它不需要知道任何關于用戶交互的東西。這點使得組件更易被復用。
該系統(tǒng)是一個完整的賓館客房預訂系統(tǒng),它由下列功能組成:
用戶注冊
登錄
注銷
設置密碼
搜索賓館
選擇賓館
客房預訂
預訂確認
當前預訂列表
應用程序中使用了JSF、EJB 3.0和Seam,視圖部分結合了Facelets。也可以選擇使用JSF、Facelets、Seam、JavaBeans和Hibernate3。
在使用過一段時間后你會發(fā)現(xiàn)該應用程序非常 健壯。你能使用回退按鈕、刷新瀏覽器、打開多個窗口, 或者鍵入各種無意義的數(shù)據,會發(fā)現(xiàn)都很難讓它崩潰。你也許會想我們花了幾個星期測試修復該系統(tǒng)才達到了這個目標。 事實卻不是這樣的,Seam的設計使你能夠用它方便地構建健壯的web應用程序,而且Seam還提供了很多以前需要通過編碼才能實現(xiàn)的健壯性。
在你瀏覽范例程序代碼研究它是如何運行時,注意觀察聲明式的狀態(tài)管理和集成的驗證是如何被用來實現(xiàn)這種健壯性的。
這個項目的結構和上一個一樣,要安裝部署該應用程序請參考Section 1.1, “試試看”。 當應用程序啟動后,可以通過 http://localhost:8080/seam-booking/ 進行訪問。
只需要用9個類(加上6個Session Bean的本地接口)就能實現(xiàn)這個應用程序。6個Session Bean動作監(jiān)聽器包括了以下功能的所有業(yè)務邏輯。
應用程序的持久化模型由三個實體bean實現(xiàn)。
我們鼓勵您隨意瀏覽源代碼。在這個教程里我們將關注功能中的某一特定部分:賓館搜索、選擇、預訂和確認。 從用戶的角度來看,從選擇賓館到確認的每一步都是工作中的一個連續(xù)單元,屬于一個 業(yè)務對話。 然而搜索卻 不 是該對話的一部分。用戶能在不同瀏覽器標簽頁中的相同搜索結果頁面中選擇多個賓館。
大多數(shù)Web應用程序架構沒有提供表示業(yè)務對話的一級構件(first class construct)。這在管理與對話相關的狀態(tài)時帶來了很多麻煩。 通常情況下,Java的Web應用程序結合兩種技術來應對這一情況:一是將某些狀態(tài)丟入 HttpSession;二是將可持久化的狀態(tài)在每個請求(Request)后寫入數(shù)據庫,并在每個新請求的開始將之重建。
由于數(shù)據庫是最不可擴展的一層,因此這么做往往導致完全無法接受的擴展性低下。在每次請求時訪問數(shù)據庫所造成的額外流量和等待時間也是一個問題。 要降低冗余流量,Java應用程序常引入一個(二級)數(shù)據緩存來保存被經常訪問的數(shù)據。 然而這個緩存是很低效的,因為它的失效算法是基于LRU(最近最少使用)策略,而不是基于用戶何時結束與該數(shù)據相關的工作。 此外,由于該緩存被許多并發(fā)事務共享,要保持緩存與數(shù)據庫的狀態(tài)一致,我們需要引入了一套完整的機制。
現(xiàn)在再讓我們考慮將狀態(tài)保存在 HttpSession 里。通過精心設計的編程,我們也許能控制session數(shù)據的大小。 但這遠比聽起來要麻煩的多,因為Web瀏覽器允許特殊的非線性導航。 但假設我們在系統(tǒng)開發(fā)到一半的時候突然發(fā)現(xiàn)一個需求,它要求用戶可以擁有 多并發(fā)業(yè)務對話(我就碰到過)。 要開發(fā)一些機制,以分離與不同并發(fā)業(yè)務會話相關的session狀態(tài),并引入故障保護,在用戶關閉瀏覽器窗口或標簽頁時銷毀業(yè)務會話狀態(tài)。 這對普通人來說可不是一件輕松的事情(我就實現(xiàn)過兩次,一次是為一個客戶應用程序,另一次是為Seam,幸好我是出了名的瘋子)。
現(xiàn)在提供一個更好的方法。
Seam引入了 對話上下文 來作為一級構件。你能在其中安全地保存業(yè)務對話狀態(tài),它會保證狀態(tài)有一個定義良好的生命周期。 而且,你不用再不停地在應用服務器和數(shù)據庫間傳遞數(shù)據,因為業(yè)務對話上下文就是一個天然的緩存,用來緩存用戶的數(shù)據。
通常情況下,我們保存在業(yè)務對話上下文中的組件是有狀態(tài)的Session Bean。(我們也在其中保存實體Bean和JavaBeans。) 在Java社區(qū)中一直有一個謠傳,認為有狀態(tài)的Session Bean是擴展性的殺手。在1998年WebFoobar 1.0發(fā)布時的確如此。 但今天的情況已經變了。像JBoss 4.0這樣的應用服務器都有很成熟的機制處理有狀態(tài)Session Bean的狀態(tài)復制。 (例如,JBoss EJB3容器可以執(zhí)行很細致的復制,只復制那些屬性值被改變過的bean。) 請注意,所有那些傳統(tǒng)技術中關于有狀態(tài)Bean是低效的爭論也同樣發(fā)生在 HttpSession 上,所以說將狀態(tài)從業(yè)務層的有狀態(tài)Session Bean遷移到Web Session中以提高性能的做法毫無疑問是被誤導的。 不正確地使用有狀態(tài)的Bean,或者是將它們用在錯誤的地方上都會使應用程序變得無法擴展。 但這并不意味著你應該 永遠不要 使用它們。總之,Seam會告訴你一個安全使用的模型。歡迎來到2005年。
OK,不再多說了,話題回到這個指南上吧。
賓館預訂范例演示了不同作用域的有狀態(tài)組件是如何協(xié)同工作實現(xiàn)復雜的行為的。 它的主頁面允許用戶搜索賓館。搜索的結果被保存在Seam的session域中。 當用戶導航到其中一個賓館時,一個業(yè)務會話便開始了,一個業(yè)務會話域組件回調session域組件以獲得選中的賓館。
賓館預訂范例還演示了如何使用Ajax4JSF在不用手工編寫JavaScript的情況下實現(xiàn)富客戶端(Rich Client)行為。
搜索功能用了一個Session域的有狀態(tài)Session Bean來實現(xiàn),有點類似于我們在上面的消息列表范例里看到的那個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 注解表明這個類是一個有狀態(tài)的Session Bean。它們的默認作用域是業(yè)務對話上下文。 |
(2) | @Restrict注解給組件加上了一個安全限制。只有登錄過的用戶才能訪問該組件。安全章節(jié)中更詳細地討論了Seam的安全問題。 |
(3) | @DataModel 注解將一個 List 作為JSF ListDataModel 暴露出去。 這簡化了搜索界面的可單擊列表的實現(xiàn)。在這個例子中,賓館的列表是以名為 hotels 的 ListDataModel 業(yè)務對話變量暴露給頁面的。 |
(4) | EJB標準中的 @Remove 注解指定了一個有狀態(tài)的Session Bean應該在注解的方法被調用后被刪除且其狀態(tài)應該被銷毀。 在Seam里,所有有狀態(tài)的Session Bean都應該定義一個標有 @Destroy @Remove 的方法。 這是Seam在銷毀Session上下文時要調用的EJB刪除方法。實際上 @Destroy 注解更有用,因為它能在Seam上下文結束時被用來做各種各樣的清理工作。如果沒有一個 @Destroy @Remove 方法,那么狀態(tài)會泄露,你就會碰到性能上的問題。 |
應用程序的主頁面是一個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>
(1) | Ajax4JSF的 <a:support> 標簽允許一個JSF動作事件監(jiān)聽器在類似 onkeyup 這樣的JavaScript事件發(fā)生時被異步的 XMLHttpRequest 調用。 更棒的是,reRender 屬性讓我們可以在收到異步響應時渲染一個JSF頁面的片段并執(zhí)行一個頁面的局部修改。 |
(2) | Ajax4JSF的 <a:status> 標簽使我們能在等待異步請求返回時顯示一個簡單的動畫。 |
(3) | Ajax4JSF的 <a:outputPanel> 標簽定義了一塊能被異步請求修改的頁面區(qū)域。 |
(4) | Seam的<s:link> 標簽使我們能將一個JSF動作監(jiān)聽器附加在一個普通的(非JavaScript)HTML鏈接上。 用它取代標準JSF的 <h:commandLink> 的好處就是它在“在新窗口中打開”和“在新標簽頁中打開”時仍然有效。 值得注意的另一點就是我們用了一個綁定了參數(shù)的方法:#{hotelBooking.selectHotel(hot)}。 在標準的統(tǒng)一EL中這是不允許的,但Seam對EL的擴展進行了擴展,使表達式能夠支持帶參數(shù)的方法。 |
這個頁面根據我們的鍵入動態(tài)地顯示搜索結果,讓我們選擇一家賓館并將它傳給 HotelBookingAction 的 selectHotel() 方法,這個對象才是 真正 有趣的地方。
現(xiàn)在讓我們來看看賓館預定范例程序是如何使用一個對話域的有狀態(tài)的Session Bean的,這個Session Bean實現(xiàn)了業(yè)務會話相關持久化數(shù)據的天然緩存。 下面的代碼很長。但如果你把它理解為實現(xiàn)業(yè)務會話的多個步驟的一系列動作的話,它是不難理解的。我們把這個類當作故事一樣從頭開始閱讀。
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的 擴展持久化上下文,所以任意實體實例在整個有狀態(tài)Session Bean的生命周期中一直受到管理。 |
(2) | @Out 注解聲明了一個屬性值在方法調用后會 向外注入 到一個上下文變量中的。 在這個例子中,名為 hotel 的上下文變量會在每個動作監(jiān)聽器調用完成后被設置為 hotel 實例變量的值。 |
(3) | @Begin 注解表明被注解的方法開始一個 長期業(yè)務對話,因此當前業(yè)務對話上下文在請求結束后不會被銷毀。相反,它會被關聯(lián)給當前窗口的每次請求,在業(yè)務對話超時時或者一個 @End 方法被調用后銷毀。 |
(4) | @End 注解表明被注解的方法被用來結束一個長期業(yè)務對話,所以當前業(yè)務對話上下文會在請求結束后被銷毀。 |
(5) | 這個EJB刪除方法會在Seam銷毀業(yè)務對話上下文時被調用。不要忘記定義該方法! |
HotelBookingAction 包含了實現(xiàn)選擇、預訂和預訂確認的所有動作監(jiān)聽器方法,并在它的實例變量中保存與之相關的狀態(tài)。 我們認為你一定會同意這個代碼比起獲取和設置 HttpSession的屬性來說要簡潔的多。
而且,一個用戶能在每個登錄Session中擁有多個獨立的業(yè)務對話。試試吧!登錄系統(tǒng),執(zhí)行搜索,在多個瀏覽器標簽頁中導航到不同的賓館頁面。 你能在同一時間建立兩個不同的賓館預約。如果某個業(yè)務對話被閑置太長時間,Seam最終會判其超時并銷毀它的狀態(tài)。如果在結束業(yè)務對話后, 你按了退回按鈕回到那個會話的某一頁,嘗試執(zhí)行一個動作,Seam會檢測到那個業(yè)務對話已經被結束了,并將你重定向到搜索頁面。
如果你查看下預訂系統(tǒng)的WAR文件,你會在 WEB-INF/lib 目錄中找到 seam-ui.jar。 這個包里有許多Seam的JSF自定義控件。本應用程序在從搜索界面導航到賓館頁面時使用了 <s:link>控件:
<s:link value="View Hotel" action="#{hotelBooking.selectHotel}"/>
這里的 <s:link> 允許我們在不打斷瀏覽器的“在新窗口打開”功能的情況下給HTML鏈接附加上一個動作監(jiān)聽器。 標準的JSF <h:commandLink> 無法在“在新窗口打開”的情況下正常工作。 稍后我們會看到 <s:link> 還能提供很多其他有用的特性,包括業(yè)務會話傳播規(guī)則。
賓館預訂系統(tǒng)里還用了些別的Seam和Ajax4JSF控件,特別是在 /book.xhtml 頁面里。我們在這里不深入討論這些控件,如果你想看懂這些代碼,請參考介紹Seam的JSF表單驗證功能的章節(jié)。
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頁面流實現(xiàn)了搜索和購物車功能。
管理員界面使用jBPM來管理訂單的審批和送貨周期。業(yè)務流程可以通過選擇不同的流程定義實現(xiàn)動態(tài)改變。
TODO
見dvdstore目錄。
Hibernate預訂系統(tǒng)是之前客房預訂系統(tǒng)的另一個版本,它使用Hibernate和JavaBeans代替了會話Bean實現(xiàn)持久化。
TODO
見hibernate目錄。
Seam可以很方便地實現(xiàn)在服務器端保存狀態(tài)的應用程序。 然而,服務器端狀態(tài)在有些情況下并不合適,特別是對那些用來提供內容的功能。 針對這類問題,我們常需要讓用戶能夠收藏頁面,有一個相對無狀態(tài)的服務器,這樣一來能夠在任何時間通過書簽來訪問那些被收藏的頁面。 Blog范例演示了如何用Seam來實現(xiàn)一個RESTful的應用程序。應用程序中的每個頁面都能被收藏,包括搜索結果頁面。
Blog范例演示了“拉”風格("pull"-style)的MVC,它不使用動作監(jiān)聽器方法來獲取數(shù)據和為視圖準備數(shù)據,而是視圖在被顯示時從組件中拉數(shù)據。
從 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> 的數(shù)據是怎么被初始化的呢? 事實上,Blog 是延遲加載的,即在需要時才被名為 blog 的Seam組件“拉”出來。 這與傳統(tǒng)的基于動作的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請求中,這使得在視圖里訪問未抓取的關聯(lián)數(shù)據時可以避免發(fā)生任何異常。 |
(2) | @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>
要實現(xiàn)一個可收藏的搜索結果頁面,我們需要在處理搜索表單提交后執(zhí)行一個瀏覽器重定向。 因為我們用JSF視圖id作為動作輸出,所以Seam會在表單提交后自動重定向到該表單id。除此之外,我們也能像這樣來定義一個導航規(guī)則:
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>
在重定向時,我們需要將表單的值作為請求參數(shù)包括進來,得到的書簽URL會是這個樣子: http://localhost:8080/seam-blog/search.seam?searchPattern=seam。 JSF沒有為此提供一個簡單的途徑,但Seam卻有。我們能在 WEB-INF/pages.xml 中定義一個 頁面參數(shù):
Example 1.30.
<pages> <page view-id="/search.xhtml"> <param name="searchPattern" value="#{searchService.searchPattern}"/> </page> ...</pages>
這告訴Seam在重定向時將 #{searchService.searchPattern} 的值作為名字是 searchPattern 的請求參數(shù)包括進去,并在顯示頁面前重新將這個值賦上。
重定向會把我們帶到 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 組件工作起來非常像傳統(tǒng)“推”風格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>
范例中還將頁面動作運用于一些其他的功能上 — 登錄和頁面訪問記數(shù)器。另外一點值得注意的是在頁面動作綁定中使用了一個參數(shù)。 這不是標準的JSF EL,是Seam為你提供的,你不僅能在頁面動作中使用它,還可以將它使用在JSF方法綁定中。
當 entry.xhtml 頁面被請求時,Seam先為模型綁定上頁面參數(shù) blogEntryId,然后運行頁面動作,該動作獲取所需的數(shù)據 — 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>
如果在數(shù)據庫中沒有找到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); }}
該范例的另一個實現(xiàn)在方法綁定中沒有使用參數(shù):
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>
你可以根據自己的喜好來選擇實現(xiàn)。