日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

JBoss Seam 入門

 gyb98 2010-12-31

Chapter 1. Seam 入門

1.1. 試試看

本教程假定你已下載JBoss AS 4.0.5并安裝了EJB 3.0 profile(請使用JBoss AS安裝器)。你也得下載一份Seam并解壓到工作目錄上。

各示例的目錄結(jié)構(gòu)仿效以下形式:

  • 網(wǎng)頁、圖片及樣式表可在 examples/registration/view 目錄中找到。

  • 諸如部署描述文件及數(shù)據(jù)導(dǎo)入腳本之類的資源可在目錄 examples/registration/resources 中找到。

  • Java源代碼保存在 examples/registration/src 中。

  • Ant構(gòu)建腳本放在 examples/registration/build.xml 文件中。

1.1.1. 在JBoss AS上運行示例

第一步,確保已安裝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/。

1.1.2. 在Tomcat服務(wù)器上運行示例

首先,確保已安裝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)境中運行。

1.1.3. 運行測試

幾乎所有的示例都有相應(yīng)的TestNG的集成測試代碼。最簡便的運行測試代碼的方法是在 examples/registration目錄中運行 ant testexample。當然也可在IDE開發(fā)工具中使用TestNG插件來運行測試。

1.2. 第一個例子:注冊示例

注冊示例是個極其普通的應(yīng)用,它可讓新用戶在數(shù)據(jù)庫中保存自己的用戶名,真實的姓名及密碼。 此示例并不想一下子就把Seam的所有的酷功能全部秀出。然而, 它演示了EJB3 會話Bean作為JSF動作監(jiān)聽器及Seam的基本配置的使用方法。

或許你對EJB 3.0還不太熟悉,因此我們會對示例的慢慢深入說明。

此示例的首頁顯示了一個非常簡單的表單,它有三個輸入字段。試著在表單上填寫內(nèi)容并提交,一旦輸入數(shù)據(jù)被提交后就會在數(shù)據(jù)庫中保存一個user對象。

1.2.1. 了解代碼

本示例由兩個JSP頁面,一個實體Bean及無狀態(tài)的會話Bean來實現(xiàn)。

讓我們看一下代碼,就從最“底層”的實體Bean開始吧。

1.2.1.1. 實體Bean:User.java

我們需要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;
   }

}
(1)

EJB3標準注解 @Entity 表明了 User 類是個實體Bean.

(2)

Seam組件需要一個 組件名稱,此名稱由注解 @Name來指定。此名稱必須在Seam應(yīng)用內(nèi)唯一。當JSF用一個與組件同名的名稱去請求Seam來解析上下文變量, 且該上下文變量尚未定義(null)時,Seam就將實例化那個組件,并將新實例綁定給上下文變量。 在此例中,Seam將在JSF第一次遇到名為 user 的變量時實例化 User

(3)

每當Seam實例化一個組件時,它就將始化后的實例綁定給組件中 默認上下文 的上下文變量。默認的上下文由 @Scope注解指定。 User Bean是個會話作用域的組件。

(4)

EJB標準注解@Table 表明了將 User 類映射到 users 表上。

(5)

name、 password 及 username 都是實體Bean的持久化屬性。所有的持久化屬性都定義了訪問方法。當JSF渲染輸出及更新模型值階段時需要調(diào)用該組件的這些方法。

(6)

EJB和Seam都要求有空的構(gòu)造器。

(7)

@NotNull 和 @Length 注解是Hibernate Validator框架的組成部份, Seam集成了Hibernate Validator并讓你用它來作為數(shù)據(jù)校驗(盡管你可能并不使用Hibernate作為持久化層)。

(8)

標準EJB注解 @Id 表明了實體Bean的主鍵屬性。

這個例子中最值得注意的是 @Name 和 @Scope 注解,它們確立了這個類是Seam的組件。

接下來我們將看到 User 類字段在更新模型值階段時直接被綁定給JSF組件并由JSF操作, 在此并不需要冗余的膠水代碼來在JSP頁面與實體Bean域模型間來回拷貝數(shù)據(jù)。

然而,實體Bean不應(yīng)該進行事務(wù)管理或數(shù)據(jù)庫訪問。故此,我們無法將此組件作為JSF動作監(jiān)聽器,因而需要會話Bean。

1.2.1.2. 無狀態(tài)會話Bean:RegisterAction.java

