2020国产成人精品视频,性做久久久久久久久,亚洲国产成人久久综合一区,亚洲影院天堂中文av色

分享

JBoss Seam 入門

 gyb98 2010-12-31

Chapter 1. Seam 入門

1.1. 試試看

本教程假定你已下載JBoss AS 4.0.5并安裝了EJB 3.0 profile(請(qǐng)使用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上運(yùn)行示例

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

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

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

1.1.3. 運(yùn)行測(cè)試

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

1.2. 第一個(gè)例子:注冊(cè)示例

注冊(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ì)象。

1.2.1. 了解代碼

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

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

1.2.1.1. 實(shí)體Bean:User.java

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

}
(1)

EJB3標(biāo)準(zhǔn)注解 @Entity 表明了 User 類是個(gè)實(shí)體Bean.

(2)

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

(3)

每當(dāng)Seam實(shí)例化一個(gè)組件時(shí),它就將始化后的實(shí)例綁定給組件中 默認(rèn)上下文 的上下文變量。默認(rèn)的上下文由 @Scope注解指定。 User Bean是個(gè)會(huì)話作用域的組件。

(4)

EJB標(biāo)準(zhǔn)注解@Table 表明了將 User 類映射到 users 表上。

(5)

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

(6)

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

(7)

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

(8)

標(biāo)準(zhǔn)EJB注解 @Id 表明了實(shí)體Bean的主鍵屬性。

這個(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。

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

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

}
(1)

EJB標(biāo)準(zhǔn)注解 @Stateless 將這個(gè)類標(biāo)記為無狀態(tài)的會(huì)話Bean。

(2)

注解 @In將Bean的一個(gè)屬性標(biāo)記為由Seam來注入。 在此例中,此屬性由名為 user 的上下文變量注入(實(shí)例的變量名)。

(3)

EJB標(biāo)準(zhǔn)注解 @PersistenceContext 用來注入EJB實(shí)體管理器。

(4)

Seam的 @Logger 注解用來注入組件的 Log 實(shí)例。

(5)

動(dòng)作監(jiān)聽器方法使用標(biāo)準(zhǔn)的EJB3 EntityManager API來與數(shù)據(jù)庫交互,并返回JSF的輸出結(jié)果。 請(qǐng)注意,由于這是個(gè)會(huì)話Bean,因此當(dāng) register() 方法被調(diào)用時(shí)事務(wù)就會(huì)自動(dòng)開始,并在結(jié)束時(shí)提交(commit)。

(6)

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

(7)

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

(8)

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

(9)

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

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

1.2.1.3. 會(huì)話Bean的本地接口:Register.java

很自然,我們的會(huì)話Bean需要一個(gè)本地接口。

Example 1.3. 

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

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

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

如果你此前曾接觸過許多的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ǔ)

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!因?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ī)則。

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

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>

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

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>

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

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

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

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

1.2.2. 工作原理

當(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í)體的屬性值。

1.3. Seam中的可點(diǎn)擊列表:消息示例

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

1.3.1. 理解代碼

此消息示例中有一個(gè)實(shí)體Bean,Message,一個(gè)會(huì)話Bean MessageListBean 及一個(gè)JSP頁面。

1.3.1.1. 實(shí)體Bean:Message.java

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

}

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

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

}
(1)

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

(2)

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

(3)

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

(4)

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

(5)

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

(6)

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

(7)

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

(8)

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

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

1.3.1.3. 會(huì)話Bean的本地接口:MessageManager.java

當(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。

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. 工作原理

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

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

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

1.4.1. 理解代碼

這個(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>
(1)

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

(2)

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

(3)

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

(4)

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

(5)

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

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

}
(1)

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

(2)

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

(3)

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

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

1.4.2. 工作原理

待完成

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

對(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)的。

1.5.1. 理解代碼

這個(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>
(1)

<page>元素定義了一個(gè)等待狀態(tài),在該狀態(tài)中系統(tǒng)顯示一個(gè)JSF視圖等待用戶輸入。 view-id與簡單JSF導(dǎo)航規(guī)則中的view id一樣。 redirect屬性告訴Seam在導(dǎo)航到頁面時(shí)使用post-then-redirect。(這會(huì)帶來友好的瀏覽器URL。)

(2)

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

(3)

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

(4)

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

這個(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;
   }
}
(1)

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

(2)

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

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

1.5.2. 工作原理

TODO

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

1.6.1. 介紹

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

  • 用戶注冊(cè)

  • 登錄

  • 注銷

  • 設(shè)置密碼

  • 搜索賓館

  • 選擇賓館

  • 客房預(yù)訂

  • 預(yù)訂確認(rèn)

  • 當(dāng)前預(yù)訂列表

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

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

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

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

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

  • Hotel 是表示一個(gè)賓館的實(shí)體Bean
  • Booking 是表示一個(gè)預(yù)訂的實(shí)體Bean
  • User 是表示一個(gè)能夠進(jìn)行賓館預(yù)訂的用戶的實(shí)體Bean

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

我們鼓勵(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)

}
(1)

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

(2)

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

(3)

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

(4)

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

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

Ajax4JSF的 <a:support> 標(biāo)簽允許一個(gè)JSF動(dòng)作事件監(jiān)聽器在類似 onkeyup 這樣的JavaScript事件發(fā)生時(shí)被異步的 XMLHttpRequest 調(diào)用。 更棒的是,reRender 屬性讓我們可以在收到異步響應(yīng)時(shí)渲染一個(gè)JSF頁面的片段并執(zhí)行一個(gè)頁面的局部修改。

(2)

Ajax4JSF的 <a:status> 標(biāo)簽使我們能在等待異步請(qǐng)求返回時(shí)顯示一個(gè)簡單的動(dòng)畫。

(3)

Ajax4JSF的 <a:outputPanel> 標(biāo)簽定義了一塊能被異步請(qǐng)求修改的頁面區(qū)域。

(4)

Seam的<s:link> 標(biāo)簽使我們能將一個(gè)JSF動(dòng)作監(jiān)聽器附加在一個(gè)普通的(非JavaScript)HTML鏈接上。 用它取代標(biāo)準(zhǔn)JSF的 <h:commandLink> 的好處就是它在“在新窗口中打開”和“在新標(biāo)簽頁中打開”時(shí)仍然有效。 值得注意的另一點(diǎn)就是我們用了一個(gè)綁定了參數(shù)的方法:#{hotelBooking.selectHotel(hot)}。 在標(biāo)準(zhǔn)的統(tǒng)一EL中這是不允許的,但Seam對(duì)EL的擴(kuò)展進(jìn)行了擴(kuò)展,使表達(dá)式能夠支持帶參數(shù)的方法。

這個(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() {}

}
(1)

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

(2)

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

(3)

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

(4)

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

(5)

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

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

1.6.4. Seam的UI控制庫

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

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

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

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

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

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

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

TODO

dvdstore目錄。

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

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

TODO

hibernate目錄。

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

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

1.9.1. 使用“拉”風(fēng)格的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>

如果我們通過收藏夾訪問這個(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();
   }

}
(1)

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

(2)

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

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

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

}

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

有些時(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)。

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

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多