程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> JSF 1.2入門,第2部分 JSF生命周期、轉換、檢驗和階段監聽器

JSF 1.2入門,第2部分 JSF生命周期、轉換、檢驗和階段監聽器

編輯:關於JAVA

簡介:本教程系列討論 Java™ Server Faces(JSF)技術的基礎知識,JSF 是一種服務器端框 架,它提供一種基於組件的 Web 用戶界面開發方式。第 1 部分 概述了 JSF 1.2 並提供了一個基本的應 用程序。本部分幫助您掌握更高級的 JSF 特性:定制的檢驗器、轉換器和階段監聽器,同時了解 JSF 應 用程序的生命周期。

開始之前

關於本系列

這個教程系列討論 JavaServer™ Faces(JSF)技術。JSF 是一種用於 Java Web 應用程序的服務器端用戶界面組件框架。本系列針對 JSF 的新手,幫助他們快速 入門 — 使用 JSF 並不是必需的,但是使用 JSF 組件可以減少工作量。本系列只討論基礎知識並 提供大量示例。

與 AWT、SWT 和 Swing 一樣,JSF 是一種比較傳統的 GUI 開發環境。它的主要 好處之一是,它將困難的工作交給框架開發人員而不是應用程序開發人員,從而簡化了 Web 開發。坦率 地說,JSF 本身比許多其他 Web 框架復雜,但是對應用程序開發人員隱藏了復雜性。與大多數其他框架 相比,用 JSF 開發 Web 應用程序要容易得多:需要的代碼更少,復雜性更低,配置更少。

如果 您從事 Java 服務器端 Web 開發,那麼 JSF 是最容易掌握的框架。它非常適合創建 Web 應用程序(不 是 Web 站點)。它讓 Web 開發人員可以集中精力處理 Java 代碼,而不需要處理請求對象、會話對象、 請求參數或復雜的 XML 文件。與其他 Java Web 框架相比,使用 JSF 可以更快速地做更多事情。

關於本教程

本教程延續 第 1 部分 的內容。如果您沒有接觸過 JSF,或者希望復習一下 ,那麼請先閱讀第 1 部分。即使您熟悉 JSF,第 1 部分中的某些內容也可能對您有所幫助。

在 本教程中,不使用工具或 IDE 支持(盡管工具支持是 JSF 的主要好處之一)。本教程只介紹基本知識並 提供少量背景信息,從而幫助您理解這裡討論的內容並有效地使用 JSF 構建 Web 應用程序。

目 標

在本教程中,繼續概述 JSF 的特性並學習如何使用所有 JSF 組件。我們要構建一個簡單的聯 系人管理系統 — 一個基本的 CRUD(創建、讀取、更新、刪除)應用程序。在學習 JSF 應用程序 的生命周期之後,用定制的轉換器和檢驗器改進這個應用程序。本教程要嘗試一些高級 JSF 編程:使用 一個階段監聽器創建一個對象級的檢驗框架。

誰應該學習本教程?

如果您是 JSF 的初學者,那麼本教程正適合您。如果您用過 JSF,但是沒有用過 JSF 1.2 特性,或 者只用 GUI 工具構建過 JSF 應用程序,那麼也可能從這個系列教程學到許多知識。

前提條件

本教程適合初級到中級水平的 Java 開發人員。您應該基本了解 Java 語言並有 GUI 開發經驗。

系統需求

要想運行本教程中的示例,需要一個 Java 開發環境(JDK)和 Apache Maven。擁有 Java IDE 會有 幫助。本教程提供了 Maven 項目文件以及 Eclipse Java EE 和 Web Tools Project(WTP)項目文件。

JSF CRUD 示例應用程序

本節介紹一個簡單的 CRUD 應用程序,在後面幾節中我們將通過構建這個應用程序來學習:

每個 JSF 標准 HTML 組件

創建定制的轉換器

使用檢驗器

使用階段監聽器

聯系人管理應用程序

在本節中將構建的應用程序是一個聯系人管理應用程序,它的結構與 第 1 部分 中的計算器應用程序 相似。在 圖 1 中可以看到,這個應用程序是一個標准的 CRUD 應用程序。它不需要導航規則,因為整個 應用程序只使用一個視圖(contacts.jsp 頁面)。

圖 1. 聯系人管理示例應用程序

圖 2 顯示這個應用程序的基本流程:

圖 2. 聯系人管理示例應用程序,鏈接圖

這個 CRUD 應用程序由以下元素組成:

ContactController:JSF 控制器

Contact:代表聯系人信息的模型對象

ContactRepository:用來創建、讀取、更新和刪除 Contact 對象的模型對象

contacts.jsp:這個 JavaServer Pages(JSP)顯示用來管理聯系人的 JSF 組件

faces-config.xml:JSF 的配置,在這裡將 ContactController 和 ContactRepository 聲明為托管 bean,並將 ContactRepository 注入 ContactController

ContactController

ContactController 後端支持 contacts.jsp 頁面。清單 1 給出了 ContactController 的代碼:

清單 1. ContactController

package com.arcmind.contact.controller;
import java.util.List;
import javax.faces.application.FacesMessage;
import javax.faces.component.UICommand;
import javax.faces.component.UIForm;
import javax.faces.context.FacesContext;
import com.arcmind.contact.model.Contact;
import com.arcmind.contact.model.ContactRepository;
public class ContactController {
   /** Contact Controller collaborates with contactRepository. */
   private ContactRepository contactRepository;
   /** The current contact that is being edited. */
   private Contact contact = new Contact();
   /** Contact to remove. */
   private Contact selectedContact;
   /** The current form. */
   private UIForm form;
   /** Add new link. */
   private UICommand addNewCommand;
   /** Persist command. */
   private UICommand persistCommand;
   /** For injection of collaborator. */
   public void setContactRepository(ContactRepository contactRepository) {
     this.contactRepository = contactRepository;
   }
   public void addNew() {
     form.setRendered(true);
     addNewCommand.setRendered(false);
     persistCommand.setValue("Add");
   }
   public void persist() {
     form.setRendered(false);
     addNewCommand.setRendered(true);
     if (contactRepository.persist(contact) == null) {
       addStatusMessage("Added " + contact);
     } else {
       addStatusMessage("Updated " + contact);
     }
   }
   public void remove() {
     contactRepository.remove(selectedContact);
     addStatusMessage("Removed " + selectedContact);
   }
   public void read() {
     contact = selectedContact;
     form.setRendered(true);
     addNewCommand.setRendered(false);
     addStatusMessage("Read " + contact);
     persistCommand.setValue("Update");
   }
   private void addStatusMessage(String message) {
     FacesContext.getCurrentInstance().addMessage(null,
         new FacesMessage(FacesMessage.SEVERITY_INFO, message, null));
   }
  //most getter/setter omitted
}

清單 1 用不到 74 行代碼創建了一個 CRUD GUI — 不算太麻煩。ContactController 在 request 范 圍內管理,所以在實例化一個新的 ContactController 時,會創建一個新的 Contact。有三個組件綁定 到 ContactController:form(UIForm 類型)、addNewCommand(UICommand 類型)和 persistCommand (UICommand 類型)。

addNew() 方法確保:

打開了 form,讓用戶可以輸入新的聯系人 — form.setRendered(true)

關閉了 addNewCommand — addNewCommand.setRendered(false)

將 persistCommand 的標簽設置為 Add — persistCommand.setValue("Add")

persist() 方法使用 contactRepository 處理現有聯系人的更新和添加新聯系人。persist() 方法關 閉 form 並打開 addNewCommand。remove() 方法使用 contactRepository 從系統中刪除聯系人。

read() 方法將 selectedContact(Contact 類型)復制到 contact。contact(也是 Contact 類型的 )是綁定到 form 的值。您可能想知道 selectedContact 來自哪裡。當用戶單擊聯系人列表中的一個聯 系人時,會選擇一個值。(在後面討論 JSP 時將討論它。)與 第 1 部分 中一樣,addStatusMessage 添加狀態消息,可以用 <h:messages> 顯示這些消息。

聯系人視圖

這個 JSP 頁面使用一個 <h:dataTable>(第 1 部分中沒有討論過的一個組件),見清單 2:

清單 2. contacts.jsp

<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Contacts</title>
<link rel="stylesheet" type="text/css"
  href="<%=request.getContextPath()%>/css/main.css" />
</head>
<body>
<f:view>
  <h4>Contacts</h4>
  <h:messages infoClass="infoClass" errorClass="errorClass"
    layout="table" globalOnly="true" />
  <h:form>
    <h:commandLink binding="#{contactController.addNewCommand}"
     action="#{contactController.addNew}" value="Add New..." />
  </h:form>
  <h:form binding="#{contactController.form}" rendered="false"
    styleClass="form">
    <h:inputHidden value="#{contactController.contact.id}" />
    <h:panelGrid columns="6">
     <%-- First Name --%>
     <h:outputLabel value="First Name" for="firstName" accesskey="f" />
     <h:inputText id="firstName" label="First Name" required="true"
       value="#{contactController.contact.firstName}" size="10" />
     <h:message for="firstName" errorClass="errorClass" />
     <%-- Last Name --%>
     <h:outputLabel value="Last Name" for="lastName" accesskey="l" />
     <h:inputText id="lastName" required="true"
       value="#{contactController.contact.lastName}" size="15" />
     <h:message for="lastName" errorClass="errorClass" />
    </h:panelGrid>
    <h:panelGroup>
     <h:commandButton binding="#{contactController.persistCommand}"
       action="#{contactController.persist}" />
    </h:panelGroup>
  </h:form>
  <h:form>
    <h:dataTable value="#{contactController.contacts}" var="contact"
     rowClasses="oddRow, evenRow"
     rendered="#{not empty contactController.contacts}"
     styleClass="contactTable" headerClass="headerTable"
     columnClasses="normal,centered">
     <h:column>
       <f:facet name="header">
        <h:column>
          <h:outputText value="Name" />
        </h:column>
       </f:facet>
       <h:outputText value="#{contact.lastName}, #{contact.firstName}" />
     </h:column>
     <h:column>
       <f:facet name="header">
        <h:column>
          <h:outputText value="Action" />
        </h:column>
       </f:facet>
       <h:panelGrid columns="2">
        <h:commandLink value="remove" action="#{contactController.remove}">
          <f:setPropertyActionListener
           target="#{contactController.selectedContact}" value="#{contact}" />
        </h:commandLink>
        <h:commandLink value="edit" action="#{contactController.read}">
          <f:setPropertyActionListener
           target="#{contactController.selectedContact}" value="#{contact}" />
        </h:commandLink>
       </h:panelGrid>
     </h:column>
    </h:dataTable>
  </h:form>