在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;
      }
   }

}
(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ù)據(jù)庫交互,并返回JSF的輸出結(jié)果。 請注意,由于這是個會話Bean,因此當 register() 方法被調(diào)用時事務(wù)就會自動開始,并在結(jié)束時提交(commit)。

(6)

請注意Seam讓你在EJB-QL中使用JSF EL表達式。因此可在標準JPA Query 對象上調(diào)用普通的JPA setParameter() 方法,這樣豈不妙哉?

(7)

Log API為顯示模板化的日志消息提供了便利。

(8)

多個JSF動作監(jiān)聽器方法返回一個字符串值的輸出,它決定了接下來應(yīng)顯示的頁面內(nèi)容。 空輸出(或返回值為空的動作監(jiān)聽器方法)重新顯示上一頁的內(nèi)容。 在普通的JSF中,用JSF的導(dǎo)航規(guī)則(navigation rule) 來決定輸出結(jié)果的JSF視圖id是很常用的。 這種間接性對于復(fù)雜的應(yīng)用是非常有用的,值得去實踐。但是,對于象示例這樣簡單的的應(yīng)用,Seam讓你使用JSF視圖id作為輸出結(jié)果,以減少對導(dǎo)航規(guī)則的需求。請注意,當你用視圖id作為輸出結(jié)果時,Seam總會執(zhí)行一次瀏覽器的重定向。

(9)

Seam提供了大量的 內(nèi)置組件(built-in components) 來協(xié)助解決那些經(jīng)常遇到的問題。 用 FacesMessages 組件就可很容易地來顯示模板化的錯誤或成功的消息。 內(nèi)置的Seam組件還可由注入或通過調(diào)用 instance() 方法來獲取。

這次我們并沒有顯式指定 @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)用,這是明顯的沒有必要 。

1.2.1.3. 會話Bean的本地接口:Register.java

很自然,我們的會話Bean需要一個本地接口。

Example 1.3. 

@Local
public interface Register
{
   public String register();
}

所有的Java代碼就這些了,現(xiàn)在去看一下部署描述文件。

1.2.1.4. Seam組件部署描述文件:components.xml

如果你此前曾接觸過許多的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語法在標記處自動填補

1.2.1.5. Web部署描述文件:web.xml

我們將以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)用中的配置與此處的配置基本相同。

1.2.1.6. JSF配置:faces-config.xml

絕大多數(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ī)則。

1.2.1.7. EJB部署描述文件:ejb-jar.xml

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>

1.2.1.8. EJB持久化部署描述文件:persistence.xml

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>

1.2.1.9. 視圖:register.xhtml 和 registered.xhtml

對于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毫無相干。

1.2.1.10. EAR部署描述文件:application.xml

最后,因為我們的應(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)用中 所有的 部署描述文件!

1.2.2. 工作原理

當提交表單時,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 實體的屬性值。

1.3. Seam中的可點擊列表:消息示例

在幾乎所有的在線應(yīng)用中都免不了將搜索結(jié)果顯示成可點擊的列表。 因此Sean在JSF層之上提供了特殊的功能,使得我們很容易用EJB-QL或HQL來查詢數(shù)據(jù)并用JSF <h:dataTable> 將查詢結(jié)果顯示成可點擊的列表。我們將在接下的例子中演示這一功能。

1.3.1. 理解代碼

此消息示例中有一個實體Bean,Message,一個會話Bean MessageListBean 及一個JSP頁面。

1.3.1.1. 實體Bean:Message.java

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;
   }

}

1.3.1.2. 有狀態(tài)的會話Bean:MessageManagerBean.java

如此前的例子,會話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() {}

}
(1)

注解 @DataModel 暴露了 java.util.List 類型的屬性給JSF頁面來作為 javax.faces.model.DataModel 的實例。 這允許我們在JSF <h:dataTable>的每一行中能使用可點擊列表。在此例中,DataModel 可在變量名為 messageList 的會話上下文中被使用。

(2)

@DataModelSelection 注解告訴了Seam來注入 List 元素到相應(yīng)的被點擊鏈接。

(3)

注解 @Out 直接暴露了被選中的值給頁面。 這樣一來,每次可點擊列表一旦被選中,Message 就被會注入給有狀態(tài)Bean的屬性,緊接著 向外注入(outjected)給變量名為message 的事件上下文的屬性。

(4)

