轉換和驗證
雖然在 JSF Web 應用程序中使用轉換和驗證不一定要理解 JavaServer Faces 生命週期的基礎知識,但是在深入轉換和驗證內容之前,最好對一些基本知識做一回顧。此外,掌握一點 JSF 生命週期技巧可以極大地幫助簡化 Web 應用程序的開發工作。還有助於更好地理解 JSF 的可插入能力。
圖 1 描繪了我們所說的“基本 JSF 生命週期”。 基本 是在暗示這只是一個典型的處理所提交表單值的請求-響應(request-and-response)場景。
顯然,不同的場景對這裏重點描述的生命週期有不同的影響。我們將在本文稍後介紹其中一些場景。現在,只需要注意轉換和驗證過程發生在應用請求值、處理驗證 和呈現響應 階段即可。
我們將在稍後介紹爲什麼轉換和驗證會在這些階段出現,但是首先讓我們澄清一個更基本的問題:轉換 是什麼?簡單地說,轉換是確保數據擁有正確的對象或者類型的過程。下面是兩個典型的轉換:
-
字符串值可以轉換爲 java.util.Date。
-
字符串值可以轉換爲 Float。
至於驗證,它用於確保數據包含所期望的內容。下面是兩個典型的驗證:
-
java.util.Date 的格式爲 MM/yyyy。
-
Float 在 1.0 和 100.0 之間。
關注生命週期階段
轉換和驗證的主要目的是確保在更新模型數據之前已經經過了正確的無害處理。之後,當需要調用應用程序方法用這些些數據實際做一些事情 時,就可以有把握地假定模型的某些狀態。轉換和驗證使您可以側重於業務邏輯,而不是側重於對輸入數據進行繁瑣的資格認定,比如 null 檢驗、長度限定、範圍邊界,等等。
因此,在更新模型數據 生命週期階段中,在組件數據被綁定到 backing bean 模型之前 進行轉換和驗證處理是有道理的。正如圖 1 所示,轉換髮生在應用請求值階段,而驗證發生在處理驗證階段。圖 2 突出顯示了這些階段。
關於 immediate 屬性
注意,圖 2 中描繪的轉換和驗證過程表示了將 UIInput 組件的 immediate 屬性設置爲 false 時的應用程序流程。如果這個屬性設置爲 true,那麼轉換和驗證會發生在生命週期更早的時期,即應用請求值階段(參見圖 3)。對使用 immediate 屬性的詳細討論超出了本文的範圍,但是在某些情況下,比如管理動態清單(可能您還記得,本系列的上一篇文章中曾介紹過),它很有用,它甚至可以繞過驗證(在與 UICommand 組件結合使用時)。能想像一個需要完全繞過驗證的應用程序嗎?
圖 3 展示了當 immediate 屬性設置爲 true 時,在 JSF 應用程序生命週期中的哪些地方進行轉換和驗證。
實際的例子
下面,我們將用一個示例應用程序展示所討論的概念。本月的示例應用程序將展示 JSF 的轉換和驗證能力。記住,這個示例應用程序非常簡單,沒有追求一些不必要的面面俱到:無論如何,我們的目的不是構建一個在真實世界中使用的應用程序!這個示例應用程序將展示以下幾點:
-
使用標準 JSF 轉換器轉換表單字段數據。
-
使用標準 JSF 驗證組件驗證表單字段數據。
-
如何編寫自定義轉換器和驗證器。
-
如何在 faces-config.xml 文件中註冊自定義轉換器和驗證器。
-
如何定製默認錯誤消息。
這個示例應用程序是一個簡單的用戶註冊表單。我們的目標是收集用戶數據,比如姓名、年齡、電子郵箱地址和電話號碼。然後,我們將展示如何利用 JSF 轉換和驗證確保收集的數據對於模型是適合的。
這個應用程序使用了三個 JSPi 頁:
-
index.jsp 將用戶定向到 UserRegistration.jsp。
-
UserRegistration.jsp 包含應用程序的表單字段。
-
results.jsp 通知應用程序用戶已經註冊。
我們將首先分析編寫 JSF 轉換過程的選擇。
JSF 轉換
如前所述,轉換是確保數據對象或者類型正確的一個過程,因此,我們將字符串值轉換爲其他類型,比如 Date 對象、基本浮點型或者 Float 對象。可以使用自帶的轉換器,也可以編寫自定義的轉換器。
JSF 提供了許多標準數據轉換器。也可以通過實現 Converter 接口插入自定義轉換器,但是這些將在後面進行介紹。下表顯示了 JSF 進行簡單數據轉換所使用的轉換器 id 及其對應的實現類。大多數數據轉換是自動發生的。
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 |
圖 4 展示了用戶年齡的默認轉換。JSF 標籤配置如下:
|
各種情況的轉換器
UserRegistration.user.age 表示一個值綁定屬性,它的類型爲 int。對於基本型或者 BigInteger/ BigDecimal 的綁定,JSF 選擇了標準轉換器。不過,還可以通過 <f:converter/> 標籤,利用一個特定的轉換器來增加粒度,如下所示。
|
在圖 5 中,可以看到 JSF 使用標準轉換器的場景。在這種情況下,雖然年齡實際上是一個有效的整數,但轉換仍然會失敗,因爲該值不是短整型的。
選擇日期格式樣式
儘管在默認情況下,JSF 可以很好地處理基本型及類似的類型,但是在處理日期數據時,必須指定轉換標籤 <f:convertDateTime/>。這個標籤基於 java.text 包,並使用短、長和自定義樣式。下面是一個例子:
|
這個例子展示瞭如何用 <f:convertDateTime/> 確保用戶的生日可以轉換爲格式爲 MM/yyyy(月/年)的日期對象。請參閱 JSF 的 java.text.SimpleDataFormat (在 參考資料 中),以獲取模式列表。
其他樣式
除了可以轉換日期和時間格式外,JSF 還提供了處理像百分數或者貨幣數據這類值的特殊轉換器。這個轉換器處理分組(如逗號)、小數、貨幣符號等。例如,以下 <f:convertNumber/> 的用法就是處理貨幣的一種技巧:
|
在圖 6 中,可以看到一些格式編排不正確的貨幣數據,以及所導致的轉換錯誤。
自定義轉換
如果需要將字段數據轉換爲特定於應用程序的值對象,則需要自定義數據轉換,如下面例子所示:
-
String 轉換爲 PhoneNumber 對象 (PhoneNumber.areaCode、PhoneNumber.prefix、 ...)。
-
String 轉換爲 Name 對象 (Name.first、Name.last)。
-
String 轉換爲 ProductCode 對象 (ProductCode.partNum、ProductCode.rev、 ...)。
要創建自定義轉換器,必須完成以步驟:
-
實現 Converter 接口(也就是 javax.faxes.convert.Converter)。
-
實現 getAsObject 方法,它將一個字段(字符串)轉換爲一個對象(例如,PhoneNumber)。
-
實現 getAsString 方法,它將一個對象(如 PhoneNumber)轉換爲一個字符串。
-
在 Faces 上下文中註冊自定義轉換器。
-
用 <f:converter/> 標籤在 JSP 中插入這個轉換器。
您可以自己看到如何在 JSF 應用程序生命週期中加入這些步驟。在圖 7 中,JSF 在應用請求值階段調用自定義轉換器的 getAsObject 方法。轉換器必須在這裏將請求字符串轉換爲所需的對象類型,然後返回這個對象,將它存儲在相應的 JSF 組件中。如果該值被返回呈現在視圖中,那麼 JSF 將在呈現響應階段調用 getAsString 方法。這意味着轉換器還要負責將對象數據轉換回字符串表示形式。
圖 7. 自定義轉換器 getAsObject 和 getAsString 方法
創建自定義轉換器
我們將使用一個案例分析來展示 Converter 接口、getAsObject 和 getAsString 方法的實現,同時還將展示如何在 Faces 上下文中註冊這個轉換器。
這個案例分析的目的是將一個單字段字符串值轉換爲一個 PhoneNumber 對象。我們將一步一步地完成這個轉換過程。
第 1 步:實現 Converter 接口
這一步實現 Converter 接口。
|
第 2 步:實現 getAsObject 方法
這一步將一個字段值轉換爲一個 PhoneNumber 對象。
|
第 3 步:實現 getAsString 方法
這一步將一個 PhoneNumber 對象轉換爲一個字符串。
|
第 4 步:在 faces 上下文中註冊自定義轉換器
第 4 步可以以兩種方式執行。第一種選擇使用(比如)arcmind.PhoneConverter 的 id 來註冊 PhoneConverter 類。JSP 頁中的 <f:converter/> 標籤會使用這個 id。下面是第 4 步的選項 1 的代碼:
|
另一種方法是註冊 PhoneConverter 類來自動處理所有 PhoneNumber 對象,如下所示。
|
第 5 步:在 JSP 中使用轉換器標籤?
自然,下一步的執行取決於所選的註冊方法。如果選擇使用 arcmind.PhoneConverter 的 id 來註冊 PhoneConverter 類,那麼就使用 <f:converter/> 標籤,如下所示。
|
如果選擇註冊 PhoneConverter 類來自動 處理所有 PhoneNumber,那麼就不需要在 JSP 頁中使用 <f:converter/> 標籤。下面是第 5 步的不帶轉換器標籤的代碼。
|
這樣,我們已經完成了這個示例應用程序的轉換處理代碼!到目前爲止完成的應用程序如下所示。
JSF 驗證
如前所述,JSF 驗證可以確保應用程序數據包含預期的內容,例如:
-
java.util.Date 爲 MM/yyyy 格式。
-
Float 在 1.0 和 100.0 之間。
在 JSF 中有 4 種驗證:
-
自帶驗證組件。
-
應用程序級驗證。
-
自定義驗證組件(它實現了 Validator 接口)。
-
在 backing bean 中的驗證方法(內聯)。
我們將在下面的討論中介紹並展示每一種形式。
JSF 驗證生命週期和組件
圖 9 顯示了用戶註冊表單中名字字段的生命週期案例分析。代碼引用被有意解釋爲僞代碼(pseudo-code)。
下面是 JSF 提供的一組標準驗證組件:
-
DoubleRangeValidator:組件的本地值必須爲數字類型,必須在由最小和/或最大值所指定的範圍內。
-
LongRangeValidator:組件的本地值必須爲數字類型,並且可以轉換爲長整型,必須在由最小和/或最大值所指定的範圍內。
-
LengthValidator:類型必須爲字符串,長度必須在由最小和/或最大值所指定的範圍內。
標準驗證
在我們的示例應用程序中,用戶的年齡可以是任意有效的整數(byte、short、int)。因爲將年齡設置爲(比如說)-2 是無意義的,所以可能要對這個字段添加一些驗證。下面是一些簡單的驗證代碼,用以確保年齡字段中的數據模型完整性:
|
完成年齡字段後,可能希望指定對名字字段的長度加以限制。可以像這樣編寫這個驗證:
|
圖 10 顯示了由上面標準驗證示例所生成的默認詳細驗證消息。
儘管 JSF 自帶的驗證在許多情況下都可以滿足,但是它有一些侷限性。在處理電子郵件驗證、電話號碼、URL、日期等數據時,有時編寫自己的驗證器會更好一些,不過我們將在稍後對此進行討論。
應用程序級驗證
在概念上,應用程序級驗證實際上是業務邏輯驗證。JSF 將表單和/或字段級驗證與業務邏輯驗證分離開。應用程序級驗證主要需要在 backing bean 中添加代碼,用這個模型確定綁定到模型中的數據是否合格。對於購物車,表單級驗證可以驗證輸入的數量是否有效,但是需要使用業務邏輯驗證檢查用戶是否超出了他或者她的信用額度。這是在 JSF 中分離關注點的另一個例子。
例如,假定用戶單擊了綁定到某個操作方法的按鈕,那麼就會在調用應用程序階段調用這個方法(有關的細節,請參見上面的圖 1)。假定在更新模型階段進行了更新,那麼在對模型數據執行任何操縱之前,可以添加一些驗證代碼,根據應用程序的業務規則檢查輸入的數據是否有效。
例如,在這個示例應用程序中,用戶單擊了 Register 按鈕,這個按鈕被綁定到應用程序控制器的 register() 方法。我們可以在 register() 方法中添加驗證代碼,以確定名字字段是否爲 null。如果該字段爲 null,那麼還可以在 FacesContext 中添加一條消息,指示相關組件返回到當前頁。
其實它現在並不是業務規則邏輯的一個好例子。更好的例子是檢查用戶是否超出了她或者她的信用額度。在該例中,不是檢查字段是否爲空,我們可以調用模型對象的方法來確保當前用戶已經不在系統中。
圖 11 描繪了這個過程。
注意在 register() 方法中,消息是如何以 ${formId}:${fieldId} 的形式添加到 FacesContext 中的。圖 12 顯示了消息與組件 id 之間的關係。
應用程序級驗證的優缺點
應用級驗證非常直觀並且容易實現。不過,這種形式的驗證是在其他形式的驗證(標準、自定義、組件)之後發生的。
應用程序級驗證的優點如下:
-
容易實現。
-
不需要單獨的類(自定義驗證器)。
-
不需要頁編寫者指定驗證器。
應用程序級驗證的缺點如下:
-
在其他形式的驗證(標準、自定義)之後發生。
-
驗證邏輯侷限於 backing bean 方法,使得重用性很有限。
-
在大型應用程序和/或團隊環境中可能難於管理。
最終,應用程序級驗證只應該用於那些需要業務邏輯驗證的環境中。
自定義驗證組件
對於標準 JSF 驗證器不支持的數據類型,則需要建立自己的自定義驗證組件,其中包括電子郵件地址和郵政編碼。如果需要明確控制顯示給最終用戶的消息,那麼還需要建立自己的驗證器。在 JSF 中,可以創建可在整個 Web 應用程序中重複使用的可插入驗證組件。
創建自定義驗證器的步驟如下,我們將一步步地分析:
-
創建一個實現了 Validator 接口的類 (javax.faces.validator.Validator)。
-
實現 validate 方法。
-
在 faces-confix.xml 文件中註冊自定義驗證。
-
在 JSP 頁中使用 <f:validator/> 標籤。
下面是創建自定義驗證器的分步示例代碼。
第 1:實現 Validator 接口
第一步是實現 Validator 接口。
|
第 2 步:實現驗證方法
接下來,需要實現 validate 方法。
throws ValidatorException { |
第 3 步:在 FacesContext 中註冊自定義驗證器
您現在應該熟悉在 FacesContext 中註冊自定義驗證器的代碼了。
|
第 4 步:在 JSP 中使用 <f:validator/> 標籤
<f:validator/> 標籤聲明使用 zipCodeValidator。<f:attribute/> 標籤將 plus4Optional 屬性設置爲 true。注意,它定義了 inputText 組件的屬性,而不是 驗證器的屬性!
|
爲了讀取 zipCodeinputText 組件的 plus4Optional 屬性,請完成以下步驟::
|
總體而言,創建自定義驗證器是相當直觀的,並且可以使該驗證在許多應用程序中重複使用。缺點是必須創建一個類,並在 faces 上下文中管理驗證器註冊。不過,通過創建一個使用這個驗證器的自定義標籤,使其看上去像是一個自帶的驗證,可以進一步實現自定義驗證器。對於常見的驗證問題,如電子郵件驗證,這種方法可以支持這樣一種設計理念,即代碼重用和一致的應用程序行爲是最重要的。
backing bean 中的驗證方法
作爲創建單獨的驗證器類的替代方法,可以只在 backing bean 的方法中實現自定義驗證,只要這個方法符合 Validator 接口的 validate 方法的參數簽名即可。例如,可以編寫以下方法:
|
之後,可通過如下所示的 validator 屬性在 JSF 中使用這個方法:
|
JSF 用 validateEmail 方法對綁定到 user.email 模型屬性的 inputText 組件值進行自定義驗證。如果電子郵件格式無效,那麼就在相關組件的 faces 上下文中添加消息。考慮到這種驗證方法實際上是 backing bean 的一部分,爲什麼通常必須用某個值與相關組件的關聯來評估該值,而不是直接檢查本地 bean 屬性呢?線索就在前面的生命週期圖中。如果現在不能馬上找到答案,也不要擔心,我們將在本文的最後對此加以說明。
默認驗證
注意上面 email 標籤的 required 屬性。利用 required 屬性是一種默認 驗證形式。如果這個屬性是 true,那麼相應的組件必須有一個值。一個重要的說明:如果 required 屬性爲 false,那麼就不用對這個標籤/組件指派驗證,這樣,JSF 將跳過對這個組件的驗證,並讓值和組件的狀態保持不變。
圖 13 概述了我們討論過的驗證形式。
自定義消息
您可能注意到了,JSF 提供的默認轉換和驗證消息非常長,這會讓那些總是輸入無效表單數據的最終用戶感到困惑和惱火。幸運的是,您可以通過創建自己的消息資源綁定來改變 JSF 提供的默認消息。jsf-impl.jar (或類似的文件中)中包含了一個 message.properties 文件,該文件包含圖 14 所示的默認消息。
通過創建自己的 message.properties 文件並斷開指定場所的 faces 上下文中綁定的消息資源,您可以更改默認消息,如圖 15 所示。
關於在 JSF 中創建自定義轉換和驗證消息的更多內容請參前閱 參考資料。
處理 JSF 生命週期
我們在本文前面留下了一些問題讓您考慮,現在可以解決它們了!我們提到的一件事是對 UICommand 按鈕使用 immediate 屬性,比如 commandLink 或者 commandButtons。現在請您考慮希望在什麼樣的場景中跳過驗證。
基本上只要用戶需要輸入數據,就需要對這個數據進行驗證。不過,如果整個數據項是可選的,那麼就不需要進行驗證。一種避免 JSF 生命週期的驗證階段的方法是利用 UICommand 組件的 immediate 屬性,該屬性可以在處理驗證階段之前 的應用請求值階段期間(而不是在處理驗證階段 之後 的調用應用程序階段)強制調用這個操作。
immediate 屬性允許您通過標準瀏覽規則控制頁流程,並繞過驗證。可以針對特定的場景實現這項技術,比如帶有可選步驟和/或表單的在線嚮導(如當用戶單擊 Skip 按鈕以進入下一視圖),或者在用戶因爲某種原因而取消某個表單的情況下。
我們在本文中留下的第二個問題是:既然驗證方法實際上是 backing bean 的一部分,那麼爲什麼通常必須利用組件關聯來判斷它的值。請參閱前面的 JSF 應用程序生命週期,看看您能否找到答案。
這裏的密訣是:儘管 validateEmail 嵌入驗證方法是實際的 backing bean 的一部分,但是該方法必須通過組件關聯來引用這,而不是直接訪問本地屬性來引用值。由於驗證發生在組件值綁定到模型之前(在更新模型值階段),所以模型處於未知狀態。因此,必須編寫嵌入自定義驗證邏輯,就像使用一個自定義 Validator 對象處理驗證一樣。這也解釋了維護相同方法簽名的需求。
這些尚待解決的枝節問題有什麼意義呢,當然,它們最終將我們帶回 JSF 應用程序生命週期。將這些問題彙總在一起,就能體現充分理解生命週期的重要性 —— 向後、向前或由內向外,這樣您就可以在需要的時候操縱它。
結束語
在本文中我們討論了相當多的 JSF 轉換和驗證的基本內容。事實上,我們討論了在自己的應用程序中使用這些過程需要知道的大部分內容(至少對這個版本的 JSF 而言)!