注釋簡化了數據驗證
|
|
級別: 中級
Ted Bergeron
(ted@triview.com), 合作創始人, Triview, Inc.
2006 年 10 月 10 日
盡管在 Web 應用程序中盡可能多的層次中構建數據驗證非常重要,但是這樣做卻非常耗時,以至于很多開發人員都會干脆忽略這個步驟 —— 這可能會導致今后大量問題的產生。但是隨著最新版本的 Java 平臺中引入了注釋,驗證變得簡單得多了。在本文中,Ted Bergeron 將向您介紹如何使用 Hibernate Annotations 的 Validator 組件在 Web 應用程序中輕松構建并維護驗證邏輯。
有時會有一種工具,它可以真正滿足開發人員和架構師的需求。開發人員在第一次下載這種工具當天就可以在自己的應用程序中開始使用這種工具。理論上來說,這種工具在開發人員花費大量時間來掌握其用法之前就可以從中獲益。架構師也很喜歡這種工具,因為它可以將開發人員導向更高理論層次的實現。Hibernate Annotations 的 Validator 組件就是一種這樣的工具。
|
開始之前需要了解的內容
在閱讀本文之前,應該對 Java 平臺版本 5(尤其是注釋)、JSP 2.0(因為本文中創建了一些標簽文件,并在 TLD 中定義了一些函數,它們都是 JSP 2.0 的新特性)和 Hibernate 及 Spring 框架有一個基本的了解。請注意即使不使用 Hibernate 來實現持久性,也可以在自己的應用程序中使用 Hibernate Validator。
|
|
Java SE 5 為 Java 語言提供了很多需要的增強功能,不過其他增強功能可能都不如 注釋 這樣潛力巨大。使用 注釋,我們就終于具有了一個標準、一級的元數據框架為 Java 類使用。Hibernate 用戶手工編寫 *.hbm.xml 文件已經很多年了(或者使用 XDoclet 來自動實現這個任務)。如果手工創建了 XML 文件,那就必須對每個所需要的持久屬性都更新這兩個文件(類定義和 XML 映射文檔)。使用 HibernateDoclet 可以簡化這個過程(請參看清單 1 給出的例子),但是這需要我們確認自己的 HibernateDoclet 版本支持要使用的 Hibernate 的版本。doclet 信息在運行時也是不可用的,因為它被編寫到了 Javadoc 風格的注釋中了。Hibernate Annotations,如圖 2 所示,通過提供一個標準、簡明的映射類的方法和所添加的運行時可用性來對這些方式進行改進。
清單 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 文件或運行時才能捕獲錯誤。使用 注釋,在編譯時就可以檢測出很多錯誤;或者如果在編輯時使用了很好的 IDE,那么在編輯時就可以檢測出部分錯誤。在從頭創建應用程序時,可以利用 hbm2ddl 工具為自己的數據庫從 hbm.xml 文件中生成 DDL。一些重要的信息 —— 比如name
屬性的最大長度必須是 60 個字符,或者 DDL 應該添加非空約束 —— 都被從 HibernateDoclet 項添加到 DDL 中。當使用注釋時,我們可以以類似的方式自動生成 DDL。
盡管這兩種代碼映射方式都可以使用,不過注釋的優勢更為明顯。使用注釋,可以用一些常量來指定長度或其他值。編譯循環的速度更快,并且不需要生成 XML 文件。其中最大的優勢是可以訪問一些有用信息,例如運行時的非空注釋或長度。除了清單 2 給出的注釋之外,還可以指定一些驗證的約束。所包含的部分約束如下:
-
@Max(value = 100)
-
@Min(value = 0)
-
@Past
-
@Future
-
@Email
在適當條件下,這些注釋會引起由 DDL 生成檢查約束。(顯然,@Future
并不是一個適當的條件。)還可以根據需要創建定制約束注釋。
驗證和應用程序層
編寫驗證代碼是一個煩人且耗時的過程。通常,很多開發人員都會放棄在特定的層進行有效性驗證,從而可以節省一些時間;但是所節省的時間是否能夠彌補在這個地方因忽略部分功能所引起的缺陷卻非常值得探討。如果在所有應用程序層中創建并維護驗證所需要的時間可以極大地減少,那么爭論的焦點就會轉向是否要在多個層次中進行有效性驗證。假設有一個應用程序,它讓用戶使用一個用戶名、密碼和信用卡號來創建一個帳號。在這個應用程序中所希望進行驗證的組件如下:
-
視圖: 通過 JavaScript 進行驗證可以避免與服務器反復進行交互,這樣可以提供更好的用戶體驗。用戶可以禁用 JavaScript,因此這個層次的驗證最好要有,但是卻并不可靠。對所需要的域進行簡單的驗證是必須的。
-
控制器: 驗證必須在服務器端的邏輯中進行處理。這個層次中的代碼可以以適合某個特定用途的方式處理驗證。例如,在添加新用戶時,控制器可以在進行處理之前檢查指定的用戶名是否已經存在。
-
服務: 相對復雜的業務邏輯驗證通常都最適合放到服務層中。例如,一旦有一個信用卡對象看起來有效,就應該使用信用卡處理服務對這個信用卡的信息進行確認。
-
DAO: 在數據到達這個層次時,應該已經是有效的了。盡管如此,執行一次快速檢查從而確保所需要的域都非空并且值也都在特定的范圍或遵循特定的格式(例如 e-mail 地址域就應該包含一個有效的 e-mail 地址)也是非常有益的。在此處捕獲錯誤總比產生可以避免的
SQLException
錯誤要好。
-
DBMS: 這是通常可以忽略驗證的地方。即使當前正在構建的應用程序是數據庫的惟一客戶機,將來還可能會添加其他客戶機。如果應用程序有一些 bug(大部分應用程序都可能會有 bug),那么無效的數據也可能會被發送給數據庫。在這種情況中,如果走運,就可以找到無效的數據,并且需要分析這些數據是否可以清除,以及如何清除。
-
模型: 這是進行驗證的一個理想地方,它不需要訪問外部服務,也不需要了解持久性數據。例如,某業務邏輯可能會要求用戶至少提供一個聯系信息,這可以是一個電話號碼也可以是一個 e-mail 地址;可以使用模型層的驗證來確保用戶的確提供了這種信息。
進行驗證的一種典型方法是對簡單的驗證使用 Commons Validator,并在控制器中編寫其他一些驗證邏輯。Commons Validator 可以生成 JavaScript 來對視圖中的驗證進行處理。但是 Commons Validator 也有自己的缺陷:它只能處理簡單的驗證問題,并且將驗證的信息都保存到了 XML 文件中。Commons Validator 被設計用來與 Struts 一起使用,而且沒有提供一種簡單的方法在應用程序層間重用驗證的聲明。
在規劃有效性驗證策略時,選擇在錯誤發生時簡單地處理這些錯誤是遠遠不夠的。一種良好的設計同時還要通過生成一個友好的用戶界面來防止出現錯誤。采用預先進行的方法進行驗證可以極大地增強用戶對于應用程序的理解。不幸的是,Commons Validator 并沒有對此提供支持。假設希望 HTML 文件設置文本域的 maxlength
屬性來與驗證匹配,或者在文本域之后放上一個百分號(%)來表示要輸入百分比的值。通常,這些信息都被硬編寫到 HTML 文檔中了。如果決定修改 name
屬性來支持 75 個字符,而不是 60 個字符,那么需要改動多少地方呢?在很多應用程序中,通常都需要:
- 更新 DDL 來增大數據庫列的長度(通過 HibernateDoclet、 hbm.xml 或 Hibernate Annotations)。
- 更新 Commons Validator XML 文件將最大值增加到 75。
- 更新所有與這個域有關的 HTML 表單,以修改
maxlength
屬性。
更好的方法是使用 Hibernate Validator。驗證的定義都被通過注釋 添加到了模型層中,同時還有對所包含的驗證處理的支持。如果選擇充分利用所有的 Hibernate,這個 Validator 就可以在 DAO 和 DBMS 層也提供驗證。在下面給出的樣例代碼中,將使用 reflection 和 JSP 2.0 標簽文件多執行一個步驟,從而充分利用注釋 為視圖層動態生成代碼。這可以清除在視圖中使用的硬編寫的業務邏輯。
在清單 3 中,dateOfBirth
被注釋為 NotNull
和過去的日期。 Hibernate 的 DDL 生成代碼對這個列添加了一個非空約束,以及一個要求日期必須是之前日期的檢查約束。e-mail 地址也是非空的,必須匹配 e-mail 地址的格式。這會生成一個非空約束,但是不會生成匹配這種格式的檢查約束。
清單 3. 通過 Hibernate Annotations 進行映射的簡單聯系方式
/**
* 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;
}
|
|
樣例應用程序
在 下載 一節,您可以下載一個樣例應用程序,它展示了本文中采用的設計思想和代碼。由于這是一個可以工作的應用程序,因此代碼比本文中討論的的內容更為復雜。例如,清單 9 就節選于標簽文件 text.tag;這個樣例應用程序具有標簽文件使用的所有代碼,以及其他三個類似的標簽文件使用的代碼(用于選擇、隱藏和檢查框的 HTML 元素)。由于這是一個可以工作的應用程序,它包含了一個在這種類型的應用程序中都可以找到的架構。還有一個 Ant 構建文件、Spring 和 Hibernate XML 封裝代碼,以及 log4j 配置。雖然這些都不是本文介紹的重點,但是您會發現仔細研究一下這個樣例應用程序的源代碼是非常有用的。 |
|
如果需要,Hibernate DAO 實現也可以使用 Validation Annotations。所需做的是在 hibernate.cfg.xml 文件中指定基于 Hibernate 事件的驗證規則。(更多信息請參考 Hibernate Validator 的文檔;可以在 參考資料 一節中找到相關的鏈接)。如果真地希望抄近路,您可以只捕獲服務或控制器中的 InvalidStateException
異常,并循環遍歷 InvalidValue
數組。
對控制器添加驗證
要執行驗證,需要創建一個 Hibernate 的 ClassValidator
實例。這個類進行實例化的代價可能會很高,因此最好只對希望進行驗證的每個類來進行實例化。一種方法是創建一個實用工具類,對每個模型對象存儲一個 ClassValidator
實例,如清單 4 所示:
清單 4. 處理驗證的實用工具類
/**
* 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 中,創建了兩個 ClassValidator
,一個用于 Customer
,另外一個用于 CreditCard
。這兩個希望進行驗證的類可以調用 getInvalidValues(BaseObject modelObject)
,會返回 InvalidValue[]
。這則會返回一個包含模型對象實例錯誤的數組。另外,這個方法也可以通過提供一個特定的屬性名來調用,這樣做會只返回與該域有關的錯誤。
在使用 Spring MVC 和 Hibernate Validator 時,為信用卡創建一個驗證過程變得非常簡單,如清單 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
實例傳遞給這個驗證過程,從而返回 InvalidValue
數組。如果發現了一個或多個這種簡單錯誤,那么就可以將 Hibernate 的 InvalidValue
數組轉換成 Spring 的 Errors
對象。如果用戶已經創建了這個信用卡并且沒有出現任何簡單錯誤,就可以將更加徹底的驗證委托給服務層進行。這一層可以與商業服務提供者一起對信用卡進行驗證。
現在我們已經看到這個簡單的模型層注釋是如何平衡到控制器、DAO 和 DBMS 層的驗證的。在 HibernateDoclet 和 Commons Validator 中發現的驗證邏輯的重合現在都已經統一到模型中了。盡管這是一個非常受歡迎的改進,但是視圖層傳統上來說一直是最需要進行詳細驗證的地方。
為視圖添加驗證
在下面的例子中,使用了 Spring MVC 和 JSP 2.0 標簽文件。JSP 2.0 允許在 TLD 文件中對定制函數進行注冊,并在一個標簽文件中進行調用。標簽文件類似于 taglibs,但是它們是使用 JSP 代碼編寫的,而不是使用 Java 代碼編寫的。采用這種方法,使用 Java 語言寫好的代碼就可以封裝成函數,而使用 JSP 寫好的代碼則可以放入標簽文件中。在這種情況中,對注釋的處理需要使用映像,這會由幾個函數來執行。綁定 Spring 或呈現 XHTML 的代碼也是標簽文件的一部分。
清單 6 中節選的 TLD 代碼定義 text.tag 文件可以使用,并定義了一個名為 required
的函數。
清單 6. 創建表單 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 節選自 Utilities
類,其中包含了標簽文件使用的所有函數。在前文中我們曾經說過,最適合使用 Java 代碼編寫的代碼都被放到了幾個 TLD 可以映射的函數中,這樣標簽文件就可以使用它們了;這些函數都是在 Utilities
類中進行編碼的。因此,我們需要三樣東西:定義這些類的 TLD 文件、Utilities
中的函數,以及標簽文件本身,后者要使用這些函數。(第四樣應該是使用這個標簽文件的 JSP 頁面。)
在清單 7 中,給出了在 TLD 中引用的函數和另外一個表示給定屬性是否是 Date
的方法。在這個類中要涉及到比較多的代碼,但是本文限于篇幅,不會給出所有的代碼;不過需要注意 findGetterMethod()
除了將表達式語言(Expression Language,EL)方法表示(customer.contact
)轉換成 Java 表示(customer.getContact()
)之外,還執行了基本的映像操作。
清單 7. Utilities 節選
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();
}
}
|
此處可以清楚地看到在運行時使用 Validation annotations 是多么容易。可以簡單地引用對象的 getter 方法,并檢查這個方法是否有相關的給定的注釋 。
清單 8 中給出的 JSP 例子進行了簡化,這樣就可以著重查看相關的部分了。此處,這里有一個表單,它有一個選擇框和兩個輸入域。所有這些域都是通過在 form.tld 文件中聲明的標簽文件進行呈現的。標簽文件被設計成使用智能缺省值,這樣就可以根據需要允許簡單編碼的 JSP 可以有定義更多信息的選項。關鍵的屬性是 propertyPath
,它使用 EL 符號將這個域映射為模型層屬性,就像是使用 Spring MVC 的 bind
標簽一樣。
清單 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 給出了其中關鍵的部分:
清單 9. 標簽文件 text.tag 節選
<%@ 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
被設置為在 propertyPath
中引用的屬性的父對象。因此,如果 propertyPath
是 customer.contact.fax.number
, 那么 objectPath
就應該被設置為 customer.contact.fax
。我們現在就使用 Spring 的 bind
標簽綁定到了包含屬性的對象上。這會將對象變量設置成對包含屬性的實例的引用。接下來,檢查這個標簽的用戶是否已經指定他/她們是否希望屬性是必須的。允許表單開發人員覆蓋從注釋中返回的值是非常重要的,因為有時他/她們希望讓控制器為所需要的域設置缺省值,而用戶可能并不希望為這個域提供值。如果表單開發人員沒有為 required
指定值,那么就可以調用這個表單 TLD 的 required
函數。這個函數調用了在 TLD 文件中映射的方法。這個方法簡單地檢查 @NotNull
注釋;如果它發現某個屬性具有這個注釋,就將 labelClass
變量設置為必須的。可以類似地確定正確的 maxlength
以及這個域是否是一個 Date
。
接下來使用 Spring 來綁定到 propertyPath
上,而不是像前面一樣只綁定到包含這個屬性的對象上。這允許在生成 label
和 input
HTML 標簽時使用 status.expression
和 status.value
。 input
標簽也可以使用一個大小 maxlength
以及適當的類來生成。如果前面已經確定屬性是一個 Date
,現在就可以添加 JavaScript 日歷了。(可以在 參考資料 一節找到一個很好的日歷組件的鏈接)。注意根據需要鏈接屬性、輸入 ID 和圖像 ID 的標簽是多么簡單。)這個 JavaScript 日歷需要一個圖像 ID 來匹配輸入域,其后綴是 _button
。
最后,可以將 <jsp:doBody/>
封裝到一個 span
標簽中,這樣允許表單開發人員在頁面中添加其他圖標,例如用來尋求幫助的圖標。(清單 8 給出了一個為信用卡號域添加的幫助圖標。)最后的部分是檢查 Spring 是否為這個屬性報告和顯示了一個錯誤,并和一個錯誤圖標一起顯示。
使用 CSS,就可以對必須的域進行一下裝飾 —— 例如,讓它們以紅色顯示、在文本邊上顯示一個星號,或者使用一個背景圖像來裝飾它。在清單 10 中,將必須的域的標簽設置成黑色,而且后面顯示一個紅色的星號(在 Firefox 以及其他標準兼容的瀏覽器中),如果是在 IE 中則還會在左邊加上一個小旗子的背景圖像:
清單 10. 對必須域進行裝飾的 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 日歷圖標。對所有的文本域設置正確的 maxlength
屬性可以防止用戶輸入太多文本所引起的錯誤。可以擴展 text
標簽來為輸入域類設置其他的數據類型。可以修改 text
標簽使用 HTML,而不是 XHTML(如果希望這樣)。可以不太費力地獲得具有正確語義的 HTML 表單,而且不需學習基于組件的框架知識,就可以利用基于組件的 Web 框架的優點。
盡管標簽文件生成的 HTML 文件可以幫助防止一些錯誤的產生,但是在視圖層并沒有任何代碼來真正進行錯誤檢查。由于可以使用類屬性,現在就可以添加一些簡單的 JavaScript 來實現這種功能了,如清單 11 所示。這里的 JavaScript 也可以是通用的,在任一表單中都可以重用。
清單 11. 簡單的 JavaScript 驗證程序
<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);"
被調用的。這個腳本簡單地獲取具有所需要的類的表單中的所有元素。由于我們的習慣是在標簽標記中使用這個類,因此代碼會通過 for
屬性來查找與這個標簽連接在一起的輸入域。如果任何輸入域為空,就會生成一條簡單的警告消息,表單提交就會取消。可以簡單地對這個腳本進行擴充,使其掃描多個類,并相應地進行驗證。
對于基于 JavaScript 的綜合的驗證集合來說,最好是使用開源實現,而不是自行開發。在清單 8 中您可能已經注意到下面的代碼:
onclick="new Effect.SlideDown('creditCardHelp')"
|
這個函數是 Script.aculo.us 庫的一部分,這個庫提供了很多高級的效果。如果正在使用 Script.aculo.us,就需要對所構建的內容使用 Prototype 庫。 JavaScript 驗證庫的一個例子是由 Andrew Tetlaw 在 Prototype 基礎上構建的。(請參看 參考資料 一節中的鏈接。)他的框架依賴于被添加到輸入域的類:
<input class="required validate-number" id="field1" name="field1" />
|
可以簡單地修改 text.tag 的邏輯在 input
標簽中插入幾個類。將 class="required"
添加到輸入標簽和 label
標簽中不會影響 CSS 規則,但會破壞清單 10 中給出的簡單 JavaScript 驗證程序。如果要混合使用框架中的代碼和簡單的 JavaScript 代碼,最好使用不同的類名,或在使用類名搜索元素時確保類名有效并檢查標簽類型。
最后的考慮
本文已經介紹了模型層的注釋如何充分用來在視圖、控制器、DAO 和 DBMS 層中創建驗證。必須手工創建服務層的驗證,例如信用卡驗證。其他模型層的驗證,例如強制屬性 C 是必須的,而屬性 A 和 B 都處于指定的狀態,這也是一個手工任務。然而,使用 Hibernate Annotations 的 Validator 組件,就可以輕松地聲明并應用大多數驗證。
展望
不論是簡單例子還是所引用框架的 JavaScript 驗證都可以對簡單的條件進行檢查,例如域必須要填寫,或者客戶機端代碼中的數據類型必須要匹配預期的類型。需要用到服務器端邏輯的驗證可以使用 Ajax 添加到 JavaScript 驗證程序中。您可以使用一個用戶注冊界面來讓用戶可以選擇用戶名。文本標簽可以進行增強來檢查 @Column(unique = true)
注釋。在找到這個注釋時,標簽可以添加一個用來觸發 Ajax 調用的類。
現在您不需要在應用程序層間維護重復的驗證邏輯了,這樣就可以節省出大量的開發時間。想像一下您最終可以為應用程序所能添加的增強功能!
下載
描述 |
名字 |
大小 |
下載方法 |
示例應用程序 |
j-hibval-source.zip |
8MB |
HTTP
|
參考資料
學習
獲得產品和技術
討論
關于作者
|
|
|
Ted Bergeron 是 Triview 的合作創始人之一,Triview 是一家企業軟件咨詢公司,位于加利福尼亞的圣地亞哥。Ted 從事基于 Web 的應用程序的設計已經有十 多年的時間了。他所做過的一些知名的項目包括為 Sybase、Orbitz、Disney 和 Qualcomm 所設計的項目。Ted 還曾用三 年的時間作為一名技術講師來教授有關 Web 開發、Java 開發和數據庫邏輯設計的課程。您可以在 Triview 的 Web 站點 上了解有關 Triview 公司的更多內容,或者也可以撥打該公司的免費電話 (866)TRIVIEW。
|
posted on 2006-10-16 14:19
xzc 閱讀(732)
評論(0) 編輯 收藏 所屬分類:
Hibernate