此有狀態(tài)Bean有個EJB3的 擴展持久化上下文(extended persistence context)。只要Bean存在,查詢中獲取的消息就會保留在受管理的狀態(tài)中。 這樣一來,此后對有狀態(tài)Bean的所有方法調(diào)用勿需顯式調(diào)用 EntityManager 就可更新這些消息了。

(5)

當我們第一次瀏覽JSP頁面時,messageList 上下文變量尚未被初始化,@Factory 注解告訴Seam來創(chuàng)建 MessageManagerBean 的實例并調(diào)用 findMessages() 方法來初始化上下文變量。 我們把 findMessages() 當作 messages 的 工廠方法。

(6)

select() 將選中的 Message 標為已讀,并同時更新數(shù)據(jù)庫。

(7)

delete() 動作監(jiān)聽器方法將選中的 Message 從數(shù)據(jù)庫中刪除。

(8)

對于每個有狀態(tài)的會話Bean,Seam組件的所有方法中 必須 有一不帶參數(shù)的方法被標為 @Remove @Destroy 以確保在Seam的上下文結(jié)束時刪除有狀態(tài)Bean,并同時清除所有服務(wù)器端的狀態(tài)。

請注意,這是個會話作用域的Seam組件。它與用戶登入會話相關(guān)聯(lián),并且登入會話的所有請求共享同一個組件的實例。 (在Seam的應(yīng)用中,我們通常使用會話作用域的組件。)

1.3.1.3. 會話Bean的本地接口:MessageManager.java

當然,每個會話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。

1.3.1.4. 視圖:messages.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>

1.3.2. 工作原理

當我們首次瀏覽 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)頁重新渲染,再次顯示消息列表。

1.4. Seam和jBPM:待辦事項列表(todo list)示例

jBPM提供了先進的工作流程和任務(wù)管理的功能。為了體驗一下jBPM是如何與Seam集成在一起工作的,在此將給你一個簡單的管理“待辦事項列表”的應(yīng)用。由于管理任務(wù)列表等功能是jBPM的核心功能,所以在此例中只用了很少的Java代碼。

1.4.1. 理解代碼

這個例子的核心是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>
(1)

節(jié)點 <start-state> 代表流程的邏輯開始。一旦流程開始時,它就立即轉(zhuǎn)入 todo節(jié)點。

(2)

<task-node> 節(jié)點代表 等待狀態(tài),就是在執(zhí)行業(yè)務(wù)流程暫停時,等待一個或多個未完成的任務(wù)。

(3)

<task> 元素定義了用戶需要完成的任務(wù)。 由于在這個節(jié)點只有定義了一個任務(wù),當它完成,或恢復(fù)執(zhí)行時我們就轉(zhuǎn)入結(jié)束狀態(tài)。 此任務(wù)從Seam中名為 todolist 的組件(JavaBeans之一)獲得任務(wù)description。

(4)

任務(wù)在創(chuàng)建時就會被分配給一個用戶或一組用戶時。在此示例中,任務(wù)是分配給當前用戶,該用戶從一個內(nèi)置的名為 actor 的Seam組件中獲得。任何Seam組件都可用來執(zhí)行任務(wù)指派。

(5)

<end-state>節(jié)點定義業(yè)務(wù)流程的邏輯結(jié)束。當執(zhí)行到達這個節(jié)點時,流程實例就要被銷毀。

如果我們用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() {}

}
(1)

description屬性從JSP頁接受用戶輸入,并將它暴露給流程定義,這樣就可讓Seam來設(shè)定任務(wù)的descrption。

(2)

Seam的 @CreateProcess 注解為指定名稱的流程定義創(chuàng)建了一個新的jBPM流程實例。

(3)

Seam的 @StartTask 注解用來啟動任務(wù),@EndTask 用來結(jié)束任務(wù),并允許恢復(fù)執(zhí)行業(yè)務(wù)流程。

在實際的應(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配置并不是很有趣。

1.4.2. 工作原理

待完成

1.5. Seam頁面流:猜數(shù)字范例

對有相對自由(特別)導(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)的。

1.5.1. 理解代碼

這個例子由一個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導(dǎo)航規(guī)則中的view id一樣。 redirect屬性告訴Seam在導(dǎo)航到頁面時使用post-then-redirect。(這會帶來友好的瀏覽器URL。)

(2)

<transition> 元素命名了一個JSF輸出。當一個JSF動作導(dǎo)致那個輸出時會觸發(fā)轉(zhuǎn)換。 在任何jBPM轉(zhuǎn)換動作調(diào)用后,執(zhí)行會進行到頁面流程圖的下一個節(jié)點。

(3)

一個轉(zhuǎn)換動作 <action> 就像JSF動作,不同的就是它只發(fā)生在一個jBPM轉(zhuǎn)換發(fā)生時。 轉(zhuǎn)換動作能調(diào)用任何Seam組件。

(4)

<decision> 節(jié)點用來劃分頁面流,通過計算JSF EL表達式?jīng)Q定要執(zhí)行的下一個節(jié)點。