</f:view>
</body>
</html>

<h:dataTable> 使用 "#{contactController.contacts}" 綁定值,可以顯示來自 contactController 的聯系人。使用 var 屬性映射每個聯系人:var="contact"。在 contacts.jsp 中, oddRow 和 evenRow 樣式(在一個 CSS 文件中定義)被設置到 rowClasses 屬性中: rowClasses="oddRow, evenRow"。這允許 <h:dataTable> 對表使用交替樣式。 <h:dataTable> 的功能很多;請通過 <h:dataTable> 的在線文檔了解可以用 <h:dataTable> 實現的所有功能和樣式。(參見 參考資料 中 JSF API Javadocs 的鏈接。)

還可以用 styleClass="contactTable" 為整個 <h:dataTable> 設置樣式,用 headerClass="headerTable" 為表頭區域設置樣式。還可以使用 columnClasses 為列設置替代樣式: columnClasses="normal,centered"。如果聯系人不存在,就不應該顯示 <h:dataTable>,設置方 法如下:rendered="#{not empty contactController.contacts}"。<h:dataTable> 組件的功能非 常豐富而且容易使用。

在 <h:dataTable> 中,使用 <h:column> 顯示屬性中的值。每個列在 <f:facet> 標記中定義一個標題。facet 是另一個組件使用的名稱組件。然後,在 <f:facet> 後面, <h:column> 組件內部,使用 <h:outputText> 組件輸出聯系人的 firstName 和 lastName 屬性。

每一行都有一個刪除鏈接和一個編輯鏈接,每個鏈接都使用一個 <h:commandLink>。刪除鏈接 綁定到 contactController.remove 方法。編輯鏈接綁定到 contactController.read 方法。基於配置 <f:setPropertyActionListener> 的方式,用當前行填充 contactController.selectedContact 屬性。在調用動作方法之前,<f:setPropertyActionListener> 使當前行的聯系人被復制到 selectedContact。

聯系人 CRUD 應用程序的 faces-config.xml

faces-config.xml 文件將 ContactRepository 與 ContactController 聯系起來,見清單 3:

清單 3. faces-config.xml

<managed-bean>
  <managed-bean-name>contactRepository</managed-bean-name>
  <managed-bean-class>
  com.arcmind.contact.model.ContactRepository
  </managed-bean-class>
  <managed-bean-scope>application</managed-bean-scope>
</managed-bean> 
<managed-bean>
  <managed-bean-name>contactController</managed-bean-name>
  <managed-bean-class>
    com.arcmind.contact.controller.ContactController
  </managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
  <managed-property>
    <property-name>contactRepository</property-name>
    <property-class>
     com.arcmind.contact.model.ContactRepository
    </property-class>
    <value>#{contactRepository}</value>
  </managed-property>
</managed-bean>

注意,contactRepository 處於 application 范圍,並使用 <managed-property> 元素將它注 入 contactController 的 contactRepository 中。可以使用這種技術把依賴項/協作組件注入控制器, 這有助於分隔模型和視圖;還允許注入偽對象(mock object),以後可以用真實的對象替換這些偽對象 。我曾經多次為 ContactRepository 這樣的模型對象建立偽對象,在完成 GUI 之後再用真實版本替換它 。

這個應用程序的模型非常簡單,見清單 4 和清單 5。清單 4 給出 Contact 類:

清單 4. Contact

package com.arcmind.contact.model;
public class Contact { private String firstName;
private String lastName; protected long id;
public Contact(String firstName, String lastName) {
this.firstName = firstName; this.lastName =
lastName; }
public Contact() { }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) {
this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) {
this.lastName = lastName; }
@Override public int hashCode() { final int prime =
31; int result = 1; result = prime * result + (int)
(id ^ (id >>> 32)); return result; }
@Override public boolean equals(Object obj) { if
(this == obj) return true; if (obj == null) return
false; if (getClass() != obj.getClass()) return
false; final Contact other = (Contact) obj; if (id
!= other.id) return false; return true; }
@Override public String toString() { return
String.format("Contact: %s %s", firstName,
lastName); }
public long getId() { return id; }
public void setId(long id) { this.id = id; }
}

清單 5 給出 ContactRepository 類,這個類模擬將聯系人寫入數據庫:

清單 5. ContactRepository

package com.arcmind.contact.model;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
public class ContactRepository {
  private Map<Long, Contact> contacts = new LinkedHashMap<Long, Contact> ();
  private static long counter = 1l;
  public List<Contact> getContacts() {
    return new ArrayList<Contact>(contacts.values());
  }
  public synchronized Contact persist(Contact contact) {
    if (contact.id == 0) {
     contact.id = counter++;
    }
    return contacts.put(contact.id, contact);
  }
  public synchronized void remove(Contact contact) {
    contacts.remove(contact.id);
  }
}

現在有了一個非常簡單的 CRUD 應用程序。在下一節中,我們以此為基礎學習如何使用不同的 JSF 組 件。

使用 JSF 組件

在本節中,我們將使用一些 JSF 組件增強 CRUD 應用程序:

<f:subview>

<h:selectOneMenu>

<h:selectOneRadio>

<h:selectBooleanCheckbox>

<h:selectManyCheckbox>

<h:inputTextarea>

圖 3 顯示這些組件在 GUI 中的樣子:

圖 3. 包含一些常用 JSF 組件的聯系人管理應用程序

子視圖

您可以想像到,很難在一個頁面中包含您的所有 JSF 組件。幸運的是,可以使用 <f:subview> 將 JSF 組件放在不同的視圖中,見清單 6:

清單 6. 子視圖 contacts.jsp

<body>
<f:view>
  <h3>Contacts (2nd version)</h3>
  <h:messages infoClass="infoClass" errorClass="errorClass"
    layout="table" globalOnly="true" />
  <h:form>
    <h:commandLink binding="#{contactController.addNewCommand}"
     action="#{contactController.addNew}" value="Add New..." />
  </h:form>
  <f:subview id="form">
    <jsp:include page="form.jsp" />
  </f:subview>
  <f:subview id="listing">
    <jsp:include page="listing.jsp" />
  </f:subview>
</f:view>
</body>

可以在父頁面或包含的頁面(不能同時)中使用 <f:subview>。在 JSF 1.2 中, <f:subview> 是可選的。在老版本的 JSF 中,它是必需的。一些 IDE 似乎要求有 <f:subview>,所以即使使用 JSF 1.2 或更高版本,仍然可能需要使用它們。

Select one

在 JSF 中,組件分為兩部分:JSF 組件本身和一個負責顯示這個組件的顯示器。UISelectOne 組件有 多個顯示器。它後端支持 HtmlSelectOneListbox、HtmlSelectOneMenu 和 HtmlSelectOneRadio。

聯系人應用程序(第二版)使用 <h:selectOneMenu>。為此,需要添加三個新的模型對象: Group(見 清單 7)、Tag 和 ContactType。還要添加兩個新的存儲庫對象:GroupRepository 和 TagRepository,它們與 ContactRepository 相似。ContactType 不需要存儲庫,因為它是一個 Enum。 Contact 類現在有三個新屬性:它所屬的組(group)、與它相關聯的標記(tags)和它的類型(type) 。

清單 7. 子視圖 contacts.jsp/form.jsp

<%-- Group --%>
<h:outputLabel value="Group" for="group" accesskey="g" />
<h:selectOneMenu id="group" validatorMessage="required"
  value="#{contactController.selectedGroupId}">
  <f:selectItems value="#{contactController.groups}" />
  <f:validateLongRange minimum="1" />
</h:selectOneMenu>
<h:message for="group" errorClass="errorClass" />

注意,selectOneMenu 使用 value 屬性將 selectOneMenu 綁定到 selectedGroupId。selectOneMenu 元素體中包含一個 <f:selectItems>,它的值綁定到 groups 屬性:value=# {contactController.groups}。在後端 bean 中創建組列表。selectedGroupId 屬性和 groups 屬性的代 碼見清單 8:

清單 8. 構建組列表

