注釋簡化了數(shù)據(jù)驗(yàn)證
|
|
級別: 中級
Ted Bergeron
(ted@triview.com), 合作創(chuàng)始人, Triview, Inc.
2006 年 10 月 10 日
盡管在 Web 應(yīng)用程序中盡可能多的層次中構(gòu)建數(shù)據(jù)驗(yàn)證非常重要,但是這樣做卻非常耗時(shí),以至于很多開發(fā)人員都會干脆忽略這個步驟 —— 這可能會導(dǎo)致今后大量問題的產(chǎn)生。但是隨著最新版本的 Java 平臺中引入了注釋,驗(yàn)證變得簡單得多了。在本文中,Ted Bergeron 將向您介紹如何使用 Hibernate Annotations 的 Validator 組件在 Web 應(yīng)用程序中輕松構(gòu)建并維護(hù)驗(yàn)證邏輯。
有時(shí)會有一種工具,它可以真正滿足開發(fā)人員和架構(gòu)師的需求。開發(fā)人員在第一次下載這種工具當(dāng)天就可以在自己的應(yīng)用程序中開始使用這種工具。理論上來說,這種工具在開發(fā)人員花費(fèi)大量時(shí)間來掌握其用法之前就可以從中獲益。架構(gòu)師也很喜歡這種工具,因?yàn)樗梢詫㈤_發(fā)人員導(dǎo)向更高理論層次的實(shí)現(xiàn)。Hibernate Annotations 的 Validator 組件就是一種這樣的工具。
|
開始之前需要了解的內(nèi)容
在閱讀本文之前,應(yīng)該對 Java 平臺版本 5(尤其是注釋)、JSP 2.0(因?yàn)楸疚闹袆?chuàng)建了一些標(biāo)簽文件,并在 TLD 中定義了一些函數(shù),它們都是 JSP 2.0 的新特性)和 Hibernate 及 Spring 框架有一個基本的了解。請注意即使不使用 Hibernate 來實(shí)現(xiàn)持久性,也可以在自己的應(yīng)用程序中使用 Hibernate Validator。
|
|
Java SE 5 為 Java 語言提供了很多需要的增強(qiáng)功能,不過其他增強(qiáng)功能可能都不如 注釋 這樣潛力巨大。使用 注釋,我們就終于具有了一個標(biāo)準(zhǔn)、一級的元數(shù)據(jù)框架為 Java 類使用。Hibernate 用戶手工編寫 *.hbm.xml 文件已經(jīng)很多年了(或者使用 XDoclet 來自動實(shí)現(xiàn)這個任務(wù))。如果手工創(chuàng)建了 XML 文件,那就必須對每個所需要的持久屬性都更新這兩個文件(類定義和 XML 映射文檔)。使用 HibernateDoclet 可以簡化這個過程(請參看清單 1 給出的例子),但是這需要我們確認(rèn)自己的 HibernateDoclet 版本支持要使用的 Hibernate 的版本。doclet 信息在運(yùn)行時(shí)也是不可用的,因?yàn)樗痪帉懙搅?Javadoc 風(fēng)格的注釋中了。Hibernate Annotations,如圖 2 所示,通過提供一個標(biāo)準(zhǔn)、簡明的映射類的方法和所添加的運(yùn)行時(shí)可用性來對這些方式進(jìn)行改進(jìn)。
清單 1. 使用 HibernateDoclet 的 Hibernate 映射代碼
/**
* @hibernate.property column="NAME" length="60" not-null="true"
*/
public String getName() {
return this.name;
}
/**
* @hibernate.many-to-one column="AGENT_ID" not-null="true" cascade="none"
* outer-join="false" lazy="true"
*/
public Agent getAgent() {
return agent;
}
/**
* @hibernate.set lazy="true" inverse="true" cascade="all" table="DEPARTMENT"
* @hibernate.collection-one-to-many class="com.triview.model.Department"
* @hibernate.collection-key column="DEPARTMENT_ID" not-null="true"
*/
public List<Department> getDepartment() {
return department;
}
|
清單 2. 使用 Hibernate Annotations 的 Hibernate 映射代碼
@NotNull
@Column(name = "name")
@Length(min = 1, max = NAME_LENGTH) // NAME_LENGTH is a constant declared elsewhere
public String getName() {
return name;
}
@NotNull
@ManyToOne(cascade = {CascadeType.MERGE }, fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id")
public Agent getAgent() {
return agent;
}
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
public List<Department> getDepartment() {
return department;
}
|
如果使用 HibernateDoclet,那么直到生成 XML 文件或運(yùn)行時(shí)才能捕獲錯誤。使用 注釋,在編譯時(shí)就可以檢測出很多錯誤;或者如果在編輯時(shí)使用了很好的 IDE,那么在編輯時(shí)就可以檢測出部分錯誤。在從頭創(chuàng)建應(yīng)用程序時(shí),可以利用 hbm2ddl 工具為自己的數(shù)據(jù)庫從 hbm.xml 文件中生成 DDL。一些重要的信息 —— 比如name
屬性的最大長度必須是 60 個字符,或者 DDL 應(yīng)該添加非空約束 —— 都被從 HibernateDoclet 項(xiàng)添加到 DDL 中。當(dāng)使用注釋時(shí),我們可以以類似的方式自動生成 DDL。
盡管這兩種代碼映射方式都可以使用,不過注釋的優(yōu)勢更為明顯。使用注釋,可以用一些常量來指定長度或其他值。編譯循環(huán)的速度更快,并且不需要生成 XML 文件。其中最大的優(yōu)勢是可以訪問一些有用信息,例如運(yùn)行時(shí)的非空注釋或長度。除了清單 2 給出的注釋之外,還可以指定一些驗(yàn)證的約束。所包含的部分約束如下:
-
@Max(value = 100)
-
@Min(value = 0)
-
@Past
-
@Future
-
@Email
在適當(dāng)條件下,這些注釋會引起由 DDL 生成檢查約束。(顯然,@Future
并不是一個適當(dāng)?shù)臈l件。)還可以根據(jù)需要創(chuàng)建定制約束注釋。
驗(yàn)證和應(yīng)用程序?qū)?/span>
編寫驗(yàn)證代碼是一個煩人且耗時(shí)的過程。通常,很多開發(fā)人員都會放棄在特定的層進(jìn)行有效性驗(yàn)證,從而可以節(jié)省一些時(shí)間;但是所節(jié)省的時(shí)間是否能夠彌補(bǔ)在這個地方因忽略部分功能所引起的缺陷卻非常值得探討。如果在所有應(yīng)用程序?qū)又袆?chuàng)建并維護(hù)驗(yàn)證所需要的時(shí)間可以極大地減少,那么爭論的焦點(diǎn)就會轉(zhuǎn)向是否要在多個層次中進(jìn)行有效性驗(yàn)證。假設(shè)有一個應(yīng)用程序,它讓用戶使用一個用戶名、密碼和信用卡號來創(chuàng)建一個帳號。在這個應(yīng)用程序中所希望進(jìn)行驗(yàn)證的組件如下:
-
視圖: 通過 JavaScript 進(jìn)行驗(yàn)證可以避免與服務(wù)器反復(fù)進(jìn)行交互,這樣可以提供更好的用戶體驗(yàn)。用戶可以禁用 JavaScript,因此這個層次的驗(yàn)證最好要有,但是卻并不可靠。對所需要的域進(jìn)行簡單的驗(yàn)證是必須的。
-
控制器: 驗(yàn)證必須在服務(wù)器端的邏輯中進(jìn)行處理。這個層次中的代碼可以以適合某個特定用途的方式處理驗(yàn)證。例如,在添加新用戶時(shí),控制器可以在進(jìn)行處理之前檢查指定的用戶名是否已經(jīng)存在。
-
服務(wù): 相對復(fù)雜的業(yè)務(wù)邏輯驗(yàn)證通常都最適合放到服務(wù)層中。例如,一旦有一個信用卡對象看起來有效,就應(yīng)該使用信用卡處理服務(wù)對這個信用卡的信息進(jìn)行確認(rèn)。
-
DAO: 在數(shù)據(jù)到達(dá)這個層次時(shí),應(yīng)該已經(jīng)是有效的了。盡管如此,執(zhí)行一次快速檢查從而確保所需要的域都非空并且值也都在特定的范圍或遵循特定的格式(例如 e-mail 地址域就應(yīng)該包含一個有效的 e-mail 地址)也是非常有益的。在此處捕獲錯誤總比產(chǎn)生可以避免的
SQLException
錯誤要好。
-
DBMS: 這是通常可以忽略驗(yàn)證的地方。即使當(dāng)前正在構(gòu)建的應(yīng)用程序是數(shù)據(jù)庫的惟一客戶機(jī),將來還可能會添加其他客戶機(jī)。如果應(yīng)用程序有一些 bug(大部分應(yīng)用程序都可能會有 bug),那么無效的數(shù)據(jù)也可能會被發(fā)送給數(shù)據(jù)庫。在這種情況中,如果走運(yùn),就可以找到無效的數(shù)據(jù),并且需要分析這些數(shù)據(jù)是否可以清除,以及如何清除。
-
模型: 這是進(jìn)行驗(yàn)證的一個理想地方,它不需要訪問外部服務(wù),也不需要了解持久性數(shù)據(jù)。例如,某業(yè)務(wù)邏輯可能會要求用戶至少提供一個聯(lián)系信息,這可以是一個電話號碼也可以是一個 e-mail 地址;可以使用模型層的驗(yàn)證來確保用戶的確提供了這種信息。
進(jìn)行驗(yàn)證的一種典型方法是對簡單的驗(yàn)證使用 Commons Validator,并在控制器中編寫其他一些驗(yàn)證邏輯。Commons Validator 可以生成 JavaScript 來對視圖中的驗(yàn)證進(jìn)行處理。但是 Commons Validator 也有自己的缺陷:它只能處理簡單的驗(yàn)證問題,并且將驗(yàn)證的信息都保存到了 XML 文件中。Commons Validator 被設(shè)計(jì)用來與 Struts 一起使用,而且沒有提供一種簡單的方法在應(yīng)用程序?qū)娱g重用驗(yàn)證的聲明。
在規(guī)劃有效性驗(yàn)證策略時(shí),選擇在錯誤發(fā)生時(shí)簡單地處理這些錯誤是遠(yuǎn)遠(yuǎn)不夠的。一種良好的設(shè)計(jì)同時(shí)還要通過生成一個友好的用戶界面來防止出現(xiàn)錯誤。采用預(yù)先進(jìn)行的方法進(jìn)行驗(yàn)證可以極大地增強(qiáng)用戶對于應(yīng)用程序的理解。不幸的是,Commons Validator 并沒有對此提供支持。假設(shè)希望 HTML 文件設(shè)置文本域的 maxlength
屬性來與驗(yàn)證匹配,或者在文本域之后放上一個百分號(%)來表示要輸入百分比的值。通常,這些信息都被硬編寫到 HTML 文檔中了。如果決定修改 name
屬性來支持 75 個字符,而不是 60 個字符,那么需要改動多少地方呢?在很多應(yīng)用程序中,通常都需要:
- 更新 DDL 來增大數(shù)據(jù)庫列的長度(通過 HibernateDoclet、 hbm.xml 或 Hibernate Annotations)。
- 更新 Commons Validator XML 文件將最大值增加到 75。
- 更新所有與這個域有關(guān)的 HTML 表單,以修改
maxlength
屬性。
更好的方法是使用 Hibernate Validator。驗(yàn)證的定義都被通過注釋 添加到了模型層中,同時(shí)還有對所包含的驗(yàn)證處理的支持。如果選擇充分利用所有的 Hibernate,這個 Validator 就可以在 DAO 和 DBMS 層也提供驗(yàn)證。在下面給出的樣例代碼中,將使用 reflection 和 JSP 2.0 標(biāo)簽文件多執(zhí)行一個步驟,從而充分利用注釋 為視圖層動態(tài)生成代碼。這可以清除在視圖中使用的硬編寫的業(yè)務(wù)邏輯。
在清單 3 中,dateOfBirth
被注釋為 NotNull
和過去的日期。 Hibernate 的 DDL 生成代碼對這個列添加了一個非空約束,以及一個要求日期必須是之前日期的檢查約束。e-mail 地址也是非空的,必須匹配 e-mail 地址的格式。這會生成一個非空約束,但是不會生成匹配這種格式的檢查約束。
清單 3. 通過 Hibernate Annotations 進(jìn)行映射的簡單聯(lián)系方式
/**
* A Simplified object that stores contact information.
*
* @author Ted Bergeron
* @version $Id: Contact.java,v 1.1 2006/04/24 03:39:34 ted Exp $
*/
@MappedSuperclass
@Embeddable
public class Contact implements Serializable {
public static final int MAX_FIRST_NAME = 30;
public static final int MAX_MIDDLE_NAME = 1;
public static final int MAX_LAST_NAME = 30;
private String fname;
private String mi;
private String lname;
private Date dateOfBirth;
private String emailAddress;
private Address address;
public Contact() {
this.address = new Address();
}
@Valid
@Embedded
public Address getAddress() {
return address;
}
public void setAddress(Address a) {
if (a == null) {
address = new Address();
} else {
address = a;
}
}
@NotNull
@Length(min = 1, max = MAX_FIRST_NAME)
@Column(name = "fname")
public String getFirstname() {
return fname;
}
public void setFirstname(String fname) {
this.fname = fname;
}
@Length(min = 1, max = MAX_MIDDLE_NAME)
@Column(name = "mi")
public String getMi() {
return mi;
}
public void setMi(String mi) {
this.mi = mi;
}
@NotNull
@Length(min = 1, max = MAX_LAST_NAME)
@Column(name = "lname")
public String getLastname() {
return lname;
}
public void setLastname(String lname) {
this.lname = lname;
}
@NotNull
@Past
@Column(name = "dob")
public Date getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
@NotNull
@Email
@Column(name = "email")
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
|
|
樣例應(yīng)用程序
在 下載 一節(jié),您可以下載一個樣例應(yīng)用程序,它展示了本文中采用的設(shè)計(jì)思想和代碼。由于這是一個可以工作的應(yīng)用程序,因此代碼比本文中討論的的內(nèi)容更為復(fù)雜。例如,清單 9 就節(jié)選于標(biāo)簽文件 text.tag;這個樣例應(yīng)用程序具有標(biāo)簽文件使用的所有代碼,以及其他三個類似的標(biāo)簽文件使用的代碼(用于選擇、隱藏和檢查框的 HTML 元素)。由于這是一個可以工作的應(yīng)用程序,它包含了一個在這種類型的應(yīng)用程序中都可以找到的架構(gòu)。還有一個 Ant 構(gòu)建文件、Spring 和 Hibernate XML 封裝代碼,以及 log4j 配置。雖然這些都不是本文介紹的重點(diǎn),但是您會發(fā)現(xiàn)仔細(xì)研究一下這個樣例應(yīng)用程序的源代碼是非常有用的。 |
|
如果需要,Hibernate DAO 實(shí)現(xiàn)也可以使用 Validation Annotations。所需做的是在 hibernate.cfg.xml 文件中指定基于 Hibernate 事件的驗(yàn)證規(guī)則。(更多信息請參考 Hibernate Validator 的文檔;可以在 參考資料 一節(jié)中找到相關(guān)的鏈接)。如果真地希望抄近路,您可以只捕獲服務(wù)或控制器中的 InvalidStateException
異常,并循環(huán)遍歷 InvalidValue
數(shù)組。
對控制器添加驗(yàn)證
要執(zhí)行驗(yàn)證,需要創(chuàng)建一個 Hibernate 的 ClassValidator
實(shí)例。這個類進(jìn)行實(shí)例化的代價(jià)可能會很高,因此最好只對希望進(jìn)行驗(yàn)證的每個類來進(jìn)行實(shí)例化。一種方法是創(chuàng)建一個實(shí)用工具類,對每個模型對象存儲一個 ClassValidator
實(shí)例,如清單 4 所示:
清單 4. 處理驗(yàn)證的實(shí)用工具類
/**
* Handles validations based on the Hibernate Annotations Validator framework.
* @author Ted Bergeron
* @version $Id: AnnotationValidator.java,v 1.5 2006/01/20 17:34:09 ted Exp $
*/
public class AnnotationValidator {
private static Log log = LogFactory.getLog(AnnotationValidator.class);
// It is considered a good practice to execute these lines once and
// cache the validator instances.
public static final ClassValidator<Customer> CUSTOMER_VALIDATOR =
new ClassValidator<Customer>(Customer.class);
public static final ClassValidator<CreditCard> CREDIT_CARD_VALIDATOR =
new ClassValidator<CreditCard>(CreditCard.class);
private static ClassValidator<? extends BaseObject> getValidator(Class<?
extends BaseObject> clazz) {
if (Customer.class.equals(clazz)) {
return CUSTOMER_VALIDATOR;
} else if (CreditCard.class.equals(clazz)) {
return CREDIT_CARD_VALIDATOR;
} else {
throw new IllegalArgumentException("Unsupported class was passed.");
}
}
public static InvalidValue[] getInvalidValues(BaseObject modelObject) {
String nullProperty = null;
return getInvalidValues(modelObject, nullProperty);
}
public static InvalidValue[] getInvalidValues(BaseObject modelObject,
String property) {
Class<? extends BaseObject>clazz = modelObject.getClass();
ClassValidator validator = getValidator(clazz);
InvalidValue[] validationMessages;
if (property == null) {
validationMessages = validator.getInvalidValues(modelObject);
} else {
// only get invalid values for specified property.
// For example, "city" applies to getCity() method.
validationMessages = validator.getInvalidValues(modelObject, property);
}
return validationMessages;
}
}
|
在清單 4 中,創(chuàng)建了兩個 ClassValidator
,一個用于 Customer
,另外一個用于 CreditCard
。這兩個希望進(jìn)行驗(yàn)證的類可以調(diào)用 getInvalidValues(BaseObject modelObject)
,會返回 InvalidValue[]
。這則會返回一個包含模型對象實(shí)例錯誤的數(shù)組。另外,這個方法也可以通過提供一個特定的屬性名來調(diào)用,這樣做會只返回與該域有關(guān)的錯誤。
在使用 Spring MVC 和 Hibernate Validator 時(shí),為信用卡創(chuàng)建一個驗(yàn)證過程變得非常簡單,如清單 5 所示:
清單 5. Spring MVC 控制器使用的 CreditCardValidator
/**
* Performs validation of a CreditCard in Spring MVC.
*
* @author Ted Bergeron
* @version $Id: CreditCardValidator.java,v 1.2 2006/02/10 21:53:50 ted Exp $
*/
public class CreditCardValidator implements Validator {
private CreditCardService creditCardService;
public void setCreditCardService(CreditCardService service) {
this.creditCardService = service;
}
public boolean supports(Class clazz) {
return CreditCard.class.isAssignableFrom(clazz);
}
public void validate(Object object, Errors errors) {
CreditCard creditCard = (CreditCard) object;
InvalidValue[] invalids = AnnotationValidator.getInvalidValues(creditCard);
// Perform "expensive" validation only if no simple errors found above.
if (invalids == null || invalids.length == 0) {
boolean validCard = creditCardService.validateCreditCard(creditCard);
if (!validCard) {
errors.reject("error.creditcard.invalid");
}
} else {
for (InvalidValue invalidValue : invalids) {
errors.rejectValue(invalidValue.getPropertyPath(),
null, invalidValue.getMessage());
}
}
}
}
|
validate()
方法只需要將 creditCard
實(shí)例傳遞給這個驗(yàn)證過程,從而返回 InvalidValue
數(shù)組。如果發(fā)現(xiàn)了一個或多個這種簡單錯誤,那么就可以將 Hibernate 的 InvalidValue
數(shù)組轉(zhuǎn)換成 Spring 的 Errors
對象。如果用戶已經(jīng)創(chuàng)建了這個信用卡并且沒有出現(xiàn)任何簡單錯誤,就可以將更加徹底的驗(yàn)證委托給服務(wù)層進(jìn)行。這一層可以與商業(yè)服務(wù)提供者一起對信用卡進(jìn)行驗(yàn)證。
現(xiàn)在我們已經(jīng)看到這個簡單的模型層注釋是如何平衡到控制器、DAO 和 DBMS 層的驗(yàn)證的。在 HibernateDoclet 和 Commons Validator 中發(fā)現(xiàn)的驗(yàn)證邏輯的重合現(xiàn)在都已經(jīng)統(tǒng)一到模型中了。盡管這是一個非常受歡迎的改進(jìn),但是視圖層傳統(tǒng)上來說一直是最需要進(jìn)行詳細(xì)驗(yàn)證的地方。
為視圖添加驗(yàn)證
在下面的例子中,使用了 Spring MVC 和 JSP 2.0 標(biāo)簽文件。JSP 2.0 允許在 TLD 文件中對定制函數(shù)進(jìn)行注冊,并在一個標(biāo)簽文件中進(jìn)行調(diào)用。標(biāo)簽文件類似于 taglibs,但是它們是使用 JSP 代碼編寫的,而不是使用 Java 代碼編寫的。采用這種方法,使用 Java 語言寫好的代碼就可以封裝成函數(shù),而使用 JSP 寫好的代碼則可以放入標(biāo)簽文件中。在這種情況中,對注釋的處理需要使用映像,這會由幾個函數(shù)來執(zhí)行。綁定 Spring 或呈現(xiàn) XHTML 的代碼也是標(biāo)簽文件的一部分。
清單 6 中節(jié)選的 TLD 代碼定義 text.tag 文件可以使用,并定義了一個名為 required
的函數(shù)。
清單 6. 創(chuàng)建表單 TLD
<?xml version="1.0" encoding="ISO-8859-1" ?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<tlib-version>1.0</tlib-version>
<short-name>form</short-name>
<uri>formtags</uri>
<tag-file>
<name>text</name>
<path>/WEB-INF/tags/form/text.tag</path>
</tag-file>
<function>
<description>determine if field is required from Annotations</description>
<name>required</name>
<function-class>com.triview.web.Utilities</function-class>
<function-signature>Boolean required(java.lang.Object,java.lang.String)
</function-signature>
</function>
</taglib>
|
清單 7 節(jié)選自 Utilities
類,其中包含了標(biāo)簽文件使用的所有函數(shù)。在前文中我們曾經(jīng)說過,最適合使用 Java 代碼編寫的代碼都被放到了幾個 TLD 可以映射的函數(shù)中,這樣標(biāo)簽文件就可以使用它們了;這些函數(shù)都是在 Utilities
類中進(jìn)行編碼的。因此,我們需要三樣?xùn)|西:定義這些類的 TLD 文件、Utilities
中的函數(shù),以及標(biāo)簽文件本身,后者要使用這些函數(shù)。(第四樣應(yīng)該是使用這個標(biāo)簽文件的 JSP 頁面。)
在清單 7 中,給出了在 TLD 中引用的函數(shù)和另外一個表示給定屬性是否是 Date
的方法。在這個類中要涉及到比較多的代碼,但是本文限于篇幅,不會給出所有的代碼;不過需要注意 findGetterMethod()
除了將表達(dá)式語言(Expression Language,EL)方法表示(customer.contact
)轉(zhuǎn)換成 Java 表示(customer.getContact()
)之外,還執(zhí)行了基本的映像操作。
清單 7. Utilities 節(jié)選
public static Boolean required(Object object, String propertyPath) {
Method getMethod = findGetterMethod(object, propertyPath);
if (getMethod == null) {
return null;
} else {
return getMethod.isAnnotationPresent(NotNull.class);
}
}
public static Boolean isDate(Object object, String propertyPath) {
return java.util.Date.class.equals(getReturnType(object, propertyPath));
}
public static Class getReturnType(Object object, String propertyPath) {
Method getMethod = findGetterMethod(object, propertyPath);
if (getMethod == null) {
return null;
} else {
return getMethod.getReturnType();
}
}
|
此處可以清楚地看到在運(yùn)行時(shí)使用 Validation annotations 是多么容易。可以簡單地引用對象的 getter 方法,并檢查這個方法是否有相關(guān)的給定的注釋 。
清單 8 中給出的 JSP 例子進(jìn)行了簡化,這樣就可以著重查看相關(guān)的部分了。此處,這里有一個表單,它有一個選擇框和兩個輸入域。所有這些域都是通過在 form.tld 文件中聲明的標(biāo)簽文件進(jìn)行呈現(xiàn)的。標(biāo)簽文件被設(shè)計(jì)成使用智能缺省值,這樣就可以根據(jù)需要允許簡單編碼的 JSP 可以有定義更多信息的選項(xiàng)。關(guān)鍵的屬性是 propertyPath
,它使用 EL 符號將這個域映射為模型層屬性,就像是使用 Spring MVC 的 bind
標(biāo)簽一樣。
清單 8. 一個包含表單的簡單 JSP 頁面
<%@ taglib tagdir="/WEB-INF/tags/form" prefix="form" %>
<form method="post" action="<c:url value="/signup/customer.edit"/>">
<form:select propertyPath="creditCard.type" collection="${creditCardTypeCollection}"
required="true" labelKey="prompt.creditcard.type"/>
<form:text propertyPath="creditCard.number" labelKey="prompt.creditcard.number">
<img src="<c:url value="/images/icons/help.png"/>" alt="Help"
onclick="new Effect.SlideDown('creditCardHelp')"/>
</form:text>
<form:text propertyPath="creditCard.expirationDate"/>
</form>
|
text.tag 文件的完整源代碼太大了,不好放在這兒,因此清單 9 給出了其中關(guān)鍵的部分:
清單 9. 標(biāo)簽文件 text.tag 節(jié)選
<%@ attribute name="propertyPath" required="true" %>
<%@ attribute name="size" required="false" type="java.lang.Integer" %>
<%@ attribute name="maxlength" required="false" type="java.lang.Integer" %>
<%@ attribute name="required" required="false" type="java.lang.Boolean" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="formtags" prefix="form" %>
<c:set var="objectPath" value="${form:getObjectPath(propertyPath)}"/>
<spring:bind path="${objectPath}">
<c:set var="object" value="${status.value}"/>
<c:if test="${object == null}">
<%-- Bind ignores the command object prefix, so simple properties of the command object
return null above. --%>
<c:set var="object" value="${commandObject}"/>
<%-- We depend on the controller adding this to request. --%>
</c:if>
</spring:bind>
<%-- If user did not specify whether this field is required,
query the object for this info. --%>
<c:if test="${required == null}">
<c:set var="required" value="${form:required(object,propertyPath)}"/>
</c:if>
<c:choose>
<c:when test="${required == null || required == false}">
<c:set var="labelClass" value="optional"/>
</c:when>
<c:otherwise>
<c:set var="labelClass" value="required"/>
</c:otherwise>
</c:choose>
<c:if test="${maxlength == null}">
<c:set var="maxlength" value="${form:maxLength(object,propertyPath)}"/>
</c:if>
<c:set var="isDate" value="${form:isDate(object,propertyPath)}"/>
<c:set var="cssClass" value="input_text"/>
<c:if test="${isDate}">
<c:set var="cssClass" value="input_date"/>
</c:if>
<div class="field">
<spring:bind path="${propertyPath}">
<label for="${status.expression}" class="${labelClass}"><fmt:message
key="prompt.${propertyPath}"/></label>
<input type="text" name="${status.expression}" value="${status.value}"
id="${status.expression}"<c:if test="${size != null}"> size="${size}"</c:if>
<c:if test="${maxlength != null}"> maxlength="${maxlength}"</c:if>
class="${cssClass}"/>
<c:if test="${isDate}">
<img id="${status.expression}_button"
src="<c:url value="/images/icons/calendar.png"/>" alt="calendar"
style="cursor: pointer;"/>
<script type="text/javascript">
Calendar.setup(
{
inputField : "${status.expression}", // ID of the input field
ifFormat : "%m/%d/%Y", // the date format
button : "${status.expression}_button" // ID of the button
}
);
</script>
</c:if>
<span class="icons"><jsp:doBody/></span>
<c:if test="${status.errorMessage != null && status.errorMessage != ''}">
<p class="fieldError"><img id="${status.expression}_error"
src="<c:url value="/images/icons/error.png"/>"
alt="error"/>${status.errorMessage}</p>
</c:if>
</spring:bind>
</div>
|
我們馬上就可以看出 propertyPath
是惟一需要的屬性。size
、 maxlength
和 required
都可以忽略。objectPath var
被設(shè)置為在 propertyPath
中引用的屬性的父對象。因此,如果 propertyPath
是 customer.contact.fax.number
, 那么 objectPath
就應(yīng)該被設(shè)置為 customer.contact.fax
。我們現(xiàn)在就使用 Spring 的 bind
標(biāo)簽綁定到了包含屬性的對象上。這會將對象變量設(shè)置成對包含屬性的實(shí)例的引用。接下來,檢查這個標(biāo)簽的用戶是否已經(jīng)指定他/她們是否希望屬性是必須的。允許表單開發(fā)人員覆蓋從注釋中返回的值是非常重要的,因?yàn)橛袝r(shí)他/她們希望讓控制器為所需要的域設(shè)置缺省值,而用戶可能并不希望為這個域提供值。如果表單開發(fā)人員沒有為 required
指定值,那么就可以調(diào)用這個表單 TLD 的 required
函數(shù)。這個函數(shù)調(diào)用了在 TLD 文件中映射的方法。這個方法簡單地檢查 @NotNull
注釋;如果它發(fā)現(xiàn)某個屬性具有這個注釋,就將 labelClass
變量設(shè)置為必須的。可以類似地確定正確的 maxlength
以及這個域是否是一個 Date
。
接下來使用 Spring 來綁定到 propertyPath
上,而不是像前面一樣只綁定到包含這個屬性的對象上。這允許在生成 label
和 input
HTML 標(biāo)簽時(shí)使用 status.expression
和 status.value
。 input
標(biāo)簽也可以使用一個大小 maxlength
以及適當(dāng)?shù)念悂砩伞H绻懊嬉呀?jīng)確定屬性是一個 Date
,現(xiàn)在就可以添加 JavaScript 日歷了。(可以在 參考資料 一節(jié)找到一個很好的日歷組件的鏈接)。注意根據(jù)需要鏈接屬性、輸入 ID 和圖像 ID 的標(biāo)簽是多么簡單。)這個 JavaScript 日歷需要一個圖像 ID 來匹配輸入域,其后綴是 _button
。
最后,可以將 <jsp:doBody/>
封裝到一個 span
標(biāo)簽中,這樣允許表單開發(fā)人員在頁面中添加其他圖標(biāo),例如用來尋求幫助的圖標(biāo)。(清單 8 給出了一個為信用卡號域添加的幫助圖標(biāo)。)最后的部分是檢查 Spring 是否為這個屬性報(bào)告和顯示了一個錯誤,并和一個錯誤圖標(biāo)一起顯示。
使用 CSS,就可以對必須的域進(jìn)行一下裝飾 —— 例如,讓它們以紅色顯示、在文本邊上顯示一個星號,或者使用一個背景圖像來裝飾它。在清單 10 中,將必須的域的標(biāo)簽設(shè)置成黑色,而且后面顯示一個紅色的星號(在 Firefox 以及其他標(biāo)準(zhǔn)兼容的瀏覽器中),如果是在 IE 中則還會在左邊加上一個小旗子的背景圖像:
清單 10. 對必須域進(jìn)行裝飾的 CSS 代碼
label.required {
color: black;
background-image: url( /images/icons/flag_red.png );
background-position: left;
background-repeat: no-repeat;
}
label.required:after {
content: '*';
}
label.optional {
color: black;
}
|
日期輸入域自動會在右邊放上一個 JavaScript 日歷圖標(biāo)。對所有的文本域設(shè)置正確的 maxlength
屬性可以防止用戶輸入太多文本所引起的錯誤。可以擴(kuò)展 text
標(biāo)簽來為輸入域類設(shè)置其他的數(shù)據(jù)類型。可以修改 text
標(biāo)簽使用 HTML,而不是 XHTML(如果希望這樣)。可以不太費(fèi)力地獲得具有正確語義的 HTML 表單,而且不需學(xué)習(xí)基于組件的框架知識,就可以利用基于組件的 Web 框架的優(yōu)點(diǎn)。
盡管標(biāo)簽文件生成的 HTML 文件可以幫助防止一些錯誤的產(chǎn)生,但是在視圖層并沒有任何代碼來真正進(jìn)行錯誤檢查。由于可以使用類屬性,現(xiàn)在就可以添加一些簡單的 JavaScript 來實(shí)現(xiàn)這種功能了,如清單 11 所示。這里的 JavaScript 也可以是通用的,在任一表單中都可以重用。
清單 11. 簡單的 JavaScript 驗(yàn)證程序
<script type="text/javascript">
function checkRequired(form) {
var requiredLabels = document.getElementsByClassName("required", form);
for (i = 0; i < requiredLabels.length; i++) {
var labelText = requiredLabels[i].firstChild.nodeValue; // Get the label's text
var labelFor = requiredLabels[i].getAttribute("for"); // Grab the for attribute
var inputTag = document.getElementById(labelFor); // Get the input tag
if (inputTag.value == null || inputTag.value == "") {
alert("Please make sure all required fields have been entered.");
return false; // Abort Submit
}
}
return true;
}
</script>
|
這個 JavaScript 是通過為表單聲明添加 onsubmit="return checkRequired(this);"
被調(diào)用的。這個腳本簡單地獲取具有所需要的類的表單中的所有元素。由于我們的習(xí)慣是在標(biāo)簽標(biāo)記中使用這個類,因此代碼會通過 for
屬性來查找與這個標(biāo)簽連接在一起的輸入域。如果任何輸入域?yàn)榭眨蜁梢粭l簡單的警告消息,表單提交就會取消。可以簡單地對這個腳本進(jìn)行擴(kuò)充,使其掃描多個類,并相應(yīng)地進(jìn)行驗(yàn)證。
對于基于 JavaScript 的綜合的驗(yàn)證集合來說,最好是使用開源實(shí)現(xiàn),而不是自行開發(fā)。在清單 8 中您可能已經(jīng)注意到下面的代碼:
onclick="new Effect.SlideDown('creditCardHelp')"
|
這個函數(shù)是 Script.aculo.us 庫的一部分,這個庫提供了很多高級的效果。如果正在使用 Script.aculo.us,就需要對所構(gòu)建的內(nèi)容使用 Prototype 庫。 JavaScript 驗(yàn)證庫的一個例子是由 Andrew Tetlaw 在 Prototype 基礎(chǔ)上構(gòu)建的。(請參看 參考資料 一節(jié)中的鏈接。)他的框架依賴于被添加到輸入域的類:
<input class="required validate-number" id="field1" name="field1" />
|
可以簡單地修改 text.tag 的邏輯在 input
標(biāo)簽中插入幾個類。將 class="required"
添加到輸入標(biāo)簽和 label
標(biāo)簽中不會影響 CSS 規(guī)則,但會破壞清單 10 中給出的簡單 JavaScript 驗(yàn)證程序。如果要混合使用框架中的代碼和簡單的 JavaScript 代碼,最好使用不同的類名,或在使用類名搜索元素時(shí)確保類名有效并檢查標(biāo)簽類型。
最后的考慮
本文已經(jīng)介紹了模型層的注釋如何充分用來在視圖、控制器、DAO 和 DBMS 層中創(chuàng)建驗(yàn)證。必須手工創(chuàng)建服務(wù)層的驗(yàn)證,例如信用卡驗(yàn)證。其他模型層的驗(yàn)證,例如強(qiáng)制屬性 C 是必須的,而屬性 A 和 B 都處于指定的狀態(tài),這也是一個手工任務(wù)。然而,使用 Hibernate Annotations 的 Validator 組件,就可以輕松地聲明并應(yīng)用大多數(shù)驗(yàn)證。
展望
不論是簡單例子還是所引用框架的 JavaScript 驗(yàn)證都可以對簡單的條件進(jìn)行檢查,例如域必須要填寫,或者客戶機(jī)端代碼中的數(shù)據(jù)類型必須要匹配預(yù)期的類型。需要用到服務(wù)器端邏輯的驗(yàn)證可以使用 Ajax 添加到 JavaScript 驗(yàn)證程序中。您可以使用一個用戶注冊界面來讓用戶可以選擇用戶名。文本標(biāo)簽可以進(jìn)行增強(qiáng)來檢查 @Column(unique = true)
注釋。在找到這個注釋時(shí),標(biāo)簽可以添加一個用來觸發(fā) Ajax 調(diào)用的類。
現(xiàn)在您不需要在應(yīng)用程序?qū)娱g維護(hù)重復(fù)的驗(yàn)證邏輯了,這樣就可以節(jié)省出大量的開發(fā)時(shí)間。想像一下您最終可以為應(yīng)用程序所能添加的增強(qiáng)功能!
下載
描述 |
名字 |
大小 |
下載方法 |
示例應(yīng)用程序 |
j-hibval-source.zip |
8MB |
HTTP
|
參考資料
學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
討論
關(guān)于作者
|
|
|
Ted Bergeron 是 Triview 的合作創(chuàng)始人之一,Triview 是一家企業(yè)軟件咨詢公司,位于加利福尼亞的圣地亞哥。Ted 從事基于 Web 的應(yīng)用程序的設(shè)計(jì)已經(jīng)有十 多年的時(shí)間了。他所做過的一些知名的項(xiàng)目包括為 Sybase、Orbitz、Disney 和 Qualcomm 所設(shè)計(jì)的項(xiàng)目。Ted 還曾用三 年的時(shí)間作為一名技術(shù)講師來教授有關(guān) Web 開發(fā)、Java 開發(fā)和數(shù)據(jù)庫邏輯設(shè)計(jì)的課程。您可以在 Triview 的 Web 站點(diǎn) 上了解有關(guān) Triview 公司的更多內(nèi)容,或者也可以撥打該公司的免費(fèi)電話 (866)TRIVIEW。
|
posted on 2006-10-16 14:19
xzc 閱讀(731)
評論(0) 編輯 收藏 所屬分類:
Hibernate