本教程假定你已下載JBoss AS 4.0.5并安裝了EJB 3.0 profile(請(qǐng)使用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 命令來啟動(dòng)JBoss AS。(譯注:此外,請(qǐng)安裝JDK1.5以上以便能直接運(yùn)行示例代碼) 現(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)配置 (當(dāng)然, SEAM也可以脫離Jboss在TOMCAT上直接運(yùn)行)。 至此,就可在Seam安裝目錄 examples/registration 中輸入 ant deploy.tomcat 構(gòu)建和部署示例了。 最后啟動(dòng)Tomcat。 試著在瀏覽器中訪問此鏈接:http://localhost:8080/jboss-seam-registration/。 當(dāng)你部署示例到Tomcat時(shí),任何的EJB3組件將在JBoss的可嵌入式的容器,也就是完全獨(dú)立的EJB3容器環(huán)境中運(yùn)行。 注冊(cè)示例是個(gè)極其普通的應(yīng)用,它可讓新用戶在數(shù)據(jù)庫中保存自己的用戶名,真實(shí)的姓名及密碼。 此示例并不想一下子就把Seam的所有的酷功能全部秀出。然而, 它演示了EJB3 會(huì)話Bean作為JSF動(dòng)作監(jiān)聽器及Seam的基本配置的使用方法。 或許你對(duì)EJB 3.0還不太熟悉,因此我們會(huì)對(duì)示例的慢慢深入說明。 此示例的首頁顯示了一個(gè)非常簡單的表單,它有三個(gè)輸入字段。試著在表單上填寫內(nèi)容并提交,一旦輸入數(shù)據(jù)被提交后就會(huì)在數(shù)據(jù)庫中保存一個(gè)user對(duì)象。 ![]() 本示例由兩個(gè)JSP頁面,一個(gè)實(shí)體Bean及無狀態(tài)的會(huì)話Bean來實(shí)現(xiàn)。 ![]() 讓我們看一下代碼,就從最“底層”的實(shí)體Bean開始吧。 我們需要EJB 實(shí)體Bean來保存用戶數(shù)據(jù)。這個(gè)類通過注解聲明性地定義了 persistence 及 validation 屬性。它也需要一些額外的注解來將這個(gè)類定義為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; } }
這個(gè)例子中最值得注意的是 @Name 和 @Scope 注解,它們確立了這個(gè)類是Seam的組件。 接下來我們將看到 User 類字段在更新模型值階段時(shí)直接被綁定給JSF組件并由JSF操作, 在此并不需要冗余的膠水代碼來在JSP頁面與實(shí)體Bean域模型間來回拷貝數(shù)據(jù)。 然而,實(shí)體Bean不應(yīng)該進(jìn)行事務(wù)管理或數(shù)據(jù)庫訪問。故此,我們無法將此組件作為JSF動(dòng)作監(jiān)聽器,因而需要會(huì)話Bean。 在Seam應(yīng)用中大都采用會(huì)話Bean來作為JSF動(dòng)作監(jiān)聽器(當(dāng)然我們也可選擇JavaBean)。 在我們的應(yīng)用程序中確實(shí)存在一個(gè)JSF動(dòng)作和一個(gè)會(huì)話Bean方法。在此示例中,只有一個(gè)JSF動(dòng)作,并且我們使用會(huì)話Bean方法與之相關(guān)聯(lián)并使用無狀態(tài)Bean,這是由于所有與動(dòng)作相關(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,若沒有顯式指定時(shí),每個(gè)Seam 組件類型就使用其默認(rèn)的作用域。對(duì)于無狀態(tài)的會(huì)話Bean, 其默認(rèn)的作用域就是無狀態(tài)的上下文。實(shí)際上 所有的 無狀態(tài)的會(huì)話Bean都屬于無狀態(tài)的上下文。 會(huì)話Bean的動(dòng)作監(jiān)聽器在此小應(yīng)用中履行了業(yè)務(wù)和持久化邏輯。在更復(fù)雜的應(yīng)用中,我們可能要將代碼分層并重構(gòu)持久化邏輯層成 專用數(shù)據(jù)存取組件,這很容易做到。但請(qǐng)注意Sean并不強(qiáng)制你在應(yīng)用分層時(shí)使用某種特定的分層策略。 此外,也請(qǐng)注意我們的SessionBean會(huì)同步訪問與web請(qǐng)求相關(guān)聯(lián)的上下文(比如在 User 對(duì)象中的表單的值),狀態(tài)會(huì)被保持在事務(wù)型的資源里(EntityManager 對(duì)象)。 這是對(duì)傳統(tǒng)J2EE的體系結(jié)構(gòu)的突破。再次說明,如果你習(xí)慣于傳統(tǒng)J2EE的分層,也可以在你的Seam應(yīng)用實(shí)行。但是對(duì)于許多的應(yīng)用,這是明顯的沒有必要 。 很自然,我們的會(huì)話Bean需要一個(gè)本地接口。 所有的Java代碼就這些了,現(xiàn)在去看一下部署描述文件。 如果你此前曾接觸過許多的Java框架,你就會(huì)習(xí)慣于將所有的組件類放在某種XML文件中來聲明,那些文件就會(huì)隨著項(xiàng)目的不斷成熟而不斷加大到最終到不可收拾的地步。 對(duì)于Seam應(yīng)用,你盡可放心,因?yàn)樗⒉灰髴?yīng)用組件都要有相應(yīng)的XML。大部份的Seam應(yīng)用要求非常少量的XML即可,且XML文件大小不會(huì)隨著項(xiàng)目的增大而快速增長。 無論如何,若能為 某些 組件(特別是Seam內(nèi)置組件)提供某些 外部配置往往是有用的。這樣一來,我們就有幾個(gè)選擇, 但最靈活的選擇還是使用位于 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 屬性。這里需要奇怪的@符號(hào)是因?yàn)锳NT腳本會(huì)在部署應(yīng)用時(shí)將正確的JNDI語法在標(biāo)記處自動(dòng)填補(bǔ) 我們將以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!因?yàn)槲覀兯械膍anaged Bean都是通過經(jīng)過注釋的Seam組件。所以在Seam的應(yīng)用中,faces-config.xml比原始的JSF更少用到。 實(shí)際上,一旦你把所有的基本描述文件配置完畢,你所需寫的 唯一類型的 XML文件就是導(dǎo)航規(guī)則及可能的jBPM流程定義。對(duì)于Seam而言, 流程(process flow) 及 配置數(shù)據(jù) 是唯一真正屬于需要XML定義的。 在此簡單的示例中,因?yàn)槲覀儗⒁晥D頁面的ID嵌入到Action代碼中,所以我們甚至都不需要定義導(dǎo)航規(guī)則。 ejb-jar.xml 文件將 SeamInterceptor 綁定到壓縮包中所有的會(huì)話Bean上,以此實(shí)現(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è)定。此例在程序啟動(dòng)時(shí)自動(dòng)創(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> 對(duì)于Seam應(yīng)用的視圖可由任意支持JSF的技術(shù)來實(shí)現(xiàn)。在此例中,我們使用了JSP,因?yàn)榇蠖鄶?shù)的開發(fā)人員都很熟悉, 且這里并沒有其它太多的要求。(我們建議你在實(shí)際開發(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>標(biāo)簽是Seam特有的。 該JSF組件告訴JSF讓它用實(shí)體Bean中所指定的Hibernat驗(yàn)證器注解來驗(yàn)證所有包含輸入的字段。 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> 這是個(gè)極其普通的使用JSF組件的JSP頁面,與Seam毫無相干。 最后,因?yàn)槲覀兊膽?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。 至此,我們了解了整個(gè)應(yīng)用中 所有的 部署描述文件! 當(dāng)提交表單時(shí),JSF請(qǐng)求Seam來解析名為 user 的變量。由于還沒有值綁定到 user 上(在任意的Seam上下文中), Seam就會(huì)實(shí)例化 user組件,接著把它保存在Seam會(huì)話上下文后,然后將 User 實(shí)體Bean實(shí)例返回給JSF。 表單輸入的值將由在 User 實(shí)體中所指定的Hibernate驗(yàn)證器來驗(yàn)證。 若有非法輸入,JSF就重新顯示當(dāng)前頁面。否則,JSF就將輸入值綁定到 User 實(shí)體Bean的字段上。 接著,JSF請(qǐng)求Seam來解析變量 register。 Seam在無狀態(tài)上下文中找到 RegisterAction 無狀態(tài)的會(huì)話Bean并把它返回。JSF隨之調(diào)用 register() 動(dòng)作監(jiān)聽器方法。 Seam攔截方法調(diào)用并在繼續(xù)調(diào)用之前從Seam會(huì)話上下文注入 User 實(shí)體。 register() 方法檢查所輸入用戶名的用戶是否已存在。 若存在該用戶名,則錯(cuò)誤消息進(jìn)入 facesmessages 組件隊(duì)列,返回?zé)o效結(jié)果并觸發(fā)瀏覽器重顯頁面。facesmessages 組件嵌在消息字符串的JSF表達(dá)式,并將JSF facesmessage 添加到視圖中。 若輸入的用戶不存在,"/registered.jsp" 輸出就會(huì)將瀏覽器重定向到 registered.jsp 頁。 當(dāng)JSF來渲染頁面時(shí),它請(qǐng)求Seam來解析名為 user 的變量,并使用從Seam會(huì)話作用域返回的User 實(shí)體的屬性值。 在幾乎所有的在線應(yīng)用中都免不了將搜索結(jié)果顯示成可點(diǎn)擊的列表。 因此Sean在JSF層之上提供了特殊的功能,使得我們很容易用EJB-QL或HQL來查詢數(shù)據(jù)并用JSF <h:dataTable> 將查詢結(jié)果顯示成可點(diǎn)擊的列表。我們將在接下的例子中演示這一功能。 ![]() 此消息示例中有一個(gè)實(shí)體Bean,Message,一個(gè)會(huì)話Bean MessageListBean 及一個(gè)JSP頁面。 Message 實(shí)體定義了消息的title,text,date和time以及該消息是否已讀的標(biāo)志: 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; } } 如此前的例子,會(huì)話Bean MessageManagerBean 用來給表單中的兩個(gè)按鈕定義個(gè)動(dòng)作監(jiān)聽器方法, 其中的一個(gè)按鈕用來從列表中選擇消息,并顯示該消息。而另一個(gè)按鈕則用來刪除一條消息,除此之外,就沒什么特別之處了。 在用戶第一次瀏覽消息頁面時(shí),MessageManagerBean 會(huì)話Bean也負(fù)責(zé)抓取消息列表,考慮到用戶可能以多種方式來瀏覽該頁面,他們也有可能不是由JSF動(dòng)作來完成,比如用戶可能將該頁加入收藏夾。 因此抓取消息列表發(fā)生在Seam的工廠方法中,而不是在動(dòng)作監(jiān)聽器方法中。 之所以將此會(huì)話Bean設(shè)為有狀態(tài)的,是因?yàn)槲覀兿朐诓煌姆?wù)器請(qǐng)求間緩存此消息列表。 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() {} }
請(qǐng)注意,這是個(gè)會(huì)話作用域的Seam組件。它與用戶登入會(huì)話相關(guān)聯(lián),并且登入會(huì)話的所有請(qǐng)求共享同一個(gè)組件的實(shí)例。 (在Seam的應(yīng)用中,我們通常使用會(huì)話作用域的組件。) 當(dāng)然,每個(gè)會(huì)話Bean都有個(gè)業(yè)務(wù)接口。 @Local public interface MessageManager { public void findMessages(); public void select(); public void delete(); public void destroy(); } 從現(xiàn)在起,我們?cè)谑纠a中將不再對(duì)本地接口作特別的說明。 由于XML文件與此前的示例幾乎都一樣,因此我們略過了 components.xml、persistence.xml、 web.xml、ejb-jar.xml、faces-config.xml 及application.xml 的細(xì)節(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> 當(dāng)我們首次瀏覽 messages.jsp 頁面時(shí),無論是否由回傳(postback)的JSF(頁面請(qǐng)求)或?yàn)g覽器直接的GET請(qǐng)求(非頁面請(qǐng)求),此JSP頁面將設(shè)法解析 messagelist 上下文變量。 由于上下文變量尚未被初始化,因此Seam將調(diào)用工廠方法 findmessages(),該方法執(zhí)行了一次數(shù)據(jù)庫查詢并導(dǎo)致 DataModel 被向外注入。 DataModel 提供了渲染 <h:dataTable> 所需的行數(shù)據(jù)。 當(dāng)用戶點(diǎn)擊 <h:commandLink> 時(shí),JSF就調(diào)用 Select() 動(dòng)作監(jiān)聽器。 Seam攔截此調(diào)用并將所選行的數(shù)據(jù)注入給 messageManager 組件的 message 屬性。 而動(dòng)作監(jiān)聽器將所選定的 Message標(biāo)為已讀。在此調(diào)用結(jié)束時(shí),Seam向外注入所選定的 Message 給名為 message 的變量。 接著,EJB容器提交事務(wù),將 Message 的已讀標(biāo)記寫入數(shù)據(jù)庫。 最后,該網(wǎng)頁重新渲染,再次顯示消息列表,并在列表下方顯示所選消息的內(nèi)容。 如果用戶點(diǎn)擊了 <h:commandButton>,JSF就調(diào)用 delete() 動(dòng)作監(jiān)聽器。 Seam攔截此調(diào)用并將所選行的數(shù)據(jù)注入給 messageManager 組件的 message 屬性。 觸發(fā)動(dòng)作監(jiān)聽器,將選定的Message 從列表中刪除并同時(shí)在 EntityManager 中調(diào)用 remove() 方法。在此調(diào)用的最后,Seam刷新 messageList 上下文變量并清除名為 message 的上下文變量。 接著,EJB容器提交事務(wù),將 Message 從數(shù)據(jù)庫中刪除。最后,該網(wǎng)頁重新渲染,再次顯示消息列表。 jBPM提供了先進(jìn)的工作流程和任務(wù)管理的功能。為了體驗(yàn)一下jBPM是如何與Seam集成在一起工作的,在此將給你一個(gè)簡單的管理“待辦事項(xiàng)列表”的應(yīng)用。由于管理任務(wù)列表等功能是jBPM的核心功能,所以在此例中只用了很少的Java代碼。 ![]() 這個(gè)例子的核心是jBPM的流程定義(process definition)。此外,還有兩個(gè)JSP頁面和兩個(gè)簡單的JavaBeans(由于他們不用訪問數(shù)據(jù)庫,或有其它事務(wù)相關(guān)的行為,因此并沒有用會(huì)話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所提供的流程定義編輯器來查看此流程定義,那它就會(huì)是這樣: ![]() 這個(gè)文檔將我們的 業(yè)務(wù)流程 定義成節(jié)點(diǎn)圖。 這可能是最常見的業(yè)務(wù)流程:只有一個(gè) 任務(wù) 被執(zhí)行,當(dāng)這項(xiàng)任務(wù)完成之后,業(yè)務(wù)流程就結(jié)束了。 第一個(gè)JavaBean處理登入界面 login.jsp。 它的工作就是用 actor 組件初始化jBPM用戶id(在實(shí)際的應(yīng)用中,它也需要驗(yàn)證用戶。) 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> 第二個(gè)JavaBean負(fù)責(zé)啟動(dòng)業(yè)務(wù)流程實(shí)例及結(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() {} }
在實(shí)際的應(yīng)用中,@StartTask 及 @EndTask 不會(huì)出現(xiàn)在同一個(gè)方法中,因?yà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> 讓我們對(duì)此逐一加以說明。 該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> 列表中的每個(gè)元素就是一個(gè)jBPM類 taskinstance 的實(shí)例。 以下代碼簡單地展示了列表中每一任務(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 的動(dòng)作方法來結(jié)束任務(wù)。它把任務(wù)id作為請(qǐng)求參數(shù)傳給Seam: <h:column> <s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/> </h:column> (請(qǐng)注意,這是在使用Seam seam-ui.jar 包中的JSF <s:button> 控件。) 這個(gè)按鈕是用來更新任務(wù)屬性。當(dāng)提交表單時(shí),Seam和jBPM將直接更改任務(wù)的持久化,不需要任何的動(dòng)作監(jiān)聽器方法: <h:commandButton value="Update Items" action="update"/> 第二個(gè)表單通過調(diào)用注解為 @CreateProcess的動(dòng)作方法來創(chuàng)建新的項(xiàng)目(item)。 <h:form id="new"> <div> <h:inputText value="#{todoList.description}"/> <h:commandButton value="Create New Item" action="#{todoList.createTodo}"/> </div> </h:form> 這個(gè)例子還需要另外幾個(gè)文件,但它們只是標(biāo)準(zhǔn)的jBPM和Seam配置并不是很有趣。 對(duì)有相對(duì)自由(特別)導(dǎo)航的Seam應(yīng)用程序而言,JSF/Seam導(dǎo)航規(guī)則是定義頁面流的一個(gè)完美的方法。 而對(duì)于那些帶有更多約束的導(dǎo)航,特別是帶狀態(tài)的用戶界面而言,導(dǎo)航規(guī)則反而使得系統(tǒng)流程變得難以理解。 要理解整個(gè)流程,你需要從視圖頁面、動(dòng)作和導(dǎo)航規(guī)則里一點(diǎn)點(diǎn)把它拼出來。 Seam允許你使用一個(gè)jPDL流程定義來定義頁面流。下面這個(gè)簡單的猜數(shù)字范例將演示這一切是如何實(shí)現(xiàn)的。 ![]() 這個(gè)例子由一個(gè)JavaBean、三個(gè)JSP頁面和一個(gè)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>
這個(gè)頁面流在JBossIDE頁面流編輯器里看上去是這個(gè)樣子的: ![]() 看過了頁面流,現(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> 請(qǐng)注意名為 guess 的命令按鈕是如何進(jìn)行轉(zhuǎn)換而不是直接調(diào)用一個(gè)動(dòng)作的。 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; } }
如你所見,這個(gè)Seam組件是純業(yè)務(wù)邏輯的!它不需要知道任何關(guān)于用戶交互的東西。這點(diǎn)使得組件更易被復(fù)用。 該系統(tǒng)是一個(gè)完整的賓館客房預(yù)訂系統(tǒng),它由下列功能組成:
![]() 應(yīng)用程序中使用了JSF、EJB 3.0和Seam,視圖部分結(jié)合了Facelets。也可以選擇使用JSF、Facelets、Seam、JavaBeans和Hibernate3。 在使用過一段時(shí)間后你會(huì)發(fā)現(xiàn)該應(yīng)用程序非常 健壯。你能使用回退按鈕、刷新瀏覽器、打開多個(gè)窗口, 或者鍵入各種無意義的數(shù)據(jù),會(huì)發(fā)現(xiàn)都很難讓它崩潰。你也許會(huì)想我們花了幾個(gè)星期測(cè)試修復(fù)該系統(tǒng)才達(dá)到了這個(gè)目標(biāo)。 事實(shí)卻不是這樣的,Seam的設(shè)計(jì)使你能夠用它方便地構(gòu)建健壯的web應(yīng)用程序,而且Seam還提供了很多以前需要通過編碼才能實(shí)現(xiàn)的健壯性。 在你瀏覽范例程序代碼研究它是如何運(yùn)行時(shí),注意觀察聲明式的狀態(tài)管理和集成的驗(yàn)證是如何被用來實(shí)現(xiàn)這種健壯性的。 這個(gè)項(xiàng)目的結(jié)構(gòu)和上一個(gè)一樣,要安裝部署該應(yīng)用程序請(qǐng)參考Section 1.1, “試試看”。 當(dāng)應(yīng)用程序啟動(dòng)后,可以通過 http://localhost:8080/seam-booking/ 進(jìn)行訪問。 只需要用9個(gè)類(加上6個(gè)Session Bean的本地接口)就能實(shí)現(xiàn)這個(gè)應(yīng)用程序。6個(gè)Session Bean動(dòng)作監(jiān)聽器包括了以下功能的所有業(yè)務(wù)邏輯。
應(yīng)用程序的持久化模型由三個(gè)實(shí)體bean實(shí)現(xiàn)。
我們鼓勵(lì)您隨意瀏覽源代碼。在這個(gè)教程里我們將關(guān)注功能中的某一特定部分:賓館搜索、選擇、預(yù)訂和確認(rèn)。 從用戶的角度來看,從選擇賓館到確認(rèn)的每一步都是工作中的一個(gè)連續(xù)單元,屬于一個(gè) 業(yè)務(wù)對(duì)話。 然而搜索卻 不 是該對(duì)話的一部分。用戶能在不同瀏覽器標(biāo)簽頁中的相同搜索結(jié)果頁面中選擇多個(gè)賓館。 大多數(shù)Web應(yīng)用程序架構(gòu)沒有提供表示業(yè)務(wù)對(duì)話的一級(jí)構(gòu)件(first class construct)。這在管理與對(duì)話相關(guān)的狀態(tài)時(shí)帶來了很多麻煩。 通常情況下,Java的Web應(yīng)用程序結(jié)合兩種技術(shù)來應(yīng)對(duì)這一情況:一是將某些狀態(tài)丟入 HttpSession;二是將可持久化的狀態(tài)在每個(gè)請(qǐng)求(Request)后寫入數(shù)據(jù)庫,并在每個(gè)新請(qǐng)求的開始將之重建。 由于數(shù)據(jù)庫是最不可擴(kuò)展的一層,因此這么做往往導(dǎo)致完全無法接受的擴(kuò)展性低下。在每次請(qǐng)求時(shí)訪問數(shù)據(jù)庫所造成的額外流量和等待時(shí)間也是一個(gè)問題。 要降低冗余流量,Java應(yīng)用程序常引入一個(gè)(二級(jí))數(shù)據(jù)緩存來保存被經(jīng)常訪問的數(shù)據(jù)。 然而這個(gè)緩存是很低效的,因?yàn)樗氖惴ㄊ腔贚RU(最近最少使用)策略,而不是基于用戶何時(shí)結(jié)束與該數(shù)據(jù)相關(guān)的工作。 此外,由于該緩存被許多并發(fā)事務(wù)共享,要保持緩存與數(shù)據(jù)庫的狀態(tài)一致,我們需要引入了一套完整的機(jī)制。 現(xiàn)在再讓我們考慮將狀態(tài)保存在 HttpSession 里。通過精心設(shè)計(jì)的編程,我們也許能控制session數(shù)據(jù)的大小。 但這遠(yuǎn)比聽起來要麻煩的多,因?yàn)閃eb瀏覽器允許特殊的非線性導(dǎo)航。 但假設(shè)我們?cè)谙到y(tǒng)開發(fā)到一半的時(shí)候突然發(fā)現(xiàn)一個(gè)需求,它要求用戶可以擁有 多并發(fā)業(yè)務(wù)對(duì)話(我就碰到過)。 要開發(fā)一些機(jī)制,以分離與不同并發(fā)業(yè)務(wù)會(huì)話相關(guān)的session狀態(tài),并引入故障保護(hù),在用戶關(guān)閉瀏覽器窗口或標(biāo)簽頁時(shí)銷毀業(yè)務(wù)會(huì)話狀態(tài)。 這對(duì)普通人來說可不是一件輕松的事情(我就實(shí)現(xiàn)過兩次,一次是為一個(gè)客戶應(yīng)用程序,另一次是為Seam,幸好我是出了名的瘋子)。 現(xiàn)在提供一個(gè)更好的方法。 Seam引入了 對(duì)話上下文 來作為一級(jí)構(gòu)件。你能在其中安全地保存業(yè)務(wù)對(duì)話狀態(tài),它會(huì)保證狀態(tài)有一個(gè)定義良好的生命周期。 而且,你不用再不停地在應(yīng)用服務(wù)器和數(shù)據(jù)庫間傳遞數(shù)據(jù),因?yàn)闃I(yè)務(wù)對(duì)話上下文就是一個(gè)天然的緩存,用來緩存用戶的數(shù)據(jù)。 通常情況下,我們保存在業(yè)務(wù)對(duì)話上下文中的組件是有狀態(tài)的Session Bean。(我們也在其中保存實(shí)體Bean和JavaBeans。) 在Java社區(qū)中一直有一個(gè)謠傳,認(rèn)為有狀態(tài)的Session Bean是擴(kuò)展性的殺手。在1998年WebFoobar 1.0發(fā)布時(shí)的確如此。 但今天的情況已經(jīng)變了。像JBoss 4.0這樣的應(yīng)用服務(wù)器都有很成熟的機(jī)制處理有狀態(tài)Session Bean的狀態(tài)復(fù)制。 (例如,JBoss EJB3容器可以執(zhí)行很細(xì)致的復(fù)制,只復(fù)制那些屬性值被改變過的bean。) 請(qǐng)注意,所有那些傳統(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,或者是將它們用在錯(cuò)誤的地方上都會(huì)使應(yīng)用程序變得無法擴(kuò)展。 但這并不意味著你應(yīng)該 永遠(yuǎn)不要 使用它們??傊?,Seam會(huì)告訴你一個(gè)安全使用的模型。歡迎來到2005年。 OK,不再多說了,話題回到這個(gè)指南上吧。 賓館預(yù)訂范例演示了不同作用域的有狀態(tài)組件是如何協(xié)同工作實(shí)現(xiàn)復(fù)雜的行為的。 它的主頁面允許用戶搜索賓館。搜索的結(jié)果被保存在Seam的session域中。 當(dāng)用戶導(dǎo)航到其中一個(gè)賓館時(shí),一個(gè)業(yè)務(wù)會(huì)話便開始了,一個(gè)業(yè)務(wù)會(huì)話域組件回調(diào)session域組件以獲得選中的賓館。 賓館預(yù)訂范例還演示了如何使用Ajax4JSF在不用手工編寫JavaScript的情況下實(shí)現(xiàn)富客戶端(Rich Client)行為。 搜索功能用了一個(gè)Session域的有狀態(tài)Session Bean來實(shí)現(xiàn),有點(diǎn)類似于我們?cè)谏厦娴南⒘斜矸独锟吹降哪莻€(gè)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)用程序的主頁面是一個(gè)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>
這個(gè)頁面根據(jù)我們的鍵入動(dòng)態(tài)地顯示搜索結(jié)果,讓我們選擇一家賓館并將它傳給 HotelBookingAction 的 selectHotel() 方法,這個(gè)對(duì)象才是 真正 有趣的地方。 現(xiàn)在讓我們來看看賓館預(yù)定范例程序是如何使用一個(gè)對(duì)話域的有狀態(tài)的Session Bean的,這個(gè)Session Bean實(shí)現(xiàn)了業(yè)務(wù)會(huì)話相關(guān)持久化數(shù)據(jù)的天然緩存。 下面的代碼很長。但如果你把它理解為實(shí)現(xiàn)業(yè)務(wù)會(huì)話的多個(gè)步驟的一系列動(dòng)作的話,它是不難理解的。我們把這個(gè)類當(dāng)作故事一樣從頭開始閱讀。 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 包含了實(shí)現(xiàn)選擇、預(yù)訂和預(yù)訂確認(rèn)的所有動(dòng)作監(jiān)聽器方法,并在它的實(shí)例變量中保存與之相關(guān)的狀態(tài)。 我們認(rèn)為你一定會(huì)同意這個(gè)代碼比起獲取和設(shè)置 HttpSession的屬性來說要簡潔的多。 而且,一個(gè)用戶能在每個(gè)登錄Session中擁有多個(gè)獨(dú)立的業(yè)務(wù)對(duì)話。試試吧!登錄系統(tǒng),執(zhí)行搜索,在多個(gè)瀏覽器標(biāo)簽頁中導(dǎo)航到不同的賓館頁面。 你能在同一時(shí)間建立兩個(gè)不同的賓館預(yù)約。如果某個(gè)業(yè)務(wù)對(duì)話被閑置太長時(shí)間,Seam最終會(huì)判其超時(shí)并銷毀它的狀態(tài)。如果在結(jié)束業(yè)務(wù)對(duì)話后, 你按了退回按鈕回到那個(gè)會(huì)話的某一頁,嘗試執(zhí)行一個(gè)動(dòng)作,Seam會(huì)檢測(cè)到那個(gè)業(yè)務(wù)對(duì)話已經(jīng)被結(jié)束了,并將你重定向到搜索頁面。 如果你查看下預(yù)訂系統(tǒng)的WAR文件,你會(huì)在 WEB-INF/lib 目錄中找到 seam-ui.jar。 這個(gè)包里有許多Seam的JSF自定義控件。本應(yīng)用程序在從搜索界面導(dǎo)航到賓館頁面時(shí)使用了 <s:link>控件: <s:link value="View Hotel" action="#{hotelBooking.selectHotel}"/> 這里的 <s:link> 允許我們?cè)诓淮驍酁g覽器的“在新窗口打開”功能的情況下給HTML鏈接附加上一個(gè)動(dòng)作監(jiān)聽器。 標(biāo)準(zhǔn)的JSF <h:commandLink> 無法在“在新窗口打開”的情況下正常工作。 稍后我們會(huì)看到 <s:link> 還能提供很多其他有用的特性,包括業(yè)務(wù)會(huì)話傳播規(guī)則。 賓館預(yù)訂系統(tǒng)里還用了些別的Seam和Ajax4JSF控件,特別是在 /book.xhtml 頁面里。我們?cè)谶@里不深入討論這些控件,如果你想看懂這些代碼,請(qǐng)參考介紹Seam的JSF表單驗(yàn)證功能的章節(jié)。 WAR文件還包括了 seam-debug.jar。如果把這個(gè)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)試頁面了。這個(gè)頁面可以讓你瀏覽并檢查任意與你當(dāng)前登錄Session相關(guān)的Seam上下文中的Seam組件。 只需瀏覽 http://localhost:8080/seam-booking/debug.seam 即可。 ![]() DVD商店程序演示了如何在任務(wù)管理和頁面流中使用jBPM。 用戶界面應(yīng)用jPDL頁面流實(shí)現(xiàn)了搜索和購物車功能。 ![]() 管理員界面使用jBPM來管理訂單的審批和送貨周期。業(yè)務(wù)流程可以通過選擇不同的流程定義實(shí)現(xiàn)動(dòng)態(tài)改變。 ![]() TODO 見dvdstore目錄。 Hibernate預(yù)訂系統(tǒng)是之前客房預(yù)訂系統(tǒng)的另一個(gè)版本,它使用Hibernate和JavaBeans代替了會(huì)話Bean實(shí)現(xiàn)持久化。 TODO 見hibernate目錄。 Seam可以很方便地實(shí)現(xiàn)在服務(wù)器端保存狀態(tài)的應(yīng)用程序。 然而,服務(wù)器端狀態(tài)在有些情況下并不合適,特別是對(duì)那些用來提供內(nèi)容的功能。 針對(duì)這類問題,我們常需要讓用戶能夠收藏頁面,有一個(gè)相對(duì)無狀態(tài)的服務(wù)器,這樣一來能夠在任何時(shí)間通過書簽來訪問那些被收藏的頁面。 Blog范例演示了如何用Seam來實(shí)現(xiàn)一個(gè)RESTful的應(yīng)用程序。應(yīng)用程序中的每個(gè)頁面都能被收藏,包括搜索結(jié)果頁面。 ![]() Blog范例演示了“拉”風(fēng)格("pull"-style)的MVC,它不使用動(dòng)作監(jiān)聽器方法來獲取數(shù)據(jù)和為視圖準(zhǔn)備數(shù)據(jù),而是視圖在被顯示時(shí)從組件中拉數(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> 如果我們通過收藏夾訪問這個(gè)頁面,那么 <h:dataTable> 的數(shù)據(jù)是怎么被初始化的呢? 事實(shí)上,Blog 是延遲加載的,即在需要時(shí)才被名為 blog 的Seam組件“拉”出來。 這與傳統(tǒng)的基于動(dò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)很不錯(cuò)了,那如何來收藏諸如搜索結(jié)果頁這樣的表單提交結(jié)果頁面呢? Blog范例在每個(gè)頁面的右上方都有一個(gè)很小的表單,這個(gè)表單允許用戶搜索文章。 這是定義在一個(gè)名為 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> 要實(shí)現(xiàn)一個(gè)可收藏的搜索結(jié)果頁面,我們需要在處理搜索表單提交后執(zhí)行一個(gè)瀏覽器重定向。 因?yàn)槲覀冇肑SF視圖id作為動(dòng)作輸出,所以Seam會(huì)在表單提交后自動(dòng)重定向到該表單id。除此之外,我們也能像這樣來定義一個(gè)導(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> 然后表單看起來會(huì)是這個(gè)樣子的: Example 1.29. <div id="search"> <h:form> <h:inputText value="#{searchAction.searchPattern}"/> <h:commandButton value="Search" action="searchResults"/> </h:form> </div> 在重定向時(shí),我們需要將表單的值作為請(qǐng)求參數(shù)包括進(jìn)來,得到的書簽URL會(huì)是這個(gè)樣子: http://localhost:8080/seam-blog/search.seam?searchPattern=seam。 JSF沒有為此提供一個(gè)簡單的途徑,但Seam卻有。我們能在 WEB-INF/pages.xml 中定義一個(gè) 頁面參數(shù): Example 1.30. <pages> <page view-id="/search.xhtml"> <param name="searchPattern" value="#{searchService.searchPattern}"/> </page> ... </pages> 這告訴Seam在重定向時(shí)將 #{searchService.searchPattern} 的值作為名字是 searchPattern 的請(qǐng)求參數(shù)包括進(jìn)去,并在顯示頁面前重新將這個(gè)值賦上。 重定向會(huì)把我們帶到 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> 此處同樣使用“拉”風(fēng)格的MVC來獲得實(shí)際搜索結(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; } } 有些時(shí)候,用“推”風(fēng)格的MVC來處理RESTful頁面更有意義,為此Seam提供了 頁面動(dòng)作。 Blog范例在文章頁面 entry.xhtml 里使用了頁面動(dòng)作。請(qǐng)注意這里是故意這么做的,因?yàn)榇颂幨褂?#8220;拉”風(fēng)格的MVC會(huì)更容易。 entryAction 組件工作起來非常像傳統(tǒng)“推”風(fēng)格MVC的面向動(dòng)作框架例如Struts里的動(dòng)作類(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 里也定義了頁面動(dòng)作: 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> 范例中還將頁面動(dòng)作運(yùn)用于一些其他的功能上 — 登錄和頁面訪問記數(shù)器。另外一點(diǎn)值得注意的是在頁面動(dòng)作綁定中使用了一個(gè)參數(shù)。 這不是標(biāo)準(zhǔn)的JSF EL,是Seam為你提供的,你不僅能在頁面動(dòng)作中使用它,還可以將它使用在JSF方法綁定中。 當(dāng) entry.xhtml 頁面被請(qǐng)求時(shí),Seam先為模型綁定上頁面參數(shù) blogEntryId,然后運(yùn)行頁面動(dòng)作,該動(dòng)作獲取所需的數(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,就會(huì)拋出 EntryNotFoundException 異常。 我們想讓該異常引起一個(gè)404錯(cuò)誤,而非505,所以為這個(gè)異常類添加個(gè)注解: 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); } } 該范例的另一個(gè)實(shí)現(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ù)自己的喜好來選擇實(shí)現(xiàn)。 |
|