public class ContactController {
  ...
  private GroupRepository groupRepository;
  ...
  private Long selectedGroupId;
  ...
  public List<SelectItem> getGroups() {
    List<Group> groups = groupRepository.list();
    List<SelectItem> list = new ArrayList<SelectItem>(groups.size() +1);
    list.add(new SelectItem(Long.valueOf(-1L), "select one"));
    for (Group group : groups) {
     SelectItem selectItem = new SelectItem(group.getId(), group.getName());
     list.add(selectItem);
    }
    return list;
  }
  //Other getter/setters removed
  ...

groups 屬性返回 SelectItem 的列表。SelectItem 類用來表示列表中的一個列表項。UISelectMany 和 UISelectOne 組件都使用這個類。注意,getGroups 方法使用 groupRepository 獲得組列表,這是一 個像 contactRepository 那樣注入的存儲庫對象。groupRepository 管理 Group 領域對象。一個 Group 代表一個組,一個 Contact 代表一個聯系人。getGroups() 創建一個 SelectItem 列表,使用 group.id 屬性作為值,使用 group.name 屬性作為標簽。

注意,這裡要添加一個值為 -1 的 “select one” SelectItem。使用這個值來判斷是否已經選擇了 列表項。通過在 selectOneMenu 中使用 <f:validateLongRange minimum="1" />,判斷出未選擇 列表項的情況(見 清單 7)。還要注意,selectOneMenu 使用 validatorMessage="required" 顯示一個 簡短的錯誤消息。

注意,清單 8 直接調用存儲庫。如果存儲庫實際上與一個數據庫或緩存通信,那麼為了進行錯誤處理 並在屬性中公開所選擇的列表項,需要在動作方法中進行調用。

在提交表單時,設置 selectedGroupId。綁定到更新按鈕和創建按鈕的 persist() 使用 selectedGroupId 在存儲庫中查找這個組,見清單 9:

清單 9. 更新 persist() 以使用 selectedGroupId

public class ContactController {
  ...
  public void persist() {
    /* Setup the group into contact. */
    contact.setGroup(groupRepository.lookup(selectedGroupId));

    /* Turn form off, turn link on. */
    form.setRendered(false);
    addNewCommand.setRendered(true);

    /* Add a status message. */
    if (contactRepository.persist(contact) == null) {
     addStatusMessage("Added " + contact);
    } else {
     addStatusMessage("Updated " + contact);
    }
  }
  ...
  public void read() {
    /* Prepare selected contact. */
    contact = selectedContact;

    /* Turn form on and the link off. */
    form.setRendered(true);
    addNewCommand.setRendered(false);
    /* Prepare selected group id. */
    selectedGroupId = contact.getGroup().getId();
    ...
    this.selectedTagIds = tagIds.toArray(new Long[tags.size()]);

    addStatusMessage("Read " + contact);
    persistCommand.setValue("Update");
  }
  ...

注意,Contact 類添加了一個 group 屬性。因此,Contact 與 Group 之間存在多對一關系。

清單 9 中的 read() 方法初始化 this.selectedTagIds,從而在視圖中顯示所選的列表項。

您已經看到如何用 <h:selectOne> 處理一對一關系。Contact 還與 Tag 形成多對多關系(用 Contact 的 tags 屬性表示)。為了處理這個關系,要使用一個 <h:selectManyCheckbox>,見清 單 10:

清單 10. 用來處理 Contact.tags 的 selectManyCheckbox

<h:selectManyCheckbox id="tags"
  value="#{contactController.selectedTagIds}">
  <f:selectItems value="#{contactController.availableTags}" />
</h:selectManyCheckbox>

<h:selectManyCheckbox> 的主要差異是,它綁定到一個 long 數組,而不是綁定到單個 long (見清單 10)。

Java 代碼與 清單 8 相似,只是現在要處理所選 ID(long)的數組,而不是處理單個 long,見清單 11:

清單 11. 後端支持 selectManyCheckbox 的 Java 代碼

public class ContactController {
   ...
  private Long[] selectedTagIds;
  private TagRepository tagRepository;
   ...
  public List<SelectItem> getAvailableTags() {
    List<Tag> tags = tagRepository.list();
    List<SelectItem> list = new ArrayList<SelectItem>(tags.size ());
    for (Tag tag : tags) {
     SelectItem selectItem = new SelectItem(tag.getId(), tag.getName());
     list.add(selectItem);
    }
    return list;
  }
   ...
  public void persist() {
     ...   
    /* Setup the tags into contact. */
    List<Tag> tags = new ArrayList<Tag>(selectedTagIds.length);
    for (Long selectedTagId : selectedTagIds) {
     tags.add(tagRepository.lookup(selectedTagId));
    }
    contact.setTags(tags);
     ...   
  }
   ...
  public void read() {
     ...   
    /* Prepare selected tag IDs. */
    List<Tag> tags = contact.getTags();
    List<Long> tagIds = new ArrayList<Long>(tags.size());
    for (Tag tag : tags) {
     tagIds.add(tag.getId());
    }
    this.selectedTagIds = tagIds.toArray(new Long[tags.size()]);
    ...
  }
   ... 

persist() 方法使用 tagRepository.lookup() 方法根據 selectedTagId 尋找 Tag 領域對象,然後 用找到的值設置 contact.tags 屬性。read() 方法根據 contact.tags 屬性中的 Tag 初始化 selectedTagId。

UISelectMany 類後端支持 <h:selectManyCheckbox>,但是它還後端支持 <h:selectManyListbox> 和 <h:selectManyMenu>。可以使用它們替代 <h:selectManyCheckbox>,見清單 12、清單 13 和圖 4。

清單 12. <selectManyListbox>

<h:selectManyListbox id="tags" value="#{contactController.selectedTagIds}">
  <f:selectItems value="#{contactController.availableTags}"/>
</h:selectManyListbox>

清單 13. <selectManyMenu>

<h:selectManyMenu id="tags" value="#{contactController.selectedTagIds}">
  <f:selectItems value="#{contactController.availableTags}"/>
</h:selectManyMenu>

圖 4. <selectManyListbox> 和 <selectManyMenu>

注意,這三個標記的設置是相同的。

JSF 1.2 為 Enum 提供了轉換器,所以不需要查找值。可以直接使用它們和綁定到它們,不需要像處 理 tags 和 group 那樣在控制器中建立間接屬性(見 清單 8 和 清單 11)。Contact 類有一個 enum 屬性,見清單 14:

清單 14. Contact 使用 enum 作為 type 屬性

public enum ContactType {
  BUSINESS, PERSONAL;

