本月,Rizon Software 的 CTO Paul Tabor 應邀與我一道解除針對 JSF 的 FUD。在本文中,我們將介紹 JSF 轉換和驗證框架的概念,它比您所想的要容易使用得多,也靈活得多。
首先我們將介紹應用于 JSF 生命周期的轉換和驗證過(guò)程,然后展示一個(gè)簡(jiǎn)單的 JSF 應用程序中的默認轉換和驗證過(guò)程。接著(zhù)將展示如何創(chuàng )建和插入自定義的實(shí)現,以應對要求更高的場(chǎng)景。正如 Rick 在以前的文章中所說(shuō)的,我們會(huì )理論與實(shí)踐并重,先介紹概念,再用一個(gè)實(shí)際例子說(shuō)明這些概念的應用。示例應用程序將涵蓋大多數轉換和驗證用例,雖然只是初級的。
注意,示例應用程序的默認編譯環(huán)境是 Maven,不過(guò), 還提供了一個(gè) Ant 腳本??梢詥螕舯卷?yè)頂部或者底部的 Code 圖標下載示例源代碼。為了簡(jiǎn)便起見(jiàn),您會(huì )發(fā)現,該例子的設置與上一篇文章中的一樣。關(guān)于構建環(huán)境配置的更多說(shuō)明,包括在 Ant 環(huán)境中而不是在 Maven 環(huán)境中編譯和運行示例應用程序的說(shuō)明,請參閱參考資料。
轉換和驗證
雖然在 JSF Web 應用程序中使用轉換和驗證不一定要理解 JavaServer Faces 生命周期的基礎知識,但是在深入轉換和驗證內容之前,最好對一些基本知識做一回顧。此外,掌握一點(diǎn) JSF 生命周期技巧可以極大地幫助簡(jiǎn)化 Web 應用程序的開(kāi)發(fā)工作。還有助于更好地理解 JSF 的可插入能力。
圖 1 描繪了我們所說(shuō)的“基本 JSF 生命周期”。 基本 是在暗示這只是一個(gè)典型的處理所提交表單值的請求-響應(request-and-response)場(chǎng)景。
圖 1. 基本 JSF 生命周期
顯然,不同的場(chǎng)景對這里重點(diǎn)描述的生命周期有不同的影響。我們將在本文稍后介紹其中一些場(chǎng)景?,F在,只需要注意轉換和驗證過(guò)程發(fā)生在應用請求值、處理驗證 和呈現響應 階段即可。
我們將在稍后介紹為什么轉換和驗證會(huì )在這些階段出現,但是首先讓我們澄清一個(gè)更基本的問(wèn)題:轉換 是什么?簡(jiǎn)單地說(shuō),轉換是確保數據擁有正確的對象或者類(lèi)型的過(guò)程。下面是兩個(gè)典型的轉換:
字符串值可以轉換為 java.util.Date。 字符串值可以轉換為 Float。
至于驗證,它用于確保數據包含所期望的內容。下面是兩個(gè)典型的驗證:
java.util.Date 的格式為 MM/yyyy。 Float 在 1.0 和 100.0 之間。
關(guān)注生命周期階段
轉換和驗證的主要目的是確保在更新模型數據之前已經(jīng)經(jīng)過(guò)了正確的無(wú)害處理。之后,當需要調用應用程序方法用這些些數據實(shí)際做一些事情 時(shí),就可以有把握地假定模型的某些狀態(tài)。轉換和驗證使您可以側重于業(yè)務(wù)邏輯,而不是側重于對輸入數據進(jìn)行繁瑣的資格認定,比如 null 檢驗、長(cháng)度限定、范圍邊界,等等。
因此,在更新模型數據 生命周期階段中,在組件數據被綁定到 backing bean 模型之前 進(jìn)行轉換和驗證處理是有道理的。正如圖 1 所示,轉換發(fā)生在應用請求值階段,而驗證發(fā)生在處理驗證階段。圖 2 突出顯示了這些階段。
圖 2. 要關(guān)注的轉換和驗證階段
關(guān)于 immediate 屬性
注意,圖 2 中描繪的轉換和驗證過(guò)程表示了將 UIInput 組件的 immediate 屬性設置為 false 時(shí)的應用程序流程。如果這個(gè)屬性設置為 true,那么轉換和驗證會(huì )發(fā)生在生命周期更早的時(shí)期,即應用請求值階段(參見(jiàn)圖 3)。對使用 immediate 屬性的詳細討論超出了本文的范圍,但是在某些情況下,比如管理動(dòng)態(tài)清單(可能您還記得,本系列的上一篇文章中曾介紹過(guò)),它很有用,它甚至可以繞過(guò)驗證(在與 UICommand 組件結合使用時(shí))。能想像一個(gè)需要完全繞過(guò)驗證的應用程序嗎?
圖 3 展示了當 immediate 屬性設置為 true 時(shí),在 JSF 應用程序生命周期中的哪些地方進(jìn)行轉換和驗證。
圖 3. 將 immediate 屬性設置為 true
實(shí)際的例子
下面,我們將用一個(gè)示例應用程序展示所討論的概念。本月的示例應用程序將展示 JSF 的轉換和驗證能力。記住,這個(gè)示例應用程序非常簡(jiǎn)單,沒(méi)有追求一些不必要的面面俱到:無(wú)論如何,我們的目的不是構建一個(gè)在真實(shí)世界中使用的應用程序!這個(gè)示例應用程序將展示以下幾點(diǎn):
使用標準 JSF 轉換器轉換表單字段數據。 使用標準 JSF 驗證組件驗證表單字段數據。 如何編寫(xiě)自定義轉換器和驗證器。 如何在 faces-config.xml 文件中注冊自定義轉換器和驗證器。 如何定制默認錯誤消息。
這個(gè)示例應用程序是一個(gè)簡(jiǎn)單的用戶(hù)注冊表單。我們的目標是收集用戶(hù)數據,比如姓名、年齡、電子郵箱地址和電話(huà)號碼。然后,我們將展示如何利用 JSF 轉換和驗證確保收集的數據對于模型是適合的。
這個(gè)應用程序使用了三個(gè) JSP 頁(yè):
index.jsp 將用戶(hù)定向到 UserRegistration.jsp。 UserRegistration.jsp 包含應用程序的表單字段。 results.jsp 通知應用程序用戶(hù)已經(jīng)注冊。
我們將首先分析編寫(xiě) JSF 轉換過(guò)程的選擇。
JSF 轉換
如前所述,轉換是確保數據對象或者類(lèi)型正確的一個(gè)過(guò)程,因此,我們將字符串值轉換為其他類(lèi)型,比如 Date 對象、基本浮點(diǎn)型或者 Float 對象??梢允褂米詭У霓D換器,也可以編寫(xiě)自定義的轉換器。
JSF 提供了許多標準數據轉換器。也可以通過(guò)實(shí)現 Converter 接口插入自定義轉換器,但是這些將在后面進(jìn)行介紹。下表顯示了 JSF 進(jìn)行簡(jiǎn)單數據轉換所使用的轉換器 id 及其對應的實(shí)現類(lèi)。大多數數據轉換是自動(dòng)發(fā)生的。
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 展示了用戶(hù)年齡的默認轉換。JSF 標簽配置如下:
<!-- UserRegistration.jsp --> <h:inputText id="age" value="#{UserRegistration.user.age}"/>
圖 4. 用戶(hù)注冊:年齡的默認轉換
各種情況的轉換器
UserRegistration.user.age 表示一個(gè)值綁定屬性,它的類(lèi)型為 int。對于基本型或者 BigInteger/ BigDecimal 的綁定,JSF 選擇了標準轉換器。不過(guò),還可以通過(guò) <f:converter/> 標簽,利用一個(gè)特定的轉換器來(lái)增加粒度,如下所示。
<!-- UserRegistration.jsp --> <h:inputText id="age" value="#{UserRegistration.user.age}"> <f:converter id="javax.faces.Short"/> </h:inputText>
在圖 5 中,可以看到 JSF 使用標準轉換器的場(chǎng)景。在這種情況下,雖然年齡實(shí)際上是一個(gè)有效的整數,但轉換仍然會(huì )失敗,因為該值不是短整型的。
圖 5. 使用 f:converter 標簽
選擇日期格式樣式
盡管在默認情況下,JSF 可以很好地處理基本型及類(lèi)似的類(lèi)型,但是在處理日期數據時(shí),必須指定轉換標簽 <f:convertDateTime/>。這個(gè)標簽基于 java.text 包,并使用短、長(cháng)和自定義樣式。下面是一個(gè)例子:
<!-- UserRegistration.jsp --> <h:inputText id="birthDate" value="#{UserRegistration.user.birthDate}"> <f:convertDateTime pattern="MM/yyyy"/> </h:inputText>
這個(gè)例子展示了如何用 <f:convertDateTime/> 確保用戶(hù)的生日可以轉換為格式為 MM/yyyy(月/年)的日期對象。請參閱 JSF 的 java.text.SimpleDataFormat (在 參考資料 中),以獲取模式列表。
其他樣式
除了可以轉換日期和時(shí)間格式外,JSF 還提供了處理像百分數或者貨幣數據這類(lèi)值的特殊轉換器。這個(gè)轉換器處理分組(如逗號)、小數、貨幣符號等。例如,以下 <f:convertNumber/> 的用法就是處理貨幣的一種技巧:
<!-- UserRegistration.jsp --> <h:inputText id="salary" value="#{UserRegistration.user.salary}"> <f:convertNumber maxFractionDigits="2" groupingUsed="true" currencySymbol="$" maxIntegerDigits="7" type="currency"/> </h:inputText>
在圖 6 中,可以看到一些格式編排不正確的貨幣數據,以及所導致的轉換錯誤。
圖 6. 使用 f:convertNumber 標簽
自定義轉換
如果需要將字段數據轉換為特定于應用程序的值對象,則需要自定義數據轉換,如下面例子所示:
String 轉換為 PhoneNumber 對象 (PhoneNumber.areaCode、PhoneNumber.prefix、 ...)。 String 轉換為 Name 對象 (Name.first、Name.last)。 String 轉換為 ProductCode 對象 (ProductCode.partNum、ProductCode.rev、 ...)。
要創(chuàng )建自定義轉換器,必須完成以步驟:
實(shí)現 Converter 接口(也就是 javax.faxes.convert.Converter)。
實(shí)現 getAsObject 方法,它將一個(gè)字段(字符串)轉換為一個(gè)對象(例如,PhoneNumber)。
實(shí)現 getAsString 方法,它將一個(gè)對象(如 PhoneNumber)轉換為一個(gè)字符串。
在 Faces 上下文中注冊自定義轉換器。
用 <f:converter/> 標簽在 JSP 中插入這個(gè)轉換器。
您可以自己看到如何在 JSF 應用程序生命周期中加入這些步驟。在圖 7 中,JSF 在應用請求值階段調用自定義轉換器的 getAsObject 方法。轉換器必須在這里將請求字符串轉換為所需的對象類(lèi)型,然后返回這個(gè)對象,將它存儲在相應的 JSF 組件中。如果該值被返回呈現在視圖中,那么 JSF 將在呈現響應階段調用 getAsString 方法。這意味著(zhù)轉換器還要負責將對象數據轉換回字符串表示形式。
圖 7. 自定義轉換器 getAsObject 和 getAsString 方法
創(chuàng )建自定義轉換器
我們將使用一個(gè)案例分析來(lái)展示 Converter 接口、getAsObject 和 getAsString 方法的實(shí)現,同時(shí)還將展示如何在 Faces 上下文中注冊這個(gè)轉換器。
這個(gè)案例分析的目的是將一個(gè)單字段字符串值轉換為一個(gè) PhoneNumber 對象。我們將一步一步地完成這個(gè)轉換過(guò)程。
第 1 步:實(shí)現 Converter 接口
這一步實(shí)現 Converter 接口。
import javax.faces.convert.Converter; import org.apache.commons.lang.StringUtils; ... public class PhoneConverter implements Converter { ... }
第 2 步:實(shí)現 getAsObject 方法
這一步將一個(gè)字段值轉換為一個(gè) PhoneNumber 對象。
public class PhoneConverter implements Converter { ... public Object getAsObject(FacesContext context, UIComponent component, String value) { if (StringUtils.isEmpty(value)){ return null;} PhoneNumber phone = new PhoneNumber(); String [] phoneComps = StringUtils.split(value," ,()-"); String countryCode = phoneComps[0]; phone.setCountryCode(countryCode); if ("1".equals(countryCode)){ String areaCode = phoneComps[1]; String prefix = phoneComps[2]; String number = phoneComps[3]; phone.setAreaCode(areaCode); phone.setPrefix(prefix); phone.setNumber(number); }else { phone.setNumber(value); } return phone; } }
第 3 步:實(shí)現 getAsString 方法
這一步將一個(gè) PhoneNumber 對象轉換為一個(gè)字符串。
public class PhoneConverter implements Converter { ... public String getAsString(FacesContext context, UIComponent component, Object value) { return value.toString(); } } public class PhoneNumber implements Serializable { ... public String toString(){ if (countryCode.equals("1")){ return countryCode + " " + areaCode + " " + prefix + " " + number; }else{ return number; } } }
第 4 步:在 faces 上下文中注冊自定義轉換器
第 4 步可以以?xún)煞N方式執行。第一種選擇使用(比如)arcmind.PhoneConverter 的 id 來(lái)注冊 PhoneConverter 類(lèi)。JSP 頁(yè)中的 <f:converter/> 標簽會(huì )使用這個(gè) id。下面是第 4 步的選項 1 的代碼:
<converter> <converter-id>arcmind.PhoneConverter</converter-id> <converter-class>com.arcmind.converters.PhoneConverter</converter-class> </converter>
另一種方法是注冊 PhoneConverter 類(lèi)來(lái)自動(dòng)處理所有 PhoneNumber 對象,如下所示。
<converter> <converter-for-class>com.arcmind.value.PhoneNumber</converter-for-class> <converter-class>com.arcmind.converters.PhoneConverter</converter-class> </converter>
第 5 步:在 JSP 中使用轉換器標簽?
自然,下一步的執行取決于所選的注冊方法。如果選擇使用 arcmind.PhoneConverter 的 id 來(lái)注冊 PhoneConverter 類(lèi),那么就使用 <f:converter/> 標簽,如下所示。
<h:inputText id="phone" value="#{UserRegistration.user.phone}"> <f:converter converterId="arcmind.PhoneConverter" /> </h:inputText>
如果選擇注冊 PhoneConverter 類(lèi)來(lái)自動(dòng) 處理所有 PhoneNumber,那么就不需要在 JSP 頁(yè)中使用 <f:converter/> 標簽。下面是第 5 步的不帶轉換器標簽的代碼。
<h:inputText id="phone" value="#{UserRegistration.user.phone}"> [Look mom no converter!] </h:inputText>
這樣,我們已經(jīng)完成了這個(gè)示例應用程序的轉換處理代碼!到目前為止完成的應用程序如下所示。
圖 8. 帶有轉換處理的示例應用程序
JSF 驗證
如前所述,JSF 驗證可以確保應用程序數據包含預期的內容,例如:
java.util.Date 為 MM/yyyy 格式。 Float 在 1.0 和 100.0 之間。
在 JSF 中有 4 種驗證:
自帶驗證組件。 應用程序級驗證。 自定義驗證組件(它實(shí)現了 Validator 接口)。 在 backing bean 中的驗證方法(內聯(lián))。
我們將在下面的討論中介紹并展示每一種形式。
JSF 驗證生命周期和組件
圖 9 顯示了用戶(hù)注冊表單中名字字段的生命周期案例分析。代碼引用被有意解釋為偽代碼(pseudo-code)。
圖 9. JSF 生命周期中的驗證
下面是 JSF 提供的一組標準驗證組件:
DoubleRangeValidator:組件的本地值必須為數字類(lèi)型,必須在由最小和/或最大值所指定的范圍內。
LongRangeValidator:組件的本地值必須為數字類(lèi)型,并且可以轉換為長(cháng)整型,必須在由最小和/或最大值所指定的范圍內。
LengthValidator:類(lèi)型必須為字符串,長(cháng)度必須在由最小和/或最大值所指定的范圍內。
標準驗證
在我們的示例應用程序中,用戶(hù)的年齡可以是任意有效的整數(byte、short、int)。因為將年齡設置為(比如說(shuō))-2 是無(wú)意義的,所以可能要對這個(gè)字段添加一些驗證。下面是一些簡(jiǎn)單的驗證代碼,用以確保年齡字段中的數據模型完整性:
<h:inputText id="age" value="#{UserRegistration.user.age}"> <f:validateLongRange maximum="150" minimum="0"/> </h:inputText>
完成年齡字段后,可能希望指定對名字字段的長(cháng)度加以限制??梢韵襁@樣編寫(xiě)這個(gè)驗證:
<h:inputText id="firstName" value="#{UserRegistration.user.firstName}"> <f:validateLength minimum="2" maximum="25" /> </h:inputText>
圖 10 顯示了由上面標準驗證示例所生成的默認詳細驗證消息。
圖 10. 標準驗證錯誤消息
盡管 JSF 自帶的驗證在許多情況下都可以滿(mǎn)足,但是它有一些局限性。在處理電子郵件驗證、電話(huà)號碼、URL、日期等數據時(shí),有時(shí)編寫(xiě)自己的驗證器會(huì )更好一些,不過(guò)我們將在稍后對此進(jìn)行討論。
應用程序級驗證
在概念上,應用程序級驗證實(shí)際上是業(yè)務(wù)邏輯驗證。JSF 將表單和/或字段級驗證與業(yè)務(wù)邏輯驗證分離開(kāi)。應用程序級驗證主要需要在 backing bean 中添加代碼,用這個(gè)模型確定綁定到模型中的數據是否合格。對于購物車(chē),表單級驗證可以驗證輸入的數量是否有效,但是需要使用業(yè)務(wù)邏輯驗證檢查用戶(hù)是否超出了他或者她的信用額度。這是在 JSF 中分離關(guān)注點(diǎn)的另一個(gè)例子。
例如,假定用戶(hù)單擊了綁定到某個(gè)操作方法的按鈕,那么就會(huì )在調用應用程序階段調用這個(gè)方法(有關(guān)的細節,請參見(jiàn)上面的圖 1)。假定在更新模型階段進(jìn)行了更新,那么在對模型數據執行任何操縱之前,可以添加一些驗證代碼,根據應用程序的業(yè)務(wù)規則檢查輸入的數據是否有效。
例如,在這個(gè)示例應用程序中,用戶(hù)單擊了 Register 按鈕,這個(gè)按鈕被綁定到應用程序控制器的 register() 方法。我們可以在 register() 方法中添加驗證代碼,以確定名字字段是否為 null。如果該字段為 null,那么還可以在 FacesContext 中添加一條消息,指示相關(guān)組件返回到當前頁(yè)。
其實(shí)它現在并不是業(yè)務(wù)規則邏輯的一個(gè)好例子。更好的例子是檢查用戶(hù)是否超出了她或者她的信用額度。在該例中,不是檢查字段是否為空,我們可以調用模型對象的方法來(lái)確保當前用戶(hù)已經(jīng)不在系統中。
圖 11 描繪了這個(gè)過(guò)程。
圖 11. 應用程序級驗證
注意在 register() 方法中,消息是如何以 ${formId}:${fieldId} 的形式添加到 FacesContext 中的。圖 12 顯示了消息與組件 id 之間的關(guān)系。
圖 12. 驗證消息
應用程序級驗證的優(yōu)缺點(diǎn)
應用級驗證非常直觀(guān)并且容易實(shí)現。不過(guò),這種形式的驗證是在其他形式的驗證(標準、自定義、組件)之后發(fā)生的。
應用程序級驗證的優(yōu)點(diǎn)如下:
容易實(shí)現。 不需要單獨的類(lèi)(自定義驗證器)。 不需要頁(yè)編寫(xiě)者指定驗證器。
應用程序級驗證的缺點(diǎn)如下:
在其他形式的驗證(標準、自定義)之后發(fā)生。 驗證邏輯局限于 backing bean 方法,使得重用性很有限。 在大型應用程序和/或團隊環(huán)境中可能難于管理。
最終,應用程序級驗證只應該用于那些需要業(yè)務(wù)邏輯驗證的環(huán)境中。
自定義驗證組件
對于標準 JSF 驗證器不支持的數據類(lèi)型,則需要建立自己的自定義驗證組件,其中包括電子郵件地址和郵政編碼。如果需要明確控制顯示給最終用戶(hù)的消息,那么還需要建立自己的驗證器。在 JSF 中,可以創(chuàng )建可在整個(gè) Web 應用程序中重復使用的可插入驗證組件。
MyFaces 是一個(gè) JSF 的開(kāi)放源代碼實(shí)現,它提供了許多額外的驗證器,其中包括一些 JSF 中所沒(méi)有的驗證器。要了解 MyFaces 的內容,請參閱參考資料。
創(chuàng )建自定義驗證器的步驟如下,我們將一步步地分析:
創(chuàng )建一個(gè)實(shí)現了 Validator 接口的類(lèi) (javax.faces.validator.Validator)。
實(shí)現 validate 方法。
在 faces-confix.xml 文件中注冊自定義驗證。
在 JSP 頁(yè)中使用 <f:validator/> 標簽。
下面是創(chuàng )建自定義驗證器的分步示例代碼。
第 1:實(shí)現 Validator 接口
第一步是實(shí)現 Validator 接口。
import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; ... public class ZipCodeValidator implements Validator{ private boolean plus4Required; private boolean plus4Optional; /** Accepts zip codes like 85710 */ private static final String ZIP_REGEX = "[0-9]{5}"; /** Accepts zip code plus 4 extensions like "-1119" or " 1119" */ private static final String PLUS4_REQUIRED_REGEX = "[ |-]{1}[0-9]{4}"; /** Optionally accepts a plus 4 */ private static final String PLUS4_OPTIONAL_REGEX = "([ |-]{1}[0-9]{4})?"; ... }
第 2 步:實(shí)現驗證方法
接下來(lái),需要實(shí)現 validate 方法。
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { /* Create the correct mask */ Pattern mask = null; /* more on this method later */ initProps(component); if (plus4Required){ mask = Pattern.compile(ZIP_REGEX + PLUS4_REQUIRED_REGEX); } else if (plus4Optional){ mask = Pattern.compile(ZIP_REGEX + PLUS4_OPTIONAL_REGEX); } else if (plus4Required && plus4Optional){ throw new IllegalStateException("Plus 4 is either optional or required"); } else { mask = Pattern.compile(ZIP_REGEX); } /* 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 中注冊自定義驗證器
您現在應該熟悉在 FacesContext 中注冊自定義驗證器的代碼了。
<validator> <validator-id>arcmind.zipCodeValidator</validator-id> <validator-class>com.arcmind.jsfquickstart.validation.ZipCodeValidator</validator-class> </validator>
第 4 步:在 JSP 中使用 <f:validator/> 標簽
<f:validator/> 標簽聲明使用 zipCodeValidator。<f:attribute/> 標簽將 plus4Optional 屬性設置為 true。注意,它定義了 inputText 組件的屬性,而不是 驗證器的屬性!
<h:inputText id="zipCode" value="#{UserRegistration.user.zipCode}"> <f:validator validatorId="armind.zipCodeValidator"/> <f:attribute name="plus4Optional" value="true"/> </h:inputText>
為了讀取 zipCodeinputText 組件的 plus4Optional 屬性,請完成以下步驟::
private void initProps(UIComponent component) { Boolean optional = Boolean.valueOf((String) component.getAttributes(). get("plus4Optional")); Boolean required = Boolean.valueOf((String) component.getAttributes(). get("plus4Required")); plus4Optional = optional==null ? plus4Optional : optional.booleanValue(); plus4Required = required==null ? plus4Optional : required.booleanValue(); }
總體而言,創(chuàng )建自定義驗證器是相當直觀(guān)的,并且可以使該驗證在許多應用程序中重復使用。缺點(diǎn)是必須創(chuàng )建一個(gè)類(lèi),并在 faces 上下文中管理驗證器注冊。不過(guò),通過(guò)創(chuàng )建一個(gè)使用這個(gè)驗證器的自定義標簽,使其看上去像是一個(gè)自帶的驗證,可以進(jìn)一步實(shí)現自定義驗證器。對于常見(jiàn)的驗證問(wèn)題,如電子郵件驗證,這種方法可以支持這樣一種設計理念,即代碼重用和一致的應用程序行為是最重要的。
backing bean 中的驗證方法
作為創(chuàng )建單獨的驗證器類(lèi)的替代方法,可以只在 backing bean 的方法中實(shí)現自定義驗證,只要這個(gè)方法符合 Validator 接口的 validate 方法的參數簽名即可。例如,可以編寫(xiě)以下方法:
[SomeBackingBean.java] public void validateEmail(FacesContext context, UIComponent toValidate, Object value) { String email = (String) value; if (email.indexOf(@) == -1) { ((UIInput)toValidate).setValid(false); FacesMessage message = new FacesMessage("Invalid Email"); context.addMessage(toValidate.getClientId(context), message); } }
之后,可通過(guò)如下所示的 validator 屬性在 JSF 中使用這個(gè)方法:
<h:inputText id="email" value="#{UserRegistration.user.email}" validator="#{UserRegistration.validateEmail}" required="true"> </h:inputText>
JSF 用 validateEmail 方法對綁定到 user.email 模型屬性的 inputText 組件值進(jìn)行自定義驗證。如果電子郵件格式無(wú)效,那么就在相關(guān)組件的 faces 上下文中添加消息??紤]到這種驗證方法實(shí)際上是 backing bean 的一部分,為什么通常必須用某個(gè)值與相關(guān)組件的關(guān)聯(lián)來(lái)評估該值,而不是直接檢查本地 bean 屬性呢?線(xiàn)索就在前面的生命周期圖中。如果現在不能馬上找到答案,也不要擔心,我們將在本文的最后對此加以說(shuō)明。
默認驗證
注意上面 email 標簽的 required 屬性。利用 required 屬性是一種默認 驗證形式。如果這個(gè)屬性是 true,那么相應的組件必須有一個(gè)值。一個(gè)重要的說(shuō)明:如果 required 屬性為 false,那么就不用對這個(gè)標簽/組件指派驗證,這樣,JSF 將跳過(guò)對這個(gè)組件的驗證,并讓值和組件的狀態(tài)保持不變。
圖 13 概述了我們討論過(guò)的驗證形式。
圖 13. 驗證視圖
自定義消息
您可能注意到了,JSF 提供的默認轉換和驗證消息非常長(cháng),這會(huì )讓那些總是輸入無(wú)效表單數據的最終用戶(hù)感到困惑和惱火。幸運的是,您可以通過(guò)創(chuàng )建自己的消息資源綁定來(lái)改變 JSF 提供的默認消息。jsf-impl.jar (或類(lèi)似的文件中)中包含了一個(gè) message.properties 文件,該文件包含圖 14 所示的默認消息。
圖 14. 默認 JSF 轉換和驗證消息
通過(guò)創(chuàng )建自己的 message.properties 文件并斷開(kāi)指定場(chǎng)所的 faces 上下文中綁定的消息資源,您可以更改默認消息,如圖 15 所示。
圖 15. 取消消息資源綁定
關(guān)于在 JSF 中創(chuàng )建自定義轉換和驗證消息的更多內容請參前閱 參考資料。
處理 JSF 生命周期
我們在本文前面留下了一些問(wèn)題讓您考慮,現在可以解決它們了!我們提到的一件事是對 UICommand 按鈕使用 immediate 屬性,比如 commandLink 或者 commandButtons?,F在請您考慮希望在什么樣的場(chǎng)景中跳過(guò)驗證。
基本上只要用戶(hù)需要輸入數據,就需要對這個(gè)數據進(jìn)行驗證。不過(guò),如果整個(gè)數據項是可選的,那么就不需要進(jìn)行驗證。一種避免 JSF 生命周期的驗證階段的方法是利用 UICommand 組件的 immediate 屬性,該屬性可以在處理驗證階段之前 的應用請求值階段期間(而不是在處理驗證階段 之后 的調用應用程序階段)強制調用這個(gè)操作。
immediate 屬性允許您通過(guò)標準瀏覽規則控制頁(yè)流程,并繞過(guò)驗證??梢葬槍μ囟ǖ膱?chǎng)景實(shí)現這項技術(shù),比如帶有可選步驟和/或表單的在線(xiàn)向導(如當用戶(hù)單擊 Skip 按鈕以進(jìn)入下一視圖),或者在用戶(hù)因為某種原因而取消某個(gè)表單的情況下。
我們在本文中留下的第二個(gè)問(wèn)題是:既然驗證方法實(shí)際上是 backing bean 的一部分,那么為什么通常必須利用組件關(guān)聯(lián)來(lái)判斷它的值。請參閱前面的 JSF 應用程序生命周期,看看您能否找到答案。
這里的密訣是:盡管 validateEmail 嵌入驗證方法是實(shí)際的 backing bean 的一部分,但是該方法必須通過(guò)組件關(guān)聯(lián)來(lái)引用這,而不是直接訪(fǎng)問(wèn)本地屬性來(lái)引用值。由于驗證發(fā)生在組件值綁定到模型之前(在更新模型值階段),所以模型處于未知狀態(tài)。因此,必須編寫(xiě)嵌入自定義驗證邏輯,就像使用一個(gè)自定義 Validator 對象處理驗證一樣。這也解釋了維護相同方法簽名的需求。
這些尚待解決的枝節問(wèn)題有什么意義呢,當然,它們最終將我們帶回 JSF 應用程序生命周期。將這些問(wèn)題匯總在一起,就能體現充分理解生命周期的重要性 —— 向后、向前或由內向外,這樣您就可以在需要的時(shí)候操縱它。
結束語(yǔ)
在本文中我們討論了相當多的 JSF 轉換和驗證的基本內容。事實(shí)上,我們討論了在自己的應用程序中使用這些過(guò)程需要知道的大部分內容(至少對這個(gè)版本的 JSF 而言)!
當然,我們不可能討論到 所有內容。例如,您可能想要了解 MyFaces (請參閱 參考資料)中 JSF 沒(méi)有提供、或者這里沒(méi)有討論到的驗證器組件。此外,雖然我們討論了大多數常用的轉換和驗證技術(shù),但還有一些沒(méi)有包含在內。例如,在編寫(xiě)自定義組件時(shí),可以在組件的解碼/編碼過(guò)程中直接處理轉換和/或驗證(取決于組件的類(lèi)型及其功能),但是我們只能將對自定義組件開(kāi)發(fā)的更深入討論留到以后進(jìn)行了。
其他要牢記的是轉換和驗證不一定會(huì )很好地協(xié)同工作。轉換將字符串轉換為對象,而大多數標準驗證是對字符串進(jìn)行的。因此,在同時(shí)使用自定義轉換和驗證必須格外小心。例如,PhoneNumber 對象不能與長(cháng)度驗證器一起使用。在這種情況下,要么編寫(xiě)自定義驗證器,要么在自定義轉換器中添加一個(gè)特別的驗證邏輯。我們偏向后一種方法,因為它讓我們可以將自定義轉換器(自帶驗證邏輯)與特定的對象類(lèi)型相關(guān)聯(lián),并讓 JSF 處理這種對象類(lèi)型。JSF 自動(dòng)為我們做這項工作,不需要在 JSP 中包含任何特定的轉換器 id。(當然,有人會(huì )稱(chēng)它為懶惰編程,它也不是對所有用例都適用的最佳解決方案。)
我們認為本月文章中的討論再次聲明了以下這點(diǎn),即 JSF 提供了一種靈活的、強大的可插入式 Web 應用程序開(kāi)發(fā)框架。除了標準轉換器和驗證器之外,JSF 還可以促進(jìn)同時(shí)滿(mǎn)足應用程序和框架開(kāi)發(fā)人員的要求的自定義實(shí)現。最終,要由您來(lái)確定選擇何種轉換和驗證策略。JSF 使您能夠在原型制造階段很快、很容易地上手(標準轉換器、驗證器、內部驗證等),并在以后的開(kāi)發(fā)階段移植到更復雜的生產(chǎn)解決方案中(自定義對象、自定義消息等)。JSF 生命周期在所有階段都提供了可靠的基礎設施,始終如一地保證數據模型的完整性。
在下個(gè)月中,我們將深入分析如何用 JSF 編寫(xiě)自已的自定義組件,并結束這一系列。
參考資料 加入本文的 論壇. (您也可以通過(guò)點(diǎn)擊文章頂部或者底部的論壇 鏈接參加討論.)
您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文。
單擊本頁(yè)頂部或者底部的 Code 圖標,以下載本文的源代碼。
不要相信那些針對 JSF 的 FUD —— 也不要錯過(guò) 懷疑論者的 JSF 系列中的任何一篇文章。
請參閱 JSF 主頁(yè),下載 JavaServer Faces API、自定義標簽庫和相關(guān)文檔。
可以從 Apache Maven Project 頁(yè)面 下載 Maven。
關(guān)于 Ant 和 Maven 的詳細安裝和編譯指令,請參閱本系列的 JSF 參考資料。
請參閱 MyFaces 主頁(yè)上介紹的、JSF 目前尚未提供的驗證器組件。
Jackwind Li Guojie 撰寫(xiě)的“用 JavaServer Faces 進(jìn)行 UI 開(kāi)發(fā)”(developerWorks,2003 年 9 月)最先介紹了這項技術(shù)。
Roland Barcia 撰寫(xiě)的由 5 部分組成的“Developing JSF Applications using WebSphere Studio V5.1.1" 教程(developerWorks, 2004 年 1 月),是對使用 JSF 進(jìn)行編程的一篇實(shí)用性介紹文章。
在前沿文章 “ 集成 Struts、Tiles 和 JavaServer Faces”(developerWorks,2003 年 9 月)中,Srikanth Shenoy 和 Nithin Mallya 向您介紹了如何集成 Struts、Tiles 和 JavaServer Faces 的特性。
David Geary 撰寫(xiě)的 Core JavaServer Faces 一書(shū)(Prentice Hall, 2004 年 6 月),是對 JavaServer Faces 技術(shù)進(jìn)行的最好的書(shū)籍形式的介紹,其中包括關(guān)于創(chuàng )建自定義消息的討論。
您可能還想查看 Sun Microsystems 的 詳細 JSF 教程。
在 developerWorks 的 Java 技術(shù)專(zhuān)區 中,可以找到關(guān)于 Java 編程各個(gè)方面的文章。
請參閱 Developer Bookstore,以獲得技術(shù)書(shū)籍的完整清單,其中包括數百本 Java 相關(guān)主題的書(shū)籍。
還請參閱 Java 技術(shù)專(zhuān)區教程頁(yè),以獲得 developerWorks 的免費 Java 教程的完整清單。