本教程假定你已下載JBoss AS 4.0.5并安裝了EJB 3.0 profile(請使用JBoss AS安裝器)。你也得下載一份Seam并解壓到工作目錄上。 各示例的目錄結(jié)構(gòu)仿效以下形式:
第一步,確保已安裝Ant,并正確設(shè)定了 $ANT_HOME 及 $JAVA_HOME 的環(huán)境變量。接著在Seam的根目錄下的 build.properties 文件中正確設(shè)定JBoss AS 4.0.5的安裝路徑。 若一切就緒,就可在JBoss的安裝根目錄下敲入 bin/run.sh 或 bin/run.bat 命令來啟動JBoss AS。(譯注:此外,請安裝JDK1.5以上以便能直接運行示例代碼) 現(xiàn)在只要在Seam安裝目錄 examples/registration 下輸入 ant deploy 就可構(gòu)建和部署示例了。 試著在瀏覽器中訪問此鏈接:http://localhost:8080/seam-registration/。 首先,確保已安裝Ant,并正確設(shè)定了 $ANT_HOME 及 $JAVA_HOME 的環(huán)境變量。接著在Seam的根目錄下的 build.properties 文件中正確設(shè)定Tomcat 6.0的安裝路徑。你需要按照25.5.1章節(jié)“安裝嵌入式的Jboss”中的指導(dǎo)配置 (當然, SEAM也可以脫離Jboss在TOMCAT上直接運行)。 至此,就可在Seam安裝目錄 examples/registration 中輸入 ant deploy.tomcat 構(gòu)建和部署示例了。 最后啟動Tomcat。 試著在瀏覽器中訪問此鏈接:http://localhost:8080/jboss-seam-registration/。 當你部署示例到Tomcat時,任何的EJB3組件將在JBoss的可嵌入式的容器,也就是完全獨立的EJB3容器環(huán)境中運行。 注冊示例是個極其普通的應(yīng)用,它可讓新用戶在數(shù)據(jù)庫中保存自己的用戶名,真實的姓名及密碼。 此示例并不想一下子就把Seam的所有的酷功能全部秀出。然而, 它演示了EJB3 會話Bean作為JSF動作監(jiān)聽器及Seam的基本配置的使用方法。 或許你對EJB 3.0還不太熟悉,因此我們會對示例的慢慢深入說明。 此示例的首頁顯示了一個非常簡單的表單,它有三個輸入字段。試著在表單上填寫內(nèi)容并提交,一旦輸入數(shù)據(jù)被提交后就會在數(shù)據(jù)庫中保存一個user對象。 ![]() 本示例由兩個JSP頁面,一個實體Bean及無狀態(tài)的會話Bean來實現(xiàn)。 ![]() 讓我們看一下代碼,就從最“底層”的實體Bean開始吧。 我們需要EJB 實體Bean來保存用戶數(shù)據(jù)。這個類通過注解聲明性地定義了 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域模型間來回拷貝數(shù)據(jù)。 然而,實體Bean不應(yīng)該進行事務(wù)管理或數(shù)據(jù)庫訪問。故此,我們無法將此組件作為JSF動作監(jiān)聽器,因而需要會話Bean。 在Seam應(yīng)用中大都采用會話Bean來作為JSF動作監(jiān)聽器(當然我們也可選擇JavaBean)。 在我們的應(yīng)用程序中確實存在一個JSF動作和一個會話Bean方法。在此示例中,只有一個JSF動作,并且我們使用會話Bean方法與之相關(guān)聯(lián)并使用無狀態(tài)Bean,這是由于所有與動作相關(guān)的狀態(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; } } }
這次我們并沒有顯式指定 @Scope,若沒有顯式指定時,每個Seam 組件類型就使用其默認的作用域。對于無狀態(tài)的會話Bean, 其默認的作用域就是無狀態(tài)的上下文。實際上 所有的 無狀態(tài)的會話Bean都屬于無狀態(tài)的上下文。 會話Bean的動作監(jiān)聽器在此小應(yīng)用中履行了業(yè)務(wù)和持久化邏輯。在更復(fù)雜的應(yīng)用中,我們可能要將代碼分層并重構(gòu)持久化邏輯層成 專用數(shù)據(jù)存取組件,這很容易做到。但請注意Sean并不強制你在應(yīng)用分層時使用某種特定的分層策略。 此外,也請注意我們的SessionBean會同步訪問與web請求相關(guān)聯(lián)的上下文(比如在 User 對象中的表單的值),狀態(tài)會被保持在事務(wù)型的資源里(EntityManager 對象)。 這是對傳統(tǒng)J2EE的體系結(jié)構(gòu)的突破。再次說明,如果你習慣于傳統(tǒng)J2EE的分層,也可以在你的Seam應(yīng)用實行。但是對于許多的應(yīng)用,這是明顯的沒有必要 。 很自然,我們的會話Bean需要一個本地接口。 所有的Java代碼就這些了,現(xiàn)在去看一下部署描述文件。 如果你此前曾接觸過許多的Java框架,你就會習慣于將所有的組件類放在某種XML文件中來聲明,那些文件就會隨著項目的不斷成熟而不斷加大到最終到不可收拾的地步。 對于Seam應(yīng)用,你盡可放心,因為它并不要求應(yīng)用組件都要有相應(yīng)的XML。大部份的Seam應(yīng)用要求非常少量的XML即可,且XML文件大小不會隨著項目的增大而快速增長。 無論如何,若能為 某些 組件(特別是Seam內(nèi)置組件)提供某些 外部配置往往是有用的。這樣一來,我們就有幾個選擇, 但最靈活的選擇還是使用位于 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內(nèi)置組件 org.jboss.seam.core.init 的 jndiPattern 屬性。這里需要奇怪的@符號是因為ANT腳本會在部署應(yīng)用時將正確的JNDI語法在標記處自動填補 我們將以WAR的形式來部署此小應(yīng)用的表示層,因此需要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應(yīng)用中的配置與此處的配置基本相同。 絕大多數(shù)的Seam應(yīng)用將JSF來作為表示層。因而我們通常需要 faces-config.xml。SEAM將用Facelet定義視圖表現(xiàn)層,所以我們需要告訴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都是通過經(jīng)過注釋的Seam組件。所以在Seam的應(yīng)用中,faces-config.xml比原始的JSF更少用到。 實際上,一旦你把所有的基本描述文件配置完畢,你所需寫的 唯一類型的 XML文件就是導(dǎo)航規(guī)則及可能的jBPM流程定義。對于Seam而言, 流程(process flow) 及 配置數(shù)據(jù) 是唯一真正屬于需要XML定義的。 在此簡單的示例中,因為我們將視圖頁面的ID嵌入到Action代碼中,所以我們甚至都不需要定義導(dǎo)航規(guī)則。 ejb-jar.xml 文件將 SeamInterceptor 綁定到壓縮包中所有的會話Bean上,以此實現(xiàn)了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的持久化層在哪找到數(shù)據(jù)源,該文件也含有一些廠商特定的設(shè)定。此例在程序啟動時自動創(chuàng)建數(shù)據(jù)庫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應(yīng)用的視圖可由任意支持JSF的技術(shù)來實現(xiàn)。在此例中,我們使用了JSP,因為大多數(shù)的開發(fā)人員都很熟悉, 且這里并沒有其它太多的要求。(我們建議你在實際開發(fā)中使用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毫無相干。 最后,因為我們的應(yīng)用是要部署成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> 此部署描述文件聯(lián)接了EAR中的所有模塊,并把Web應(yīng)用綁定到此應(yīng)用的首頁 /seam-registration。 至此,我們了解了整個應(yīng)用中 所有的 部署描述文件! 當提交表單時,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隨之調(diào)用 register() 動作監(jiān)聽器方法。 Seam攔截方法調(diào)用并在繼續(xù)調(diào)用之前從Seam會話上下文注入 User 實體。 register() 方法檢查所輸入用戶名的用戶是否已存在。 若存在該用戶名,則錯誤消息進入 facesmessages 組件隊列,返回無效結(jié)果并觸發(fā)瀏覽器重顯頁面。facesmessages 組件嵌在消息字符串的JSF表達式,并將JSF facesmessage 添加到視圖中。 若輸入的用戶不存在,"/registered.jsp" 輸出就會將瀏覽器重定向到 registered.jsp 頁。 當JSF來渲染頁面時,它請求Seam來解析名為 user 的變量,并使用從Seam會話作用域返回的User 實體的屬性值。 在幾乎所有的在線應(yīng)用中都免不了將搜索結(jié)果顯示成可點擊的列表。 因此Sean在JSF層之上提供了特殊的功能,使得我們很容易用EJB-QL或HQL來查詢數(shù)據(jù)并用JSF <h:dataTable> 將查詢結(jié)果顯示成可點擊的列表。我們將在接下的例子中演示這一功能。 ![]() 此消息示例中有一個實體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設(shè)為有狀態(tài)的,是因為我們想在不同的服務(wù)器請求間緩存此消息列表。 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組件。它與用戶登入會話相關(guān)聯(lián),并且登入會話的所有請求共享同一個組件的實例。 (在Seam的應(yīng)用中,我們通常使用會話作用域的組件。) 當然,每個會話Bean都有個業(yè)務(wù)接口。 @Local public 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有什么關(guān)系。 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頁面將設(shè)法解析 messagelist 上下文變量。 由于上下文變量尚未被初始化,因此Seam將調(diào)用工廠方法 findmessages(),該方法執(zhí)行了一次數(shù)據(jù)庫查詢并導(dǎo)致 DataModel 被向外注入。 DataModel 提供了渲染 <h:dataTable> 所需的行數(shù)據(jù)。 當用戶點擊 <h:commandLink> 時,JSF就調(diào)用 Select() 動作監(jiān)聽器。 Seam攔截此調(diào)用并將所選行的數(shù)據(jù)注入給 messageManager 組件的 message 屬性。 而動作監(jiān)聽器將所選定的 Message標為已讀。在此調(diào)用結(jié)束時,Seam向外注入所選定的 Message 給名為 message 的變量。 接著,EJB容器提交事務(wù),將 Message 的已讀標記寫入數(shù)據(jù)庫。 最后,該網(wǎng)頁重新渲染,再次顯示消息列表,并在列表下方顯示所選消息的內(nèi)容。 如果用戶點擊了 <h:commandButton>,JSF就調(diào)用 delete() 動作監(jiān)聽器。 Seam攔截此調(diào)用并將所選行的數(shù)據(jù)注入給 messageManager 組件的 message 屬性。 觸發(fā)動作監(jiān)聽器,將選定的Message 從列表中刪除并同時在 EntityManager 中調(diào)用 remove() 方法。在此調(diào)用的最后,Seam刷新 messageList 上下文變量并清除名為 message 的上下文變量。 接著,EJB容器提交事務(wù),將 Message 從數(shù)據(jù)庫中刪除。最后,該網(wǎng)頁重新渲染,再次顯示消息列表。 jBPM提供了先進的工作流程和任務(wù)管理的功能。為了體驗一下jBPM是如何與Seam集成在一起工作的,在此將給你一個簡單的管理“待辦事項列表”的應(yīng)用。由于管理任務(wù)列表等功能是jBPM的核心功能,所以在此例中只用了很少的Java代碼。 ![]() 這個例子的核心是jBPM的流程定義(process definition)。此外,還有兩個JSP頁面和兩個簡單的JavaBeans(由于他們不用訪問數(shù)據(jù)庫,或有其它事務(wù)相關(guān)的行為,因此并沒有用會話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所提供的流程定義編輯器來查看此流程定義,那它就會是這樣: ![]() 這個文檔將我們的 業(yè)務(wù)流程 定義成節(jié)點圖。 這可能是最常見的業(yè)務(wù)流程:只有一個 任務(wù) 被執(zhí)行,當這項任務(wù)完成之后,業(yè)務(wù)流程就結(jié)束了。 第一個JavaBean處理登入界面 login.jsp。 它的工作就是用 actor 組件初始化jBPM用戶id(在實際的應(yīng)用中,它也需要驗證用戶。) 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內(nèi)置的 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負責啟動業(yè)務(wù)流程實例及結(jié)束任務(wù)。 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() {} }
在實際的應(yīng)用中,@StartTask 及 @EndTask 不會出現(xiàn)在同一個方法中,因為為了完成任務(wù),通常用應(yīng)用中有許多工作要做。 最后,該應(yīng)用的主要內(nèi)容在 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內(nèi)置組件 taskInstanceList 獲得的任務(wù)渲染成任務(wù)列表,此列表在JSF表單內(nèi)被定義。 <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 的實例。 以下代碼簡單地展示了列表中每一任務(wù)的有趣特性。為了讓用戶能更改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> 該按鈕通過調(diào)用被注解為 @StartTask @EndTask 的動作方法來結(jié)束任務(wù)。它把任務(wù)id作為請求參數(shù)傳給Seam: <h:column> <s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/> </h:column> (請注意,這是在使用Seam seam-ui.jar 包中的JSF <s:button> 控件。) 這個按鈕是用來更新任務(wù)屬性。當提交表單時,Seam和jBPM將直接更改任務(wù)的持久化,不需要任何的動作監(jiān)聽器方法: <h:commandButton value="Update Items" action="update"/> 第二個表單通過調(diào)用注解為 @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配置并不是很有趣。 對有相對自由(特別)導(dǎo)航的Seam應(yīng)用程序而言,JSF/Seam導(dǎo)航規(guī)則是定義頁面流的一個完美的方法。 而對于那些帶有更多約束的導(dǎo)航,特別是帶狀態(tài)的用戶界面而言,導(dǎo)航規(guī)則反而使得系統(tǒng)流程變得難以理解。 要理解整個流程,你需要從視圖頁面、動作和導(dǎo)航規(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>
這個頁面流在JBossIDE頁面流編輯器里看上去是這個樣子的: ![]() 看過了頁面流,現(xiàn)在再來理解剩下的程序就變得十分簡單了! 這是應(yīng)用程序的主頁面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 的命令按鈕是如何進行轉(zhuǎn)換而不是直接調(diào)用一個動作的。 win.jspx 頁面的內(nèi)容是可想而知的: 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 也差不多(我就不重復(fù)復(fù)制/粘貼了)。最后,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組件是純業(yè)務(wù)邏輯的!它不需要知道任何關(guān)于用戶交互的東西。這點使得組件更易被復(fù)用。 該系統(tǒng)是一個完整的賓館客房預(yù)訂系統(tǒng),它由下列功能組成:
![]() 應(yīng)用程序中使用了JSF、EJB 3.0和Seam,視圖部分結(jié)合了Facelets。也可以選擇使用JSF、Facelets、Seam、JavaBeans和Hibernate3。 在使用過一段時間后你會發(fā)現(xiàn)該應(yīng)用程序非常 健壯。你能使用回退按鈕、刷新瀏覽器、打開多個窗口, 或者鍵入各種無意義的數(shù)據(jù),會發(fā)現(xiàn)都很難讓它崩潰。你也許會想我們花了幾個星期測試修復(fù)該系統(tǒng)才達到了這個目標。 事實卻不是這樣的,Seam的設(shè)計使你能夠用它方便地構(gòu)建健壯的web應(yīng)用程序,而且Seam還提供了很多以前需要通過編碼才能實現(xiàn)的健壯性。 在你瀏覽范例程序代碼研究它是如何運行時,注意觀察聲明式的狀態(tài)管理和集成的驗證是如何被用來實現(xiàn)這種健壯性的。 這個項目的結(jié)構(gòu)和上一個一樣,要安裝部署該應(yīng)用程序請參考Section 1.1, “試試看”。 當應(yīng)用程序啟動后,可以通過 http://localhost:8080/seam-booking/ 進行訪問。 只需要用9個類(加上6個Session Bean的本地接口)就能實現(xiàn)這個應(yīng)用程序。6個Session Bean動作監(jiān)聽器包括了以下功能的所有業(yè)務(wù)邏輯。
應(yīng)用程序的持久化模型由三個實體bean實現(xiàn)。
我們鼓勵您隨意瀏覽源代碼。在這個教程里我們將關(guān)注功能中的某一特定部分:賓館搜索、選擇、預(yù)訂和確認。 從用戶的角度來看,從選擇賓館到確認的每一步都是工作中的一個連續(xù)單元,屬于一個 業(yè)務(wù)對話。 然而搜索卻 不 是該對話的一部分。用戶能在不同瀏覽器標簽頁中的相同搜索結(jié)果頁面中選擇多個賓館。 大多數(shù)Web應(yīng)用程序架構(gòu)沒有提供表示業(yè)務(wù)對話的一級構(gòu)件(first class construct)。這在管理與對話相關(guān)的狀態(tài)時帶來了很多麻煩。 通常情況下,Java的Web應(yīng)用程序結(jié)合兩種技術(shù)來應(yīng)對這一情況:一是將某些狀態(tài)丟入 HttpSession;二是將可持久化的狀態(tài)在每個請求(Request)后寫入數(shù)據(jù)庫,并在每個新請求的開始將之重建。 由于數(shù)據(jù)庫是最不可擴展的一層,因此這么做往往導(dǎo)致完全無法接受的擴展性低下。在每次請求時訪問數(shù)據(jù)庫所造成的額外流量和等待時間也是一個問題。 要降低冗余流量,Java應(yīng)用程序常引入一個(二級)數(shù)據(jù)緩存來保存被經(jīng)常訪問的數(shù)據(jù)。 然而這個緩存是很低效的,因為它的失效算法是基于LRU(最近最少使用)策略,而不是基于用戶何時結(jié)束與該數(shù)據(jù)相關(guān)的工作。 此外,由于該緩存被許多并發(fā)事務(wù)共享,要保持緩存與數(shù)據(jù)庫的狀態(tài)一致,我們需要引入了一套完整的機制。 現(xiàn)在再讓我們考慮將狀態(tài)保存在 HttpSession 里。通過精心設(shè)計的編程,我們也許能控制session數(shù)據(jù)的大小。 但這遠比聽起來要麻煩的多,因為Web瀏覽器允許特殊的非線性導(dǎo)航。 但假設(shè)我們在系統(tǒng)開發(fā)到一半的時候突然發(fā)現(xiàn)一個需求,它要求用戶可以擁有 多并發(fā)業(yè)務(wù)對話(我就碰到過)。 要開發(fā)一些機制,以分離與不同并發(fā)業(yè)務(wù)會話相關(guān)的session狀態(tài),并引入故障保護,在用戶關(guān)閉瀏覽器窗口或標簽頁時銷毀業(yè)務(wù)會話狀態(tài)。 這對普通人來說可不是一件輕松的事情(我就實現(xiàn)過兩次,一次是為一個客戶應(yīng)用程序,另一次是為Seam,幸好我是出了名的瘋子)。 現(xiàn)在提供一個更好的方法。 Seam引入了 對話上下文 來作為一級構(gòu)件。你能在其中安全地保存業(yè)務(wù)對話狀態(tài),它會保證狀態(tài)有一個定義良好的生命周期。 而且,你不用再不停地在應(yīng)用服務(wù)器和數(shù)據(jù)庫間傳遞數(shù)據(jù),因為業(yè)務(wù)對話上下文就是一個天然的緩存,用來緩存用戶的數(shù)據(jù)。 通常情況下,我們保存在業(yè)務(wù)對話上下文中的組件是有狀態(tài)的Session Bean。(我們也在其中保存實體Bean和JavaBeans。) 在Java社區(qū)中一直有一個謠傳,認為有狀態(tài)的Session Bean是擴展性的殺手。在1998年WebFoobar 1.0發(fā)布時的確如此。 但今天的情況已經(jīng)變了。像JBoss 4.0這樣的應(yīng)用服務(wù)器都有很成熟的機制處理有狀態(tài)Session Bean的狀態(tài)復(fù)制。 (例如,JBoss EJB3容器可以執(zhí)行很細致的復(fù)制,只復(fù)制那些屬性值被改變過的bean。) 請注意,所有那些傳統(tǒng)技術(shù)中關(guān)于有狀態(tài)Bean是低效的爭論也同樣發(fā)生在 HttpSession 上,所以說將狀態(tài)從業(yè)務(wù)層的有狀態(tài)Session Bean遷移到Web Session中以提高性能的做法毫無疑問是被誤導(dǎo)的。 不正確地使用有狀態(tài)的Bean,或者是將它們用在錯誤的地方上都會使應(yīng)用程序變得無法擴展。 但這并不意味著你應(yīng)該 永遠不要 使用它們??傊琒eam會告訴你一個安全使用的模型。歡迎來到2005年。 OK,不再多說了,話題回到這個指南上吧。 賓館預(yù)訂范例演示了不同作用域的有狀態(tài)組件是如何協(xié)同工作實現(xiàn)復(fù)雜的行為的。 它的主頁面允許用戶搜索賓館。搜索的結(jié)果被保存在Seam的session域中。 當用戶導(dǎo)航到其中一個賓館時,一個業(yè)務(wù)會話便開始了,一個業(yè)務(wù)會話域組件回調(diào)session域組件以獲得選中的賓館。 賓館預(yù)訂范例還演示了如何使用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) }
應(yīng)用程序的主頁面是一個Facelets頁面。讓我們來看下與賓館搜索相關(guān)的部分: 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>
這個頁面根據(jù)我們的鍵入動態(tài)地顯示搜索結(jié)果,讓我們選擇一家賓館并將它傳給 HotelBookingAction 的 selectHotel() 方法,這個對象才是 真正 有趣的地方。 現(xiàn)在讓我們來看看賓館預(yù)定范例程序是如何使用一個對話域的有狀態(tài)的Session Bean的,這個Session Bean實現(xiàn)了業(yè)務(wù)會話相關(guān)持久化數(shù)據(jù)的天然緩存。 下面的代碼很長。但如果你把它理解為實現(xiàn)業(yè)務(wù)會話的多個步驟的一系列動作的話,它是不難理解的。我們把這個類當作故事一樣從頭開始閱讀。 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 包含了實現(xiàn)選擇、預(yù)訂和預(yù)訂確認的所有動作監(jiān)聽器方法,并在它的實例變量中保存與之相關(guān)的狀態(tài)。 我們認為你一定會同意這個代碼比起獲取和設(shè)置 HttpSession的屬性來說要簡潔的多。 而且,一個用戶能在每個登錄Session中擁有多個獨立的業(yè)務(wù)對話。試試吧!登錄系統(tǒng),執(zhí)行搜索,在多個瀏覽器標簽頁中導(dǎo)航到不同的賓館頁面。 你能在同一時間建立兩個不同的賓館預(yù)約。如果某個業(yè)務(wù)對話被閑置太長時間,Seam最終會判其超時并銷毀它的狀態(tài)。如果在結(jié)束業(yè)務(wù)對話后, 你按了退回按鈕回到那個會話的某一頁,嘗試執(zhí)行一個動作,Seam會檢測到那個業(yè)務(wù)對話已經(jīng)被結(jié)束了,并將你重定向到搜索頁面。 如果你查看下預(yù)訂系統(tǒng)的WAR文件,你會在 WEB-INF/lib 目錄中找到 seam-ui.jar。 這個包里有許多Seam的JSF自定義控件。本應(yīng)用程序在從搜索界面導(dǎo)航到賓館頁面時使用了 <s:link>控件: <s:link value="View Hotel" action="#{hotelBooking.selectHotel}"/> 這里的 <s:link> 允許我們在不打斷瀏覽器的“在新窗口打開”功能的情況下給HTML鏈接附加上一個動作監(jiān)聽器。 標準的JSF <h:commandLink> 無法在“在新窗口打開”的情況下正常工作。 稍后我們會看到 <s:link> 還能提供很多其他有用的特性,包括業(yè)務(wù)會話傳播規(guī)則。 賓館預(yù)訂系統(tǒng)里還用了些別的Seam和Ajax4JSF控件,特別是在 /book.xhtml 頁面里。我們在這里不深入討論這些控件,如果你想看懂這些代碼,請參考介紹Seam的JSF表單驗證功能的章節(jié)。 WAR文件還包括了 seam-debug.jar。如果把這個jar部屬在 WEB-INF/lib 下,結(jié)合Facelets,你能在 web.xml 或者 seam.properties 里設(shè)置如下的Seam屬性: <context-param> <param-name>org.jboss.seam.core.init.debug</param-name> <param-value>true</param-value> </context-param> 這樣就能訪問Seam調(diào)試頁面了。這個頁面可以讓你瀏覽并檢查任意與你當前登錄Session相關(guān)的Seam上下文中的Seam組件。 只需瀏覽 http://localhost:8080/seam-booking/debug.seam 即可。 ![]() DVD商店程序演示了如何在任務(wù)管理和頁面流中使用jBPM。 用戶界面應(yīng)用jPDL頁面流實現(xiàn)了搜索和購物車功能。 ![]() 管理員界面使用jBPM來管理訂單的審批和送貨周期。業(yè)務(wù)流程可以通過選擇不同的流程定義實現(xiàn)動態(tài)改變。 ![]() TODO 見dvdstore目錄。 Hibernate預(yù)訂系統(tǒng)是之前客房預(yù)訂系統(tǒng)的另一個版本,它使用Hibernate和JavaBeans代替了會話Bean實現(xiàn)持久化。 TODO 見hibernate目錄。 Seam可以很方便地實現(xiàn)在服務(wù)器端保存狀態(tài)的應(yīng)用程序。 然而,服務(wù)器端狀態(tài)在有些情況下并不合適,特別是對那些用來提供內(nèi)容的功能。 針對這類問題,我們常需要讓用戶能夠收藏頁面,有一個相對無狀態(tài)的服務(wù)器,這樣一來能夠在任何時間通過書簽來訪問那些被收藏的頁面。 Blog范例演示了如何用Seam來實現(xiàn)一個RESTful的應(yīng)用程序。應(yīng)用程序中的每個頁面都能被收藏,包括搜索結(jié)果頁面。 ![]() Blog范例演示了“拉”風格("pull"-style)的MVC,它不使用動作監(jiān)聽器方法來獲取數(shù)據(jù)和為視圖準備數(shù)據(jù),而是視圖在被顯示時從組件中拉數(shù)據(jù)。 從 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ù)據(jù)是怎么被初始化的呢? 事實上,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(); } }
這些看起來已經(jīng)很不錯了,那如何來收藏諸如搜索結(jié)果頁這樣的表單提交結(jié)果頁面呢? 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)一個可收藏的搜索結(jié)果頁面,我們需要在處理搜索表單提交后執(zhí)行一個瀏覽器重定向。 因為我們用JSF視圖id作為動作輸出,所以Seam會在表單提交后自動重定向到該表單id。除此之外,我們也能像這樣來定義一個導(dǎo)航規(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來獲得實際搜索結(jié)果: 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ù)據(jù) — blogEntry — 并將它放在Seam事件上下文中。最后顯示以下內(nèi)容: 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ù)據(jù)庫中沒有找到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> 你可以根據(jù)自己的喜好來選擇實現(xiàn)。 |
|