  public String toString () {
    return this.name().toLowerCase();
  }
}
...
public class Contact implements Serializable {
   ...
  private Group group;
  private List<Tag> tags;
  ...
  private ContactType type = ContactType.PERSONAL;
   ... 
  public ContactType getType() {
    return type;
  }
  public void setType(ContactType type) {
    this.type = type;
  }
  ...
}

因為 JSF 為 Enum 提供了轉換器,所以可以直接綁定到 type 屬性,見清單 15:

清單 15. 直接綁定到 contact.type

<h:selectOneRadio id="type" value="#{contactController.contact.type}">
  <f:selectItem itemValue="PERSONAL" itemLabel="personal" />
  <f:selectItem itemValue="BUSINESS" itemLabel="business" />
</h:selectOneRadio>

清單 15 還演示了 <f:selectItem>(它創建單一值)的使用方法。JSF 為 Enum 提供的內置轉 換器要求值是字符串值 — 實際上是 Enum 的名稱。還可以使用 <h:selectOneListbox>,見清單 16:

清單 16. 使用 <h:selectOneListbox>

<h:selectOneListbox id="type" value="#{contactController.contact.type}">
  <f:selectItem itemValue="PERSONAL" itemLabel="personal"/>
  <f:selectItem itemValue="BUSINESS" itemLabel="business"/>
</h:selectOneListbox>

聯系人管理應用程序的其他組件

聯系人 CRUD 應用程序還演示了 <h:selectBooleanCheckbox> 和 <h:inputTextarea> 的用法,見清單 17:

清單 17. 使用 <h:selectBooleanCheckbox> 和 <h:textArea>

<h:inputHidden value="#{contactController.contact.id}" />
...
<%-- active --%>
<h:outputLabel value="Active" for="active" accesskey="a" />
<h:selectBooleanCheckbox id="active"
  value="#{contactController.contact.active}" />
<h:message for="active" errorClass="errorClass" />
<%-- Description --%>
...
<h:outputLabel value="Description" for="description"
  accesskey="d" style="font: large;" />
<h:inputTextarea id="description" cols="80" rows="5"
  value="#{contactController.contact.description}" />
<h:message for="description" errorClass="errorClass" />

<h:inputTextarea> 有兩個額外屬性,它們設置為 cols="80" rows="5"。綁定方法與前面一樣 。

清單 18 顯示在清單 17 中綁定的屬性:

清單 18. Contact 類

public class Contact implements Serializable {
   ...
  private String description;
  private boolean active;
  protected long id;
   ...
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public boolean isActive() {
    return active;
  }
  public void setActive(boolean active) {
    this.active = active;
  }
  public long getId() {
    return id;
  }
  public void setId(long id) {
    this.id = id;
  } 
   ...
}

id、description、type、firstName 和 lastName 屬性從 Contact 直接綁定到 UI。group 和 tags 屬性不能直接綁定,因為它們沒有 JSF 轉換器。在本教程後面的一節(“JSF 數據轉換器”)中,將討 論轉換器並為這個應用程序創建一些轉換器。現在先簡要討論一下 JSF 應用程序的生命周期。

JSF 應用程序的生命周期

與許多人認為的相反,即使不了解 JSF 技術的細節,也可以編寫 JSF 應用程序;只需通過開發一個 項目,就可以學到許多東西。但是,了解某些基礎知識會大大促進開發工作並節省許多時間。本節暫時拋 開聯系人應用程序,談談 JSF 請求處理生命周期的六個階段,看看在每個階段會發生什麼以及各階段是 如何相互連接的。這些內容會為本教程余下部分的工作提供一些背景知識。

JSF 應用程序生命周期的階段

JSF 應用程序生命周期的六個階段是:

恢復視圖

應用請求值;處理事件

處理檢驗;處理事件

更新模型值;處理事件

調用應用程序;處理事件

顯示響應

這六個階段是 JSF 處理表單 GUI 的一般次序。這個列表按照每個階段可能的執行次序和事件處理進 行排列,但是 JSF 生命周期並不是固定的。可以改變執行的次序,跳過某些階段或完全脫離生命周期。 例如,如果一個無效的請求值被復制到組件,那麼會重新顯示當前視圖,並可能不執行某些階段。

還可以選擇完全脫離 JSF,比如將處理委托給一個 servlet 或另一個應用程序框架。在這種情況下, 可以執行一個 FacesContext.responseComplete 方法調用,將用戶重定向到另一個頁面或 Web 資源,然 後使用請求調度器(從 FacesContext 中的請求對象獲得)轉發到適當的 Web 資源。也可以調用 FacesContext.renderResponse 來重新顯示原來的視圖。

最重要的是,在利用生命周期組織您的開發工作的同時不會受其束縛。在需要時可以修改默認的生命 周期,而不必擔心破壞應用程序。在大多數情況下,您會發現采用 JSF 的生命周期是值得的,因為它非 常符合邏輯。

在執行任何應用程序邏輯之前,必須檢驗表單;在執行檢驗之前,必須對字段數據進行轉換。如果堅 持采用生命周期,您就可以集中精力考慮檢驗和轉換的細節,而不必關注請求過程本身的階段。還要注意 ,其他 Web 框架也有相似的生命周期;只不過沒這麼明顯。

一些使用 JSF 的開發人員可能從來沒有編寫過組件或擴展過框架,而其他開發人員的工作卻集中在這 些任務上。 盡管對於幾乎任何項目,JSF 生命周期都是相同的,開發人員可以根據其在項目中的角色參 與不同的階段。如果您主要從事整體應用程序開發,那麼可能關注請求處理生命周期中間的幾個階段:

應用請求值

處理檢驗

更新模型值

調用應用程序

如果您主要從事 JSF 組件開發,那麼可能關注生命周期的第一個階段和最後一個階段:

恢復視圖

顯示響應

下面分別討論一下 JSF 請求處理生命周期的每個階段,包括事件處理和檢驗。在開始之前,先看看圖 5,圖 5 顯示 JSF 應用程序生命周期的概況:

圖 5. JSF 應用程序生命周期

階段 1:恢復視圖

在 JSF 生命周期的第一個階段 — 恢復視圖 中,通過 FacesServlet servlet 發來一個請求。這個 servlet 檢查這個請求並提取出視圖 ID(視圖 ID 由 JSP 頁面的名稱決定)。

JSF 框架控制器使用這個視圖 ID 為當前視圖尋找組件。如果這個視圖還不存在,JSF 控制器就創建 它。如果視圖已經存在,JSF 控制器就使用它。視圖包含所有 GUI 組件。

生命周期的這個階段有三種視圖實例:新視圖、初始視圖和 postback,每種視圖的處理方法各不相同 。對於新視圖,JSF 構建一個 Faces 頁面的視圖,並將事件處理函數和檢驗器連接到組件。視圖保存在 一個 FacesContext 對象中。

FacesContext 存儲狀態信息,JSF 需要使用這些信息為當前請求管理 GUI 組件的狀態。 FacesContext 將視圖存儲在它的 viewRoot 屬性中;viewRoot 包含與當前視圖 ID 對應的所有 JSF 組 件。

對於初始視圖(第一次裝載頁面),JSF 創建一個空視圖。在處理 JSP 頁面時,填充這個空視圖。填 充初始視圖之後,JSF 直接進入顯示響應階段。

對於 postback(用戶返回到以前訪問過的一個頁面),與頁面對應的視圖已經存在,所以只需恢復它 。在這種情況下,JSF 使用現有視圖的狀態信息重新構造它的狀態。

階段 2:應用請求值

應用請求值 階段的目標是獲取每個組件的當前狀態。首先,必須從 FacesContext 對象獲取或創建組 件,然後獲取它們的值。組件值常常取自請求參數,但是也可以取自 cookie 或請求頭。對於許多組件, 來自請求參數的值存儲在組件的 submittedValue 中。

如果組件的直接事件處理屬性是 true,那麼值被轉換為正確的類型並被檢驗(在下一階段中進一步進 行轉換)。然後,將轉換後的值存儲在組件中。如果值轉換或值檢驗失敗,那麼生成一個錯誤消息並放在 FacesContext 中,在顯示響應階段,這個錯誤消息與任何其他檢驗錯誤一起顯示。

階段 3:處理檢驗

轉換和檢驗一般發生在處理檢驗 階段。組件轉換並存儲它的 submittedValue。例如,如果字段綁定 到一個 Integer 屬性,那麼值就轉換為一個 Integer。如果值轉換失敗,那麼生成一個錯誤消息並放在 FacesContext 中,在顯示響應階段,這個錯誤消息與任何其他檢驗錯誤一起顯示。

在應用請求值階段之後,發生生命周期的第一次事件處理。在這個階段,根據應用程序的檢驗規則檢 驗每個組件的值。檢驗規則可以是預定義的(JSF 附帶的),也可以由開發人員定義。將用戶輸入的值與 檢驗規則進行對比。如果輸入的值是無效的,就將一個錯誤消息添加到 FacesContext 中,並將組件標為 無效。如果一個組件被標為無效,JSF 就跳過其他階段,進入顯示響應階段,就會顯示當前的視圖和檢驗 錯誤消息。如果沒有發生檢驗錯誤,JSF 就進入更新模型值階段。

階段 4:更新模型值

JSF 應用程序生命周期的第四個階段 — 更新模型值 — 通過更新托管 bean 的屬性,更新服務器端 模型的實際值。只有綁定到一個組件的值的 bean 屬性被更新。注意,這個階段在檢驗之後發生,所以可 以確信復制到 bean 屬性的值是有效的(至少在表單字段級上有效;它們在業務規則級上仍然可能是無效 的)。

階段 5:調用應用程序

在生命周期的第五個階段 — 調用應用程序 — JSF 控制器調用應用程序來處理表單提交。組件值已 經經過轉換、檢驗並應用於模型對象,所以現在可以使用它們執行應用程序的業務邏輯。

在這個階段,調用您的動作處理方法,比如這個示例應用程序的 ContactController 中的 persist() 方法和 read() 方法。

在這個階段,還為一個給定的序列或可能的多個序列指定下一個邏輯視圖。對於成功的表單提交,可 以定義特定的結果並返回這個結果。例如,在得到成功的結果時,將用戶轉移到下一個頁面。為了讓這個 導航操作起作用,必須在 faces-config.xml 文件中以導航規則的形式為成功的結果創建一個映射。發生 導航之後,就進入生命周期的最後一個階段。JSF 獲得從動作方法返回的對象並調用它的 toString() 方 法。然後使用這個值作為導航規則的結果。

階段 6:顯示響應

在生命周期的第六個階段 — 顯示響應,顯示視圖和它的所有組件,這些組件都處於當前狀態。

圖 6 展示了 JSF 應用程序生命周期的六個階段(包括檢驗和事件處理)的對象狀態圖:

圖 6. JSF 應用程序生命周期的六個階段

JSF 數據轉換器

轉換過程可以確保數據是正確的對象或類型,因此將字符串值轉換為其他類型,比如 Date 對象、原 始數據類型 float 或 Float 對象。可以使用內置的轉換器,也可以編寫定制的轉換器。本節討論 JSF 的標准轉換器,然後詳細討論定制的轉換器。

JSF 的標准轉換器

JSF 提供了許多標准的數據轉換器,而且大多數數據轉換是自動發生的。表 1 給出用於簡單數據轉換 的轉換器 ID 和對應的實現類。

表 1. 標准的 JSF 轉換器

轉換器 實現類 javax.faces.BigDecimal javax.faces.convert.BigDecimalConverter javax.faces.BigInteger javax.faces.convert.BigIntegerConverter javax.faces.Boolean javax.faces.convert.BooleanConverter javax.faces.Byte javax.faces.convert.ByteConverter javax.faces.Character javax.faces.convert.CharacterConverter javax.faces.DateTime javax.faces.convert.DateTimeConverter javax.faces.Double javax.faces.convert.DoubleConverter javax.faces.Float javax.faces.convert.FloatConverter

所以,如果綁定到一個 int 或 Integer,那麼會自動執行轉換。清單 19 給出聯系人管理應用程序的 一個組件,它直接綁定到 age:#{contactController.contact.age}。

清單 19. 綁定到 age:JSF 自動執行轉換

  <%-- age --%>
<h:outputLabel value="Age" for="age" accesskey="age" />
<h:inputText id="age" size="3" value="#{contactController.contact.age}">
</h:inputText>

JSF 會為所有原始數據類型、包裝器、String 和 Enum 屬性執行自動轉換。它還會轉換日期和數字。 數字可以有許多種格式,所以它的轉換器允許指定最終用戶將使用的格式。對於日期,也是如此。清單 20 演示如何使用 JSF 轉換器將日期轉換為指定的格式。

盡管 JSF 在默認情況下可以很好地處理原始數據類型等數據,但是在處理日期數據時,必須指定 <f:convertDateTime/> 轉換標記。這個標記基於 java.text 包並使用短模式、長模式和定制模式 。清單 20 演示如何使用 <f:convertDateTime/> 將用戶的生日轉換為 MM/yyyy(月/年)格式的 日期對象。在 java.text.SimpleDateFormat 的 Java API 文檔中可以找到模式的列表(參見 參考資料 )。

清單 20. 指定日期的格式