這個頁面流在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;
   }
}
(1)

一開始,JSP頁面請求一個 numberGuess 組件,Seam會為該組件創(chuàng)建一個新的實例,并調(diào)用 @Create 方法,允許組件初始化自己。

(2)

@Begin 注解啟動了一個Seam 業(yè)務(wù)會話(conversation) (稍后詳細說明),并指定業(yè)務(wù)會話頁面流所要使用的頁面流定義。

如你所見,這個Seam組件是純業(yè)務(wù)邏輯的!它不需要知道任何關(guān)于用戶交互的東西。這點使得組件更易被復(fù)用。

1.5.2. 工作原理

TODO

1.6. 一個完整的Seam應(yīng)用程序:賓館預(yù)訂范例

1.6.1. 介紹

該系統(tǒng)是一個完整的賓館客房預(yù)訂系統(tǒng),它由下列功能組成:

  • 用戶注冊

  • 登錄

  • 注銷

  • 設(shè)置密碼

  • 搜索賓館

  • 選擇賓館

  • 客房預(yù)訂

  • 預(yù)訂確認

  • 當前預(yù)訂列表

應(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)這種健壯性的。

1.6.2. 預(yù)訂系統(tǒng)概況

這個項目的結(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ù)邏輯。

  • BookingListAction 獲得當前登錄用戶的預(yù)訂列表。
  • ChangePasswordAction 修改當前用戶的密碼。
  • HotelBookingAction 實現(xiàn)了應(yīng)用程序的核心功能:賓館客房搜索、選擇、預(yù)訂和預(yù)訂確認。 這功能是以 業(yè)務(wù)對話(conversation) 形式實現(xiàn)的,所以它是整個程序中最有意思的一個類。
  • RegisterAction 注冊一個新用戶。

應(yīng)用程序的持久化模型由三個實體bean實現(xiàn)。

  • Hotel 是表示一個賓館的實體Bean
  • Booking 是表示一個預(yù)訂的實體Bean
  • User 是表示一個能夠進行賓館預(yù)訂的用戶的實體Bean

1.6.3. 理解Seam業(yè)務(wù)對話(Conversation)

我們鼓勵您隨意瀏覽源代碼。在這個教程里我們將關(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)

}
(1)

EJB標準中的 @Stateful 注解表明這個類是一個有狀態(tài)的Session Bean。它們的默認作用域是業(yè)務(wù)對話上下文。

(2)

@Restrict注解給組件加上了一個安全限制。只有登錄過的用戶才能訪問該組件。安全章節(jié)中更詳細地討論了Seam的安全問題。

(3)

@DataModel 注解將一個 List 作為JSF ListDataModel 暴露出去。 這簡化了搜索界面的可單擊列表的實現(xiàn)。在這個例子中,賓館的列表是以名為 hotels 的 ListDataModel 業(yè)務(wù)對話變量暴露給頁面的。

(4)

EJB標準中的 @Remove 注解指定了一個有狀態(tài)的Session Bean應(yīng)該在注解的方法被調(diào)用后被刪除且其狀態(tài)應(yīng)該被銷毀。 在Seam里,所有有狀態(tài)的Session Bean都應(yīng)該定義一個標有 @Destroy @Remove 的方法。 這是Seam在銷毀Session上下文時要調(diào)用的EJB刪除方法。實際上 @Destroy 注解更有用,因為它能在Seam上下文結(jié)束時被用來做各種各樣的清理工作。如果沒有一個 @Destroy @Remove 方法,那么狀態(tài)會泄露,你就會碰到性能上的問題。

應(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>
(1)

Ajax4JSF的 <a:support> 標簽允許一個JSF動作事件監(jiān)聽器在類似 onkeyup 這樣的JavaScript事件發(fā)生時被異步的 XMLHttpRequest 調(diào)用。 更棒的是,reRender 屬性讓我們可以在收到異步響應(yīng)時渲染一個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ù)的方法。

這個頁面根據(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() {}

}
(1)