  <%-- birthDate --%>
<h:outputLabel value="Birth Date" for="birthDate" accesskey="b" />
  <h:inputText id="birthDate" value="#{contactController.contact.birthDate}">
    <f:convertDateTime pattern="MM/yyyy"/>
  </h:inputText>
<h:message for="birthDate" errorClass="errorClass" />

JSF 的定制轉換器

如果需要將字段數據轉換為應用程序特有的值對象,就需要定制的數據轉換,比如:

將 String 轉換為 PhoneNumber 對象(PhoneNumber.areaCode、PhoneNumber.prefix 等等)

將 String 轉換為 Name 對象(Name.first、Name.last)

將 String 轉換為 ProductCode 對象(ProductCode.partNum、ProductCode.rev 等等)

將 String 轉換為 Group

將 String 轉換為 Tags

為了創建定制的轉換器,必須:

實現 Converter 接口(也稱為 javax.faxes.convert.Converter)。

實現 getAsObject() 方法,這個方法將字段(字符串)轉換為對象(例如 PhoneNumber)。

實現 getAsString 方法,這個方法將對象(例如,PhoneNumber)轉換為字符串。

在 Faces 上下文中注冊定制轉換器。

圖 7 說明這些步驟在 JSF 應用程序生命周期中的位置:

圖 7. 定制轉換器 getAsObject() 和 getAsString() 方法

在圖 7 中,JSF 在處理檢驗階段調用定制轉換器的 getAsObject() 方法。在這個方法中,必須將請 求字符串值轉換為所需的對象類型,然後將這個對象返回給對應的 JSF 組件。在將值返回到視圖時,JSF 在顯示響應階段調用 getAsString 方法。這意味著轉換器也負責將對象數據轉換回字符串。

實現 Converter 接口

這個示例應用程序的 Contact 領域對象與 Group 之間存在多對一關系,與 Tag 之間存在多對多關系 。在前面(見 清單 9 和 清單 11),在 ContactController 中定義了從 id 值到領域對象的轉換。並 不在視圖中直接綁定到領域屬性,而是在 ContactController 中綁定到 id 字段。如果使用 JSF 轉換器 ,就可以減少許多代碼,這會大大簡化控制器和視圖。

清單 21 和清單 22 分別為 Group 和 Tag 轉換器實現 Converter 接口:

清單 21. 實現 Converter 接口的 Group 轉換器

package com.arcmind.contact.converter;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.application.FacesMessage;
import com.arcmind.contact.model.Group;
import com.arcmind.contact.model.GroupRepository;
public class GroupConverter implements Converter {
  ...
}

清單 22. 實現 Converter 接口的 Tag 轉換器

package com.arcmind.contact.converter;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import com.arcmind.contact.model.Tag;
import com.arcmind.contact.model.TagRepository;
public class TagConverter implements Converter {
  ...
}

實現 getAsObject() 方法

下一步是實現 getAsObject() 方法,這個方法將字段(字符串)轉換為對象。清單 23 給出 GroupConverter 的 getAsObject() 方法:

清單 23. GroupConverter 的 getAsObject() 方法

...
public class GroupConverter implements Converter {
  public Object getAsObject(FacesContext facesContext, UIComponent component,
     String value) {
    GroupRepository repo = (GroupRepository) facesContext
       .getExternalContext().getApplicationMap()
       .get("groupRepository");
    Long id = Long.valueOf(value);
    if (id == -1L) {
     throw new ConverterException(new FacesMessage(
        FacesMessage.SEVERITY_ERROR, "required", "required"));
    }
    return repo.lookup(id);
  }
  ...
}

注意,GroupConverter 查找 groupRepository 並使用 groupRepository 從存儲庫中讀取 Group。還 要注意,它檢查值是否是 -1L;如果是這樣,就通過拋出一個新的 ConverterException 輸出所需的消息 。

TagConverter 是相似的。它使用 tagRepository 查找標記值。清單 24 給出 TagConverter 的 getAsObject() 方法:

清單 24. TagConverter 的 getAsObject() 方法

...
public class TagConverter implements Converter {
  public Object getAsObject(FacesContext facesContext, UIComponent component,
     String value) {
    TagRepository repo = (TagRepository) facesContext
       .getExternalContext().getApplicationMap()
       .get("tagRepository");
    return repo.lookup(Long.valueOf(value));
  }
  ...
}

這兩個轉換器都沒有進行全面的錯誤檢查。如果存儲庫對象實際上與數據庫或緩存服務器通信,那麼 可能需要把 getAsObject() 包裝在 try/catch 塊中,並在數據庫出現問題時生成一個嚴重性為 SEVERITY_FATAL 的 FacesMessage — 就像 清單 23 中的 GroupConverter 處理 -1L 的方法一樣。

實現 getAsString 方法

JSF 需要顯示當前選擇的值。這需要調用 Converter 的 getAsString 方法。清單 25 給出 GroupConverter 的 getAsString 方法:

清單 25. GroupConverter 的 getAsString 方法

...
public class GroupConverter implements Converter {
  ...

  public String getAsString(FacesContext facesContext, UIComponent component,
     Object value) {
    return value == null ? "-1" : "" + ((Group) value).getId();
  }
}

清單 26 給出 TagConverter 的 getAsString() 方法:

清單 26. TagConverter 的 getAsString() 方法

...
public class TagConverter implements Converter {
  ...

  public String getAsString(FacesContext facesContext, UIComponent component,
     Object value) {
    return value == null ? "-1" : "" + ((Tag) value).getId();
  }
}

在 Faces 上下文中注冊定制轉換器

編寫了自己的轉換器之後,需要讓 JSF 在每次遇到導致 Group 或 Tag 的值綁定時使用這些轉換器。 這需要在 faces-config.xml 文件中使用 <converter> 元素注冊轉換器,見清單 27:

清單 27. 在 faces-config.xml 中注冊轉換器

<converter>
  <converter-for-class>
    com.arcmind.contact.model.Group
  </converter-for-class>
  <converter-class>
    com.arcmind.contact.converter.GroupConverter
  </converter-class>
</converter>
<converter>
  <converter-for-class>
    com.arcmind.contact.model.Tag
  </converter-for-class>
  <converter-class>
    com.arcmind.contact.converter.TagConverter
  </converter-class>
</converter>

清單 27 用 <converter-class> 元素指定轉換器類,用 <converter-for-class> 元素 指定轉換所針對的類。

讓轉換器處理一組標記

遺憾的是,轉換器不能處理泛型,比如 List<Tag>。JSF 不允許對泛型列表進行轉換。(它應 該可以。Java Persistence API [JPA] 可以用 List<Tag> 定義關系。JPA 和 JSF 1.2 都是與 Java EE 5 同時出現的,所以您會認為它們都支持泛型。)為了解決這個問題,可以使用數組。清單 28 演示如何使用 Tag 數組,而不是 List<Tag>:

清單 28. 使用數組代替泛型

public class Contact implements Serializable {
   ...
   private List<Tag> tags;
  public Tag[] getTags() {
    if (tags != null) {
      return tags.toArray(new Tag[tags.size()]);
    } else {
      return null;
    }
  }
  public void setTags(Tag[] tags) {
    this.tags = Arrays.asList(tags);
  } 

JSF 檢驗器

轉換和檢驗的主要用途是,在更新模型數據之前,確保值符合要求。這樣,在調用應用程序方法來處 理數據時,就可以對模型的狀態做某些假設。通過使用轉換和檢驗,可以集中精力考慮業務邏輯,而不必 為輸入數據的限制條件(比如空值檢測、長度限制、范圍邊界等等)操心。

所以,應該在更新模型數據階段中將組件數據綁定到托管 bean 模型之前執行轉換和檢驗。正如在 “ JSF 應用程序的生命周期” 一節中看到的,在處理檢驗階段進行轉換和檢驗 — 先轉換,再檢驗。

在 JSF 中有四種檢驗形式:

內置的檢驗組件

應用程序級檢驗

後端 bean 中的檢驗方法(內聯)

定制的檢驗組件(它們實現 Validator 接口)

本節解釋這些檢驗形式並演示它們的使用方法。

標准檢驗

JSF 提供三個標准檢驗組件:

DoubleRangeValidator:組件的本地值必須是數字類型的;必須處於最小值、最大值或這兩者指定的 范圍內。

LongRangeValidator:組件的本地值必須是數字類型的,並可以轉換為 long;必須處於最小值、最大 值或這兩者指定的范圍內。

LengthValidator:類型必須是 string;長度必須處於最小值、最大值或這兩者指定的范圍內。

在這個示例應用程序中,聯系人的年齡可以是任何有效的整數。因為 -2 這樣的年齡是沒有意義的, 所以需要給這個字段添加某些檢驗。清單 29 使用 <f:validateLongRange> 進行簡單的檢驗,確 保年齡字段中的數據是有意義的:

清單 29. 使用 <f:validateLongRange> 檢驗年齡的值是否合理

  <%-- age --%>
<h:outputLabel value="Age" for="age" accesskey="age" />
<h:inputText id="age" size="3" value="#{contactController.contact.age}">
  <f:validateLongRange minimum="0" maximum="150"/>
</h:inputText>
<h:message for="age" errorClass="errorClass" />

在檢驗年齡字段之後,可能希望為名字字段指定長度限制,見清單 30。

清單 30. 確保 firstName 不是太長也不是太短

  <%-- First Name --%>
<h:outputLabel value="First Name" for="firstName" accesskey="f" />
<h:inputText id="firstName" label="First Name" required="true"
value="#{contactController.contact.firstName}" size="10" >
  <f:validateLength minimum="2" maximum="25" />
</h:inputText>
<h:message for="firstName" errorClass="errorClass" />

盡管 JSF 內置的檢驗在許多場景中都是有效的,但是它們的功能有限。在處理電子郵件、電話號碼、 URL、日期等數據時,編寫自己的檢驗器可能更好(本節後面會討論定制的檢驗器)。還可以使用 Tomahawk、Shale、JSF-Validations 和 Crank 提供的檢驗器。

應用程序級檢驗

從概念上說,應用程序級檢驗實際上是業務邏輯檢驗。JSF 將表單級或字段級檢驗與業務邏輯檢驗分 隔開。應用程序級檢驗需要在使用模型的托管 bean 方法中添加代碼,以確保綁定到模型的數據的質量。 例如,對於購物車來說,可以使用表單級檢驗確保輸入的數量是有效的,但是還需要通過業務邏輯檢驗檢 查用戶是否超過了他的信用限額。這是 JSF 中關注點隔離的另一個例子。

假設用戶單擊一個綁定到動作方法的按鈕,這個動作方法在調用應用程序階段被調用(細節參見前面 的 圖 5)。在對模型數據進行任何操作之前(通常在更新模型階段更新模型數據),可以根據應用程序 的業務邏輯檢查輸入的數據是否是有效的。

例如,在這個示例應用程序中,用戶單擊 Update/Add 按鈕,這個按鈕綁定到應用程序控制器的 persist() 方法。可以在 persist() 方法中添加檢驗代碼,檢查系統中是否已經存在當前的 firstName/lastName 組合。如果這個聯系人已經存在,那麼可以在 FacesContext 中添加一個消息,然 後返回 null(如果在這個動作上應用了導航規則),從而讓 JSF 留在當前視圖上。

我們再看一下聯系人應用程序,這一次在 persist() 動作方法中執行一些應用程序級邏輯,見清單 31 和清單 32。清單 31 給出控制器中的應用程序級檢驗邏輯:

清單 31. 控制器中的應用程序級檢驗邏輯

public class ContactController {
  public String persist() {

    /* Perform the application level validation. */
     try {
       contact.validate();
     } catch (ContactValidationException contactValidationException) {
       addErrorMessage(contactValidationException.getLocalizedMessage());
       return null;
     }

   
    /* Turn form off, turn link on. */
    form.setRendered(false);
    addNewCommand.setRendered(true);

   
    /* Add a status message. */
    if (contactRepository.persist(contact) == null) {
     addStatusMessage("Added " + contact);
    } else {
     addStatusMessage("Updated " + contact);
    }
    return "contactPersisted";
  }       
   private void addErrorMessage(String message) {
     FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(
         FacesMessage.SEVERITY_ERROR, message, null));
   }

在清單 31 中,persist() 方法調用 contact 對象上的 validate() 方法。它捕獲任何異常並把異常 錯誤消息轉換為 FacesMessage。如果發生異常,它會返回 null,其含義為:留在當前視圖上,不要導航 到下一個視圖。

實際的檢驗代碼包含在模型中 — 即,Contact 類的 validate() 方法,見清單 32。這一點很重要: 在為聯系人添加更多的檢驗代碼時,不需要修改控制器或視圖層。

清單 32. 檢驗代碼在模型中,而不在控制器中

...
public class Contact implements Serializable {
  ...       
   public void validate() throws ContactValidationException {
    if (
     (homePhoneNumber == null || "".equals(homePhoneNumber)) &&
     (workPhoneNumber == null || "".equals(workPhoneNumber)) &&
     (mobilePhoneNumber == null || "".equals(mobilePhoneNumber))      
    ) {
     throw new ContactValidationException("At least one phone number" +
        "must be set");

   }
}

應用程序級檢驗很簡單,也很容易使用。它的優點是:

容易實現

不需要單獨的類(定制檢驗器)

頁面作者不需要指定檢驗器

應用程序級檢驗的缺點是,它在其他形式的檢驗(標准、定制和組件)之後執行,而且錯誤消息只在 執行其他形式的檢驗之後顯示。

最後,應用程序級檢驗應該只用於需要業務邏輯檢驗的場合。

後端 bean 中的定制檢驗器

對於標准 JSF 檢驗器不支持的數據類型(包括電子郵件地址和 ZIP 編碼),需要構建自己的檢驗組 件。如果希望對顯示給最終用戶的檢驗消息進行顯式地控制,也需要構建自己的檢驗器。通過使用 JSF, 可以創建可插入的檢驗組件,可以在整個 Web 應用程序中重用這些組件。

如果不想創建單獨的檢驗器類,也可以在後端 bean 方法中實現定制的檢驗。這種方式對於應用程序 開發人員更合適。例如,可以在托管 bean 中編寫一個方法來檢驗電話號碼,見清單 33:

清單 33. 電話號碼檢驗

public class ContactValidators {
   private static Pattern phoneMask;
   static {
     String countryCode = "^[0-9]{1,2}";
     String areaCode = "(|-|\\(){1,2}[0-9]{3}(|-|\\)){1,2}";
     String prefix = "(|-)?[0-9]{3}";
     String number = "(|-)[0-9]{4}$";
     phoneMask = Pattern.compile(countryCode + areaCode + prefix + number);
   }
   public void validatePhone(FacesContext context, UIComponent component,
       Object value) throws ValidatorException {
     String sValue = (String)value;
     Matcher matcher = phoneMask.matcher(sValue);
     if (!matcher.matches()) {
       FacesMessage message = new FacesMessage();
       message.setDetail("Phone number not valid");
       message.setSummary("Phone number not valid");
       message.setSeverity(FacesMessage.SEVERITY_ERROR);
       throw new ValidatorException(message);
     }
   }
... //ADD MORE VALIDATION METHODS FOR THE APP HERE!
}

ContactValidators 類有一個 validatePhone() 方法。validatePhone() 方法使用 Java regex API 確保輸入的字符串是有效的電話號碼。如果值與模式不匹配,那麼 validatePhone() 方法會拋出一個 ValidatorException。

要使用 ContactValidators 類,需要在 faces-config.xml 文件中注冊它,見清單 34:

清單 34. 將 ContactValidators 注冊為托管 bean

<managed-bean>
<managed-bean-name>contactValidators</managed-bean-name>
<managed-bean-class>com.arcmind.contact.validators.ContactValidators</managed-bean -class>
<managed-bean-scope>application</managed-bean-scope>
</managed-bean>

要使用檢驗器,需要對工作電話號碼、家庭電話號碼和移動電話號碼使用 validator 屬性,見清單 35:

清單 35. 通過 validator 屬性在視圖中使用檢驗器