這個bean使用EJB3的 擴展持久化上下文,所以任意實體實例在整個有狀態(tài)Session Bean的生命周期中一直受到管理。

(2)

@Out 注解聲明了一個屬性值在方法調(diào)用后會 向外注入 到一個上下文變量中的。 在這個例子中,名為 hotel 的上下文變量會在每個動作監(jiān)聽器調(diào)用完成后被設(shè)置為 hotel 實例變量的值。

(3)

@Begin 注解表明被注解的方法開始一個 長期業(yè)務(wù)對話,因此當前業(yè)務(wù)對話上下文在請求結(jié)束后不會被銷毀。相反,它會被關(guān)聯(lián)給當前窗口的每次請求,在業(yè)務(wù)對話超時時或者一個 @End 方法被調(diào)用后銷毀。

(4)

@End 注解表明被注解的方法被用來結(jié)束一個長期業(yè)務(wù)對話,所以當前業(yè)務(wù)對話上下文會在請求結(jié)束后被銷毀。

(5)

這個EJB刪除方法會在Seam銷毀業(yè)務(wù)對話上下文時被調(diào)用。不要忘記定義該方法!

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é)束了,并將你重定向到搜索頁面。

1.6.4. Seam的UI控制庫

如果你查看下預(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é)。

1.6.5. Seam調(diào)試頁面

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 即可。

1.7. 一個使用Seam和jBPM的完整范例:DVD商店

DVD商店程序演示了如何在任務(wù)管理和頁面流中使用jBPM。

用戶界面應(yīng)用jPDL頁面流實現(xiàn)了搜索和購物車功能。

管理員界面使用jBPM來管理訂單的審批和送貨周期。業(yè)務(wù)流程可以通過選擇不同的流程定義實現(xiàn)動態(tài)改變。

TODO

dvdstore目錄。

1.8. 結(jié)合Seam和Hibernate的范例:Hibernate預(yù)訂系統(tǒng)

Hibernate預(yù)訂系統(tǒng)是之前客房預(yù)訂系統(tǒng)的另一個版本,它使用Hibernate和JavaBeans代替了會話Bean實現(xiàn)持久化。

TODO

hibernate目錄。

1.9. 一個RESTful的Seam應(yīng)用程序:Blog范例

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ù)。

1.9.1. 使用“拉”風格的MVC

從 index.xhtml Facelets頁面中取出的片斷顯示了blog的最近文章列表:

Example 1.25. 

<h:dataTable value="#{blog.recentBlogEntries}" var="blogEntry" rows="3">
   <h:column>
      <div class="blogEntry">
         <h3>#{blogEntry.title}</h3>
         <div>
            <h:outputText escape="false"
                  value="#{blogEntry.excerpt==null ? blogEntry.body : blogEntry.excerpt}"/>
         </div>
         <p>
            <h:outputLink value="entry.seam" rendered="#{blogEntry.excerpt!=null}">
               <f:param name="blogEntryId" value="#{blogEntry.id}"/>
               Read more...
            </h:outputLink>
         </p>
         <p>
            [Posted on
            <h:outputText value="#{blogEntry.date}">
               <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
            </h:outputText>]
             
            <h:outputLink value="entry.seam">[Link]
               <f:param name="blogEntryId" value="#{blogEntry.id}"/>
            </h:outputLink>
         </p>
      </div>
   </h:column>
</h:dataTable>

如果我們通過收藏夾訪問這個頁面,那么 <h:dataTable> 的數(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();
   }

}
(1)

這個組件使用了一個 受Seam管理的持久化上下文(seam-managed persistence context)。 與我們看過的其他例子不同,這個持久化上下文是由Seam管理的,而不是EJB3容器。 持久化上下文貫穿于整個Web請求中,這使得在視圖里訪問未抓取的關(guān)聯(lián)數(shù)據(jù)時可以避免發(fā)生任何異常。

(2)

@Unwrap 注解告訴Seam將 Blog 而不是 BlogService 組件作為方法的返回值提供給客戶端。 這是Seam的 管理員組件模式(manager component pattern)

這些看起來已經(jīng)很不錯了,那如何來收藏諸如搜索結(jié)果頁這樣的表單提交結(jié)果頁面呢?

1.9.2. 可收藏的搜索結(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;
   }

}

1.9.3. 在RESTful應(yīng)用程序中使用“推”風格("push"-style)的MVC

有些時候,用“推”風格的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)。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多