  <%-- Work --%>
<h:outputLabel value="Work" for="work" accesskey="w" />
<h:inputText id="work"
  value="#{contactController.contact.workPhoneNumber}" size="11"
  validator="#{contactValidators.validatePhone}" />
  <h:message for="work" errorClass="errorClass" />
  <%-- Home --%>
<h:outputLabel value="Home" for="home" accesskey="h" />
<h:inputText id="home"
  value="#{contactController.contact.homePhoneNumber}" size="11"
  validator="#{contactValidators.validatePhone}" />
<h:message for="home" errorClass="errorClass" />
  <%-- Mobile --%>
<h:outputLabel value="Mobile" for="mobile" accesskey="m" />
<h:inputText id="mobile"
  value="#{contactController.contact.mobilePhoneNumber}" size="11"
  validator="#{contactValidators.validatePhone}" />
<h:message for="mobile" errorClass="errorClass" />

可以看到,這裡把 validatePhone() 方法綁定到 <h:inputText> 組件:<h:inputText id="mobile" ... validator="#{contactValidators.validatePhone}"。

對於應用程序開發人員來說,使用托管 bean 執行檢驗是不錯的方法。但是,如果要開發可重用的框 架或可重用的組件集,那麼最好創建單獨的定制檢驗器。

單獨的定制檢驗器

可以使用 JSF 創建可插入的檢驗組件,這些組件可以在整個 Web 應用程序中重用。

要創建定制的檢驗器,需要執行以下步驟:

創建一個實現 Validator 接口(javax.faces.validator.Validator)的類。

實現 validate() 方法。

在 faces-config.xml 文件中注冊定制的檢驗器。

在 JSP 中使用 <f:validator/> 標記。

我們來逐一介紹這些步驟,並提供創建定制檢驗器的示例代碼。

步驟 1:實現 Validator 接口

第一步是實現 Validator 接口,見清單 36:

清單 36. 實現 Validator 接口

package com.arcmind.validators;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ZipCodeValidator implements Validator {
   /** Accepts zip codes like 85710 */
   private static final String ZIP_REGEX = "[0-9]{5}";
   /** Optionally accepts a plus 4 */
   private static final String PLUS4_OPTIONAL_REGEX = "([ |-]{1}[0-9]{4})?";
   private static Pattern mask = null;
   static {
     mask = Pattern.compile(ZIP_REGEX + PLUS4_OPTIONAL_REGEX);
   }

步驟 2:實現 validate() 方法

接下來,需要實現 validate() 方法,見清單 37:

清單 37. 實現 validate() 方法

public class ZipCodeValidator implements Validator {
   ...
   public void validate(FacesContext context, UIComponent component,
       Object value) throws ValidatorException {
     /* Get the string value of the current field */
     String zipField = (String) value;
     /* Check to see if the value is a zip code */
     Matcher matcher = mask.matcher(zipField);
     if (!matcher.matches()) {
       FacesMessage message = new FacesMessage();
       message.setDetail("Zip code not valid");
       message.setSummary("Zip code not valid");
       message.setSeverity(FacesMessage.SEVERITY_ERROR);
       throw new ValidatorException(message);
     }
   }
}

步驟 3:注冊定制檢驗器

現在,您應該對向 FacesContext 注冊定制檢驗器的代碼很熟悉了(見清單 38):

清單 38. 在 faces-config.xml 中注冊定制檢驗器

<validator>
  <validator-id>arcmind.zipCode</validator-id>
  <validator-class>com.arcmind.validators.ZipCodeValidator</validator-class>
</validator>

步驟 4:在 JSP 中使用 <f:validator> 標記

<f:validator/> 標記聲明使用 zipCode 檢驗器,見清單 39:

清單 39. 在 JSP 中使用 <f:validator> 標記

  <%-- zip --%>
<h:outputLabel value="Zip" for="zip" accesskey="zip" />
<h:inputText id="zip" size="5"
  value="#{contactController.contact.zip}">
  <f:validator validatorId="arcmind.zipCode"/>
</h:inputText>
<h:message for="zip" errorClass="errorClass" />

總之,創建定制檢驗器是非常容易的,而且這些檢驗器可以跨許多應用程序重用。缺點是必須創建一 個單獨的類,並在 faces 上下文中管理檢驗器的注冊。但是,可以進一步改進定制檢驗器的實現:創建 一個使用這個檢驗器的定制標記,使它看起來像內置的檢驗。對於需要經常檢驗的數據,比如電子郵件地 址,這種方法可以簡化代碼,盡可能增加代碼重用和提高應用程序行為的一致性。

再論檢驗和轉換

在到達檢驗階段之前,轉換已經執行過了。例如,如果有一個 int 屬性綁定到 inputText 字段,那 麼先對這個字段進行轉換,然後再進行檢驗。

假設您有一個 PhoneNumber 值對象,並使用它(而不是使用 String)在 Contact 中存儲電話號碼。 那麼 清單 33 中電話號碼的檢驗規則就沒什麼意義了。實際上,這個檢驗規則只證明 String 采用了電 話號碼的格式。這個邏輯實際上應該放在轉換器中,見清單 40:

清單 40. 再論檢驗和轉換:PhoneConverter

package com.arcmind.contact.converter;
import java.util.regex.Pattern;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import com.arcmind.contact.model.PhoneNumber;
/**
* @author Richard Hightower
*
*/
public class PhoneConverter implements Converter {
   private static Pattern phoneMask;
   static {
     String countryCode = "^[0-9]{1,2}";
     String areaCode = "( |-|\\(){1,2}[0-9]{3}( |-|\\)){1,2}";
     String prefix = "( |-)?[0-9]{3}";
     String number = "( |-)[0-9]{4}$";
     phoneMask = Pattern.compile(countryCode + areaCode + prefix + number);
   }
   public Object getAsObject(FacesContext context, UIComponent component,
       String value) {
     System.out.println("PhoneConverter.getAsObject()");
     if (value.isEmpty()) {
       return null;
     }
     /* Before we parse, let's see if it really is a phone number. */
     if (!phoneMask.matcher(value).matches()) {
       FacesMessage message = new FacesMessage();
       message.setDetail("Phone number not valid");
       message.setSummary("Phone number not valid");
       message.setSeverity(FacesMessage.SEVERITY_ERROR);
       throw new ConverterException(message);
     }

     /* Now let's parse the string and populate a phone number object. */
     PhoneNumber phone = new PhoneNumber();
     phone.setOriginal(value);
     String[] phoneComps = value.split("[ ,()-]");
     String countryCode = phoneComps[0];
     phone.setCountryCode(countryCode);
     if ("1".equals(countryCode) && phoneComps.length == 4) {
       phone.setAreaCode(phoneComps[1]);
       phone.setPrefix(phoneComps[2]);
       phone.setNumber(phoneComps[3]);
     } else if ("1".equals(countryCode) && phoneComps.length != 4) {
       throw new ConverterException(new FacesMessage(
           "No Soup for you butter fingers!"));
     } else if (phoneComps.length == 1 && value.length() > 10){
       phone.setCountryCode(value.substring(0,1));
       phone.setAreaCode(value.substring(1,4));
       phone.setPrefix(value.substring(4,7));
       phone.setNumber(value.substring(7));      
     } else {
       phone.setNumber(value);
     }
     return phone;
   }
   public String getAsString(FacesContext context, UIComponent component,
       Object value) {
     System.out.println("PhoneConverter.getAsString()");
     return value.toString();
   }
}

與檢驗器不同,轉換器的好處是可以在 faces-config.xml 中注冊(見 清單 27),讓轉換器連接到 某個類。每當這個類出現在表達式語言(EL)值綁定中時,會自動使用這個轉換器;不需要在 JSP 中添 加 <f:converter>。新的電話號碼轉換器會自動應用於 PhoneNumber,不需要在視圖中指定轉換器 。

原來的電話號碼檢驗成了電話號碼轉換的一部分,您可能想知道電話號碼檢驗器現在是什麼樣子。可 以通過編寫一個檢驗器來回答這個問題,它證明電話號碼屬於亞利桑那州,見清單 41:

清單 41. 確保電話號碼屬於亞利桑那州

package com.arcmind.contact.validators;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;
import com.arcmind.contact.model.PhoneNumber;
public class ContactValidators {
   public void validatePhone(FacesContext context, UIComponent component,
       Object value) throws ValidatorException {
     System.out.println("ContactValidators.validatePhone()");
     PhoneNumber phoneNumber = (PhoneNumber)value;
     if (!phoneNumber.getAreaCode().equals("520")
       && !phoneNumber.getAreaCode().equals("602")) {
       FacesMessage message = new FacesMessage();
       message.setDetail("Arizona residents only");
       message.setSummary("Arizona residents only");
       message.setSeverity(FacesMessage.SEVERITY_ERROR);
       throw new ValidatorException(message);
     }
   }
}

注意,與前面的檢驗器不同,這個電話號碼檢驗器並不處理 String。在調用它之前,已經調用了轉換 器。因此,值並不是 String,而是 PhoneNumber。

處理階段監聽器

JSF API 文檔(參見 參考資料)指出,PhaseListener 是 “一個接口,如果對象希望在請求處理生 命周期的每個標准階段開始和結束時得到通知,就需要實現這個接口”(Sun Microsystems Inc.,2006 年)。我們已經編寫了一些轉換器、檢驗器和動作方法,在本節中就來編寫一些階段監聽器。在 JSF 1.2 之前,PhaseListener 是以全局方式定義的。在 JSF 1.2 中,可以在視圖級注冊 PhaseListener 事件, 或者使用 <f:phaseListener binding="..." /> 標記。

實現階段監聽器

為了實現階段監聽器,需要實現 PhaseListener 接口,見清單 42:

清單 42. DebugPhaseListener

package com.arcmind.phase;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
@SuppressWarnings("serial")
public class DebugPhaseListener implements PhaseListener {
   public void beforePhase(PhaseEvent phaseEvent) {
     System.out.println("------ BEFORE PHASE " + phaseEvent.getPhaseId());
   }
   public void afterPhase(PhaseEvent phaseEvent) {
     System.out.println("------ AFTER PHASE " + phaseEvent.getPhaseId());
     if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE) {
       System.out.println("REQUEST END\n\n");
     }
   }
   public PhaseId getPhaseId() {
     return PhaseId.ANY_PHASE;
   }
}

PhaseListener 會在每個 JSF 階段之前和之後通知您。用 getPhaseId 方法告訴 JSF 您對哪些階段 感興趣。PhaseId.ANY_PHASE 表示希望在每個階段之前和之後都得到通知。DebugPhaseListener 輸出階 段事件名稱,讓您可以看到發生了什麼情況。我在每個檢驗、轉換和動作方法中添加了 System.out.println 語句。還在 firstName 屬性的獲取方法/設置方法中添加了 System.out.println 語句,讓您可以看到系統什麼時候訪問它。

接下來,必須在 faces-config.xml 中注冊階段監聽器,見清單 43:

清單 43. 在 faces-config.xml 中注冊 DebugPhaseListener

<lifecycle>
  <phase-listener>com.arcmind.phase.DebugPhaseListener</phase-listener>
</lifecycle>

階段監聽器的輸出

清單 44 給出第一次裝載表單時的輸出:

清單 44. 第一次裝載表單時 DebugPhaseListener 的輸出

------ BEFORE PHASE RESTORE_VIEW 1
------ AFTER PHASE RESTORE_VIEW 1
------ BEFORE PHASE RENDER_RESPONSE 6
ContactController.getContacts()
------ AFTER PHASE RENDER_RESPONSE 6
REQUEST END

根據 清單 42 中的代碼,JSF 發現這個請求是對一個視圖的初始請求,它使用 JSP 構建視圖,然後 直接進入顯示響應階段。注意,在顯示響應階段,會調用控制器的 getContacts() 方法。

在單擊 Add New 鏈接時,會看到清單 45 所示的輸出:

清單 45. 調用 addNew() 方法之後 DebugPhaseListener 的輸出

------ BEFORE PHASE RESTORE_VIEW 1
------ AFTER PHASE RESTORE_VIEW 1
------ BEFORE PHASE APPLY_REQUEST_VALUES 2
------ AFTER PHASE APPLY_REQUEST_VALUES 2
------ BEFORE PHASE PROCESS_VALIDATIONS 3
------ AFTER PHASE PROCESS_VALIDATIONS 3
------ BEFORE PHASE UPDATE_MODEL_VALUES 4
------ AFTER PHASE UPDATE_MODEL_VALUES 4
------ BEFORE PHASE INVOKE_APPLICATION 5
ContactController.addNew()
------ AFTER PHASE INVOKE_APPLICATION 5
------ BEFORE PHASE RENDER_RESPONSE 6
Contact.getFirstName()
ContactController.getGroups()
ContactController.getGroups()
ContactController.getAvailableTags()
ContactController.getContacts()
------ AFTER PHASE RENDER_RESPONSE 6
REQUEST END

addNew() 方法會使表單顯示出來。因為 addNew() 是一個 postback,所以 JSF 會經歷所有階段。因 為在調用 addNew() 方法之前不顯示表單,所以在調用 addNew() 方法之前不處理它的字段。

接下來,輸入無效的 ZIP 編碼(比如 aaa)和無效的電話號碼(比如 aaa)。然後選擇兩個標記和一 個組,並單擊 Add 按鈕。這時會看到清單 46 所示的輸出:

清單 46. 輸入無效 ZIP、無效電話號碼、一個組和一些標記之後的輸出

------ BEFORE PHASE RESTORE_VIEW 1
------ AFTER PHASE RESTORE_VIEW 1
------ BEFORE PHASE APPLY_REQUEST_VALUES 2
------ AFTER PHASE APPLY_REQUEST_VALUES 2
------ BEFORE PHASE PROCESS_VALIDATIONS 3
GroupConverter.getAsObject
ContactController.getAvailableGroups()
ZipCodeValidator.validate()
ContactValidators.validatePhone()
TagConverter.getAsObject
TagConverter.getAsObject
ContactController.getAvailableTags()
ContactController.getAvailableTags()
------ AFTER PHASE PROCESS_VALIDATIONS 3
------ BEFORE PHASE RENDER_RESPONSE 6
ContactController.getAvailableGroups()
GroupConverter.getAsString
GroupConverter.getAsString
GroupConverter.getAsString
ContactController.getAvailableTags()
TagConverter.getAsString
TagConverter.getAsString
TagConverter.getAsString
TagConverter.getAsString
ContactController.getContacts()
------ AFTER PHASE RENDER_RESPONSE 6
REQUEST END

在處理檢驗階段,為選擇的組調用 GroupConverter.getAsObject() 方法。調用 TagConverter.getAsObject() 兩次,分別針對選擇的每個標記。在處理檢驗階段,調用前面編寫的定制 檢驗器 — ZipCodeValidator.validate() 和 ContactValidators.validatePhone()。在顯示響應階段, 為可用組和可用標記列表中的每個對象,調用轉換器的 getAsString() 方法。

現在可以假設,在 ContactController.getAvailableGroups() 和 ContactController.getAvailableTags() 每次訪問數據庫時,您需要訪問數據庫 4 次才能獲得組列表和 標記列表。您可能希望添加一些邏輯,以便只為每個請求訪問數據庫一次。例如,可以只在數據還未裝載 過的情況下,才從動作裝載數據。

提交有效表單時的輸出見清單 47:

清單 47. 完整的表單提交處理過程

------ BEFORE PHASE RESTORE_VIEW 1
------ AFTER PHASE RESTORE_VIEW 1
------ BEFORE PHASE APPLY_REQUEST_VALUES 2
------ AFTER PHASE APPLY_REQUEST_VALUES 2
------ BEFORE PHASE PROCESS_VALIDATIONS 3
GroupConverter.getAsObject
ContactController.getAvailableGroups()
ZipCodeValidator.validate()
ContactValidators.validatePhone()
ContactValidators.validatePhone()
ContactValidators.validatePhone()
TagConverter.getAsObject
TagConverter.getAsObject
ContactController.getAvailableTags()
ContactController.getAvailableTags()
------ AFTER PHASE PROCESS_VALIDATIONS 3
------ BEFORE PHASE UPDATE_MODEL_VALUES 4
Contact.setFirstName()
------ AFTER PHASE UPDATE_MODEL_VALUES 4
------ BEFORE PHASE INVOKE_APPLICATION 5
ContactController.persist()
------ AFTER PHASE INVOKE_APPLICATION 5
------ BEFORE PHASE RENDER_RESPONSE 6
ContactController.getContacts()
ContactController.getContacts()
ContactController.getContacts()
ContactController.getContacts()
ContactController.getContacts()
ContactController.getContacts()
Contact.getFirstName()
ContactController.getContacts()
ContactController.getContacts()
------ AFTER PHASE RENDER_RESPONSE 6
REQUEST END

清單 47 包含請求生命周期的所有階段。注意,這裡經過了 INVOKE_APPLICATION 階段並調用了 persist():ContactController.persist()。

添加一個對象級檢驗框架

可以使用 PhaseListener 捕捉階段監聽器事件並改變 JSF 處理請求的方式。假設您希望在 JSF 中添 加自己的對象級檢驗框架。可以使用清單 48 這樣的接口:

清單 48. 驗證器接口

package com.arcmind.contact.model;
public interface Validateable {
  public void validate() throws ValidationException;
}

然後修改 Contact 對象,讓它實現這個接口,見清單 49:

清單 49. Contact 實現驗證器接口

package com.arcmind.contact.model;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@SuppressWarnings("serial")
public class Contact implements Serializable, Validateable {
   ...
   public void validate() throws ValidationException {
     if (
       homePhoneNumber == null &&
       workPhoneNumber == null &&
       mobilePhoneNumber == null      
      ) {
       throw new ValidationException("At least one phone number " +
       "must be set", "");

     }
   }
...

可以創建一個 PhaseListener 尋找並檢驗 Validateable 對象。這個 PhaseListener 可以接收異常 消息並把它們轉換為 FacesMessage,但是如何知道什麼時候需要檢驗對象呢?看起來這一信息只能放在 控制器類中。因此,為了按照通用方式處理這個問題,可以編寫一個超類控制器,它創建一個匿名的內部 類 PhaseListener,您可以綁定到這個內部類,見清單 50:

清單 50. 包含 PhaseListener 的基類

package com.arcmind.contact.controller;
import java.io.Serializable;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import com.arcmind.contact.model.Validateable;
import com.arcmind.contact.model.ValidationException;
@SuppressWarnings("serial")
public abstract class AbstractCrudController implements Serializable {
  private boolean edit;
  public PhaseListener phaseListener = new PhaseListener() {

    public void afterPhase(PhaseEvent event) {
     validate();
    }
    public void beforePhase(PhaseEvent event) {
    }
    public PhaseId getPhaseId() {
     return PhaseId.UPDATE_MODEL_VALUES;
    }

  };

  abstract Object getFormObject(); //subclass defines this
  private void validate() {
    Object form = getFormObject();
    if (! (form instanceof Validateable) || form == null) {
     return;
    }
    Validateable validateable = (Validateable) form;
    try {
     validateable.validate(); //validate object
    } catch (ValidationException validationException) {
     FacesContext.getCurrentInstance().renderResponse(); //Do not invoke application.
     addErrorMessage(validationException.getMessage());
    }
  }
  public PhaseListener getPhaseListener() {
    return phaseListener;
  }
   protected void addErrorMessage(String message) {
     FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(
         FacesMessage.SEVERITY_ERROR, message, null));
   }
   public boolean isEdit() {
     return edit;
   }
   public void setEdit(boolean edit) {
     this.edit = edit;
   }
}

注意,PhaseListener 只監聽 UPDATE_MODEL_VALUES。它然後調用 validate() 方法。這個方法檢查 form 對象是否存在以及是否是驗證器的一個實例,然後在領域對象上調用 validate(),這會讓模型執行 檢驗。還要注意,如果模型檢驗失敗,AbstractCrudCOntroller 的 validate() 方法會調用 FacesContext.getCurrentInstance().renderResponse(),這會使 JSF 跳過 INVOKE_APPLICATION 階段 。

現在,將 ContactController 改為 AbstractCrudController 的子類,見清單 51:

清單 51. ContactController 擴展 AbstractCrudController,從而提供 PhaseListener 檢驗支持

public class ContactController extends AbstractCrudController{
  ...
  public void addNew() {
    ... same as before except sets the edit mode.
    super.setEdit(true);
  }
  public void persist() {
    ... same as before except sets the edit mode.
    super.setEdit(false);
  }
  public void read() {
    ... same as before except sets the edit mode.
    super.setEdit(true);
  }
   @Override
   Object getFormObject() {
     if (super.isEdit()) {
       return this.contact;
     } else {
       return null;
     }
   }
}

注意,清單 51 實現 AbstractCrudController 中的抽象方法 getFormObject。它使用編輯模式,所 以只有當控制器處於編輯模式時,它才會返回聯系人對象。控制器按照這種方式判斷什麼時候應該檢驗對 象。

最後,需要注冊您的階段監聽器,見清單 52:

清單 52. 綁定到 phaseListener 屬性

<f:view>
  <h3>Contacts (4th version)</h3>
  <f:phaseListener binding="#{contactController.phaseListener}" />

可以使用階段監聽器改變 JSF 處理請求的方式,因此可以根據自己的需要擴展 JSF 框架。

結束語

本教程討論了 JSF 的請求處理生命周期,並演示了組件模型的一些基本特性。本教程用大量篇幅討論 了 JSF 轉換和檢驗。實際上,本教程已經涵蓋了在應用程序中進行轉換和檢驗時所需了解的大部分知識 (至少對於這個 JSF 版本差不多了)!當然,本教程不可能覆蓋所有內容。例如,對於 JSF 沒有提供或 此處未討論的檢驗器組件,您可能希望利用 Tomahawk、Crank、Shale 和 jsf-comp(參見 參考資料)。 另外,盡管本教程討論了最常用的轉換和檢驗技術,但是還有其他一些技術未涉及到。

要記住,轉換和檢驗不一定能夠很好地配合。轉換器把字符串轉換為對象,而大多數標准檢驗器都處 理字符串。因此,在同時使用定制轉換器和檢驗器時,必須小心。例如,本教程中的 PhoneNumber 對象 不能使用長度檢驗器。在這種情況下,要麼也編寫一個定制檢驗器,要麼將所有特殊的檢驗邏輯包含在定 制轉換器中。我喜歡後一種方式,因為只需把一個定制轉換器(包含內置的檢驗邏輯)與某一對象類型關 聯起來,然後讓 JSF 處理這個對象類型。JSF 會自動執行轉換和檢驗,而不需要在 JSP 中指定任何轉換 器 id。(當然,有人會認為這種編程方式是偷懶,而且它不一定適合所有情況。)

JSF 為 Web 應用程序開發提供了一種靈活、強大且可插入的框架。除了標准的轉換器和檢驗器之外, JSF 還允許應用程序和框架開發人員以自己喜歡的方式對實現進行定制。最後,選用哪種轉換和檢驗策略 由您自己決定。通過使用 JSF,在原型設計階段,可以輕松快速地建立原型(使用標准轉換器、檢驗器、 內聯檢驗);在以後的開發階段,可以遷移到更復雜的生產解決方案(使用定制對象、定制消息)。另外 ,無論處於哪個開發階段,JSF 應用程序生命周期都提供一個可靠的基礎結構,幫助您以一致的方式確保 數據模型的完整性。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved