??????Map BuffaloContext.getContext.getSession()
??????Map BuffaloContext.getContext.getApplication()
??????Map BuffaloContext.getContext.getCookie()
注釋簡化了數(shù)據(jù)驗證 ![]() |
![]() |
![]() |
|
級別: 中級
Ted Bergeron
(ted@triview.com), 合作創(chuàng)始人, Triview, Inc.
2006 年 10 月 10 日
盡管在 Web 應用程序中盡可能多的層次中構(gòu)建數(shù)據(jù)驗證非常重要,但是這樣做卻非常耗時,以至于很多開發(fā)人員都會干脆忽略這個步驟 —— 這可能會導致今后大量問題的產(chǎn)生。但是隨著最新版本的 Java 平臺中引入了注釋,驗證變得簡單得多了。在本文中,Ted Bergeron 將向您介紹如何使用 Hibernate Annotations 的 Validator 組件在 Web 應用程序中輕松構(gòu)建并維護驗證邏輯。
有時會有一種工具,它可以真正滿足開發(fā)人員和架構(gòu)師的需求。開發(fā)人員在第一次下載這種工具當天就可以在自己的應用程序中開始使用這種工具。理論上來說,這種工具在開發(fā)人員花費大量時間來掌握其用法之前就可以從中獲益。架構(gòu)師也很喜歡這種工具,因為它可以將開發(fā)人員導向更高理論層次的實現(xiàn)。Hibernate Annotations 的 Validator 組件就是一種這樣的工具。
![]() |
|
Java SE 5 為 Java 語言提供了很多需要的增強功能,不過其他增強功能可能都不如 注釋 這樣潛力巨大。使用 注釋,我們就終于具有了一個標準、一級的元數(shù)據(jù)框架為 Java 類使用。Hibernate 用戶手工編寫 *.hbm.xml 文件已經(jīng)很多年了(或者使用 XDoclet 來自動實現(xiàn)這個任務(wù))。如果手工創(chuàng)建了 XML 文件,那就必須對每個所需要的持久屬性都更新這兩個文件(類定義和 XML 映射文檔)。使用 HibernateDoclet 可以簡化這個過程(請參看清單 1 給出的例子),但是這需要我們確認自己的 HibernateDoclet 版本支持要使用的 Hibernate 的版本。doclet 信息在運行時也是不可用的,因為它被編寫到了 Javadoc 風格的注釋中了。Hibernate Annotations,如圖 2 所示,通過提供一個標準、簡明的映射類的方法和所添加的運行時可用性來對這些方式進行改進。
/** * @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; } |
@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,那么在編輯時就可以檢測出部分錯誤。在從頭創(chuàng)建應用程序時,可以利用 hbm2ddl 工具為自己的數(shù)據(jù)庫從 hbm.xml 文件中生成 DDL。一些重要的信息 —— 比如name
屬性的最大長度必須是 60 個字符,或者 DDL 應該添加非空約束 —— 都被從 HibernateDoclet 項添加到 DDL 中。當使用注釋時,我們可以以類似的方式自動生成 DDL。
盡管這兩種代碼映射方式都可以使用,不過注釋的優(yōu)勢更為明顯。使用注釋,可以用一些常量來指定長度或其他值。編譯循環(huán)的速度更快,并且不需要生成 XML 文件。其中最大的優(yōu)勢是可以訪問一些有用信息,例如運行時的非空注釋或長度。除了清單 2 給出的注釋之外,還可以指定一些驗證的約束。所包含的部分約束如下:
@Max(value = 100)
@Min(value = 0)
@Past
@Future
@Email
在適當條件下,這些注釋會引起由 DDL 生成檢查約束。(顯然,@Future
并不是一個適當?shù)臈l件。)還可以根據(jù)需要創(chuàng)建定制約束注釋。
編寫驗證代碼是一個煩人且耗時的過程。通常,很多開發(fā)人員都會放棄在特定的層進行有效性驗證,從而可以節(jié)省一些時間;但是所節(jié)省的時間是否能夠彌補在這個地方因忽略部分功能所引起的缺陷卻非常值得探討。如果在所有應用程序?qū)又袆?chuàng)建并維護驗證所需要的時間可以極大地減少,那么爭論的焦點就會轉(zhuǎn)向是否要在多個層次中進行有效性驗證。假設(shè)有一個應用程序,它讓用戶使用一個用戶名、密碼和信用卡號來創(chuàng)建一個帳號。在這個應用程序中所希望進行驗證的組件如下:
SQLException
錯誤要好。進行驗證的一種典型方法是對簡單的驗證使用 Commons Validator,并在控制器中編寫其他一些驗證邏輯。Commons Validator 可以生成 JavaScript 來對視圖中的驗證進行處理。但是 Commons Validator 也有自己的缺陷:它只能處理簡單的驗證問題,并且將驗證的信息都保存到了 XML 文件中。Commons Validator 被設(shè)計用來與 Struts 一起使用,而且沒有提供一種簡單的方法在應用程序?qū)娱g重用驗證的聲明。
在規(guī)劃有效性驗證策略時,選擇在錯誤發(fā)生時簡單地處理這些錯誤是遠遠不夠的。一種良好的設(shè)計同時還要通過生成一個友好的用戶界面來防止出現(xiàn)錯誤。采用預先進行的方法進行驗證可以極大地增強用戶對于應用程序的理解。不幸的是,Commons Validator 并沒有對此提供支持。假設(shè)希望 HTML 文件設(shè)置文本域的 maxlength
屬性來與驗證匹配,或者在文本域之后放上一個百分號(%)來表示要輸入百分比的值。通常,這些信息都被硬編寫到 HTML 文檔中了。如果決定修改 name
屬性來支持 75 個字符,而不是 60 個字符,那么需要改動多少地方呢?在很多應用程序中,通常都需要:
maxlength
屬性。 更好的方法是使用 Hibernate Validator。驗證的定義都被通過注釋 添加到了模型層中,同時還有對所包含的驗證處理的支持。如果選擇充分利用所有的 Hibernate,這個 Validator 就可以在 DAO 和 DBMS 層也提供驗證。在下面給出的樣例代碼中,將使用 reflection 和 JSP 2.0 標簽文件多執(zhí)行一個步驟,從而充分利用注釋 為視圖層動態(tài)生成代碼。這可以清除在視圖中使用的硬編寫的業(yè)務(wù)邏輯。
在清單 3 中,dateOfBirth
被注釋為 NotNull
和過去的日期。 Hibernate 的 DDL 生成代碼對這個列添加了一個非空約束,以及一個要求日期必須是之前日期的檢查約束。e-mail 地址也是非空的,必須匹配 e-mail 地址的格式。這會生成一個非空約束,但是不會生成匹配這種格式的檢查約束。
/** * 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; } |
![]() |
|
如果需要,Hibernate DAO 實現(xiàn)也可以使用 Validation Annotations。所需做的是在 hibernate.cfg.xml 文件中指定基于 Hibernate 事件的驗證規(guī)則。(更多信息請參考 Hibernate Validator 的文檔;可以在 參考資料 一節(jié)中找到相關(guān)的鏈接)。如果真地希望抄近路,您可以只捕獲服務(wù)或控制器中的 InvalidStateException
異常,并循環(huán)遍歷 InvalidValue
數(shù)組。
要執(zhí)行驗證,需要創(chuàng)建一個 Hibernate 的 ClassValidator
實例。這個類進行實例化的代價可能會很高,因此最好只對希望進行驗證的每個類來進行實例化。一種方法是創(chuàng)建一個實用工具類,對每個模型對象存儲一個 ClassValidator
實例,如清單 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 中,創(chuàng)建了兩個 ClassValidator
,一個用于 Customer
,另外一個用于 CreditCard
。這兩個希望進行驗證的類可以調(diào)用 getInvalidValues(BaseObject modelObject)
,會返回 InvalidValue[]
。這則會返回一個包含模型對象實例錯誤的數(shù)組。另外,這個方法也可以通過提供一個特定的屬性名來調(diào)用,這樣做會只返回與該域有關(guān)的錯誤。
在使用 Spring MVC 和 Hibernate Validator 時,為信用卡創(chuàng)建一個驗證過程變得非常簡單,如清單 5 所示:
/** * 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
數(shù)組。如果發(fā)現(xiàn)了一個或多個這種簡單錯誤,那么就可以將 Hibernate 的 InvalidValue
數(shù)組轉(zhuǎn)換成 Spring 的 Errors
對象。如果用戶已經(jīng)創(chuàng)建了這個信用卡并且沒有出現(xiàn)任何簡單錯誤,就可以將更加徹底的驗證委托給服務(wù)層進行。這一層可以與商業(yè)服務(wù)提供者一起對信用卡進行驗證。
現(xiàn)在我們已經(jīng)看到這個簡單的模型層注釋是如何平衡到控制器、DAO 和 DBMS 層的驗證的。在 HibernateDoclet 和 Commons Validator 中發(fā)現(xiàn)的驗證邏輯的重合現(xiàn)在都已經(jīng)統(tǒng)一到模型中了。盡管這是一個非常受歡迎的改進,但是視圖層傳統(tǒng)上來說一直是最需要進行詳細驗證的地方。
![]() ![]() |
![]()
|
在下面的例子中,使用了 Spring MVC 和 JSP 2.0 標簽文件。JSP 2.0 允許在 TLD 文件中對定制函數(shù)進行注冊,并在一個標簽文件中進行調(diào)用。標簽文件類似于 taglibs,但是它們是使用 JSP 代碼編寫的,而不是使用 Java 代碼編寫的。采用這種方法,使用 Java 語言寫好的代碼就可以封裝成函數(shù),而使用 JSP 寫好的代碼則可以放入標簽文件中。在這種情況中,對注釋的處理需要使用映像,這會由幾個函數(shù)來執(zhí)行。綁定 Spring 或呈現(xiàn) XHTML 的代碼也是標簽文件的一部分。
清單 6 中節(jié)選的 TLD 代碼定義 text.tag 文件可以使用,并定義了一個名為 required
的函數(shù)。
<?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
類,其中包含了標簽文件使用的所有函數(shù)。在前文中我們曾經(jīng)說過,最適合使用 Java 代碼編寫的代碼都被放到了幾個 TLD 可以映射的函數(shù)中,這樣標簽文件就可以使用它們了;這些函數(shù)都是在 Utilities
類中進行編碼的。因此,我們需要三樣東西:定義這些類的 TLD 文件、Utilities
中的函數(shù),以及標簽文件本身,后者要使用這些函數(shù)。(第四樣應該是使用這個標簽文件的 JSP 頁面。)
在清單 7 中,給出了在 TLD 中引用的函數(shù)和另外一個表示給定屬性是否是 Date
的方法。在這個類中要涉及到比較多的代碼,但是本文限于篇幅,不會給出所有的代碼;不過需要注意 findGetterMethod()
除了將表達式語言(Expression Language,EL)方法表示(customer.contact
)轉(zhuǎn)換成 Java 表示(customer.getContact()
)之外,還執(zhí)行了基本的映像操作。
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 方法,并檢查這個方法是否有相關(guān)的給定的注釋 。
清單 8 中給出的 JSP 例子進行了簡化,這樣就可以著重查看相關(guān)的部分了。此處,這里有一個表單,它有一個選擇框和兩個輸入域。所有這些域都是通過在 form.tld 文件中聲明的標簽文件進行呈現(xiàn)的。標簽文件被設(shè)計成使用智能缺省值,這樣就可以根據(jù)需要允許簡單編碼的 JSP 可以有定義更多信息的選項。關(guān)鍵的屬性是 propertyPath
,它使用 EL 符號將這個域映射為模型層屬性,就像是使用 Spring MVC 的 bind
標簽一樣。
<%@ 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)鍵的部分:
<%@ 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
就應該被設(shè)置為 customer.contact.fax
。我們現(xiàn)在就使用 Spring 的 bind
標簽綁定到了包含屬性的對象上。這會將對象變量設(shè)置成對包含屬性的實例的引用。接下來,檢查這個標簽的用戶是否已經(jīng)指定他/她們是否希望屬性是必須的。允許表單開發(fā)人員覆蓋從注釋中返回的值是非常重要的,因為有時他/她們希望讓控制器為所需要的域設(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 標簽時使用 status.expression
和 status.value
。 input
標簽也可以使用一個大小 maxlength
以及適當?shù)念悂砩伞H绻懊嬉呀?jīng)確定屬性是一個 Date
,現(xiàn)在就可以添加 JavaScript 日歷了。(可以在 參考資料 一節(jié)找到一個很好的日歷組件的鏈接)。注意根據(jù)需要鏈接屬性、輸入 ID 和圖像 ID 的標簽是多么簡單。)這個 JavaScript 日歷需要一個圖像 ID 來匹配輸入域,其后綴是 _button
。
最后,可以將 <jsp:doBody/>
封裝到一個 span
標簽中,這樣允許表單開發(fā)人員在頁面中添加其他圖標,例如用來尋求幫助的圖標。(清單 8 給出了一個為信用卡號域添加的幫助圖標。)最后的部分是檢查 Spring 是否為這個屬性報告和顯示了一個錯誤,并和一個錯誤圖標一起顯示。
使用 CSS,就可以對必須的域進行一下裝飾 —— 例如,讓它們以紅色顯示、在文本邊上顯示一個星號,或者使用一個背景圖像來裝飾它。在清單 10 中,將必須的域的標簽設(shè)置成黑色,而且后面顯示一個紅色的星號(在 Firefox 以及其他標準兼容的瀏覽器中),如果是在 IE 中則還會在左邊加上一個小旗子的背景圖像:
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 日歷圖標。對所有的文本域設(shè)置正確的 maxlength
屬性可以防止用戶輸入太多文本所引起的錯誤。可以擴展 text
標簽來為輸入域類設(shè)置其他的數(shù)據(jù)類型。可以修改 text
標簽使用 HTML,而不是 XHTML(如果希望這樣)。可以不太費力地獲得具有正確語義的 HTML 表單,而且不需學習基于組件的框架知識,就可以利用基于組件的 Web 框架的優(yōu)點。
盡管標簽文件生成的 HTML 文件可以幫助防止一些錯誤的產(chǎn)生,但是在視圖層并沒有任何代碼來真正進行錯誤檢查。由于可以使用類屬性,現(xiàn)在就可以添加一些簡單的 JavaScript 來實現(xiàn)這種功能了,如清單 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);"
被調(diào)用的。這個腳本簡單地獲取具有所需要的類的表單中的所有元素。由于我們的習慣是在標簽標記中使用這個類,因此代碼會通過 for
屬性來查找與這個標簽連接在一起的輸入域。如果任何輸入域為空,就會生成一條簡單的警告消息,表單提交就會取消。可以簡單地對這個腳本進行擴充,使其掃描多個類,并相應地進行驗證。
對于基于 JavaScript 的綜合的驗證集合來說,最好是使用開源實現(xiàn),而不是自行開發(fā)。在清單 8 中您可能已經(jīng)注意到下面的代碼:
onclick="new Effect.SlideDown('creditCardHelp')" |
這個函數(shù)是 Script.aculo.us 庫的一部分,這個庫提供了很多高級的效果。如果正在使用 Script.aculo.us,就需要對所構(gòu)建的內(nèi)容使用 Prototype 庫。 JavaScript 驗證庫的一個例子是由 Andrew Tetlaw 在 Prototype 基礎(chǔ)上構(gòu)建的。(請參看 參考資料 一節(jié)中的鏈接。)他的框架依賴于被添加到輸入域的類:
<input class="required validate-number" id="field1" name="field1" /> |
可以簡單地修改 text.tag 的邏輯在 input
標簽中插入幾個類。將 class="required"
添加到輸入標簽和 label
標簽中不會影響 CSS 規(guī)則,但會破壞清單 10 中給出的簡單 JavaScript 驗證程序。如果要混合使用框架中的代碼和簡單的 JavaScript 代碼,最好使用不同的類名,或在使用類名搜索元素時確保類名有效并檢查標簽類型。
![]() ![]() |
![]()
|
本文已經(jīng)介紹了模型層的注釋如何充分用來在視圖、控制器、DAO 和 DBMS 層中創(chuàng)建驗證。必須手工創(chuàng)建服務(wù)層的驗證,例如信用卡驗證。其他模型層的驗證,例如強制屬性 C 是必須的,而屬性 A 和 B 都處于指定的狀態(tài),這也是一個手工任務(wù)。然而,使用 Hibernate Annotations 的 Validator 組件,就可以輕松地聲明并應用大多數(shù)驗證。
不論是簡單例子還是所引用框架的 JavaScript 驗證都可以對簡單的條件進行檢查,例如域必須要填寫,或者客戶機端代碼中的數(shù)據(jù)類型必須要匹配預期的類型。需要用到服務(wù)器端邏輯的驗證可以使用 Ajax 添加到 JavaScript 驗證程序中。您可以使用一個用戶注冊界面來讓用戶可以選擇用戶名。文本標簽可以進行增強來檢查 @Column(unique = true)
注釋。在找到這個注釋時,標簽可以添加一個用來觸發(fā) Ajax 調(diào)用的類。
現(xiàn)在您不需要在應用程序?qū)娱g維護重復的驗證邏輯了,這樣就可以節(jié)省出大量的開發(fā)時間。想像一下您最終可以為應用程序所能添加的增強功能!
![]() ![]() |
![]()
|
描述 | 名字 | 大小 | 下載方法 |
---|---|---|---|
示例應用程序 | j-hibval-source.zip | 8MB | HTTP |
![]() |
||||
![]() |
關(guān)于下載方法的信息 |
![]() |
![]() |
Get Adobe? Reader? |
![]() |
||
|
![]() |
Ted Bergeron 是 Triview 的合作創(chuàng)始人之一,Triview 是一家企業(yè)軟件咨詢公司,位于加利福尼亞的圣地亞哥。Ted 從事基于 Web 的應用程序的設(shè)計已經(jīng)有十 多年的時間了。他所做過的一些知名的項目包括為 Sybase、Orbitz、Disney 和 Qualcomm 所設(shè)計的項目。Ted 還曾用三 年的時間作為一名技術(shù)講師來教授有關(guān) Web 開發(fā)、Java 開發(fā)和數(shù)據(jù)庫邏輯設(shè)計的課程。您可以在 Triview 的 Web 站點 上了解有關(guān) Triview 公司的更多內(nèi)容,或者也可以撥打該公司的免費電話 (866)TRIVIEW。 |
?修改后的結(jié)果以post方式提交給/fck/servlet/ContextServlet,該url對應的即為ContextServlet。
ContextServlet負責讀取FCKeditor里的內(nèi)容,并賦予給session中的ContextBean。doPost()方法用于實現(xiàn)該功能
需要注意兩個問題,
其一:FCKeditor內(nèi)的中文信息讀取是可能出現(xiàn)亂碼,需要額外的處理:
?? String?name=new?String(request.getParameter("EditorDefault").getBytes("ISO-8859-1"),"UTF-8");
其二:由于servlet處于FacesContext范圍外,因此不能通過FacesContext.getCurrentInstance()來得到當前FacesContext,因此ContextServlet定義了自己的方法用于獲取FacesContext:
?? ContextServlet處理完了FCKeditor內(nèi)容后,將跳轉(zhuǎn)到form.jsp。
這樣一個簡單的編輯功能就完成了。
3.遺留問題:
?? 我在上傳文件時還是會出現(xiàn)中文亂碼的問題,按照其他人說的那樣把網(wǎng)頁的charset=utf-8改成gb2312一樣會有問題,還請各位高手賜教^_^
另外,關(guān)于整個demo的源代碼如果大家需要,可以留言給我,我用郵箱發(fā)給您。不足之處,還請各位多多指點
開發(fā)環(huán)境: FCKeditor 版本 FCKeditor_2.3.1 FCKeditor.Java 2.3 下載地址: http://www.fckeditor.net/download/default.html 開始: 解壓 FCKeditor_2.3.1 包中的 fckconfig.js、fckeditor.js、fckstyles.xml、fcktemplates.xml 文件夾到項目中的 WebContent 目錄 解壓 FCKeditor-2.3.zip 包中的 \web\WEB-INF\lib 下的兩個 jar 文件到項目的 WebContent\WEB-INF\lib 目錄 解壓 FCKeditor-2.3.zip 包中的 \src 下的 FCKeditor.tld 文件到項目的 WebContent\WEB-INF 目錄 刪除 WebContent\edit 目錄下的 _source 文件夾 修改 web.xml 文件,加入以下內(nèi)容
代碼
< servlet > 新建一個提交頁 jsp1.jsp 文件和一個接收頁 jsp2.jsp 文件 jsp1.jsp 代碼如下: 代碼 <%@ page contentType = "text/html;charset=UTF-8" language = "java" %> 在 WebContent 目錄下新建 UserFiles 文件夾,在此文件夾下新建 File,Image,Flash 三個文件夾。 ok現(xiàn)在運行一下看看吧! |
試用了一下FCKeditor,根據(jù)網(wǎng)上的文章小結(jié)一下: 2.建立項目:tomcat/webapps/TestFCKeditor. 3.將FCKeditor2.2解壓縮,將整個目錄FCKeditor復制到項目的根目錄下, 4.將FCKeditor-2.3.zip壓縮包中\(zhòng)web\WEB-INF\目錄下的web.xml文件合并到項目的\WEB-INF\目錄下的web.xml文件中。 |
5. 修改合并后的web.xml文件,將名為SimpleUploader的Servlet的enabled參數(shù)值改為true,
以允許上傳功能,Connector Servlet的baseDir參數(shù)值用于設(shè)置上傳文件存放的位置。
添加標簽定義:
<taglib>
<taglib-uri>/TestFCKeditor</taglib-uri>
<taglib-location>/WEB-INF/FCKeditor.tld</taglib-location>
</taglib>
運行圖:
6. 上面文件中兩個servlet的映射分別為:/editor/filemanager/browser/default/connectors/jsp/connector
和/editor/filemanager/upload/simpleuploader,需要在兩個映射前面加上/FCKeditor,
即改為/FCKeditor/editor/filemanager/browser/default/connectors/jsp/connector和
/FCKeditor/editor/filemanager/upload/simpleuploader。
7.進入skin文件夾,如果你想使用fckeditor默認的這種奶黃色,
那就把除了default文件夾外的另兩個文件夾直接刪除.
8.刪除/FCKeditor/目錄下除fckconfig.js, fckeditor.js, fckstyles.xml, fcktemplates.xml四個文件以外的所有文件
刪除目錄/editor/_source,
刪除/editor/filemanager/browser/default/connectors/下的所有文件
刪除/editor/filemanager/upload/下的所有文件
刪除/editor/lang/下的除了fcklanguagemanager.js, en.js, zh.js, zh-cn.js四個文件的所有文件
9.打開/FCKeditor/fckconfig.js
修改 FCKConfig.DefaultLanguage = 'zh-cn' ;
把FCKConfig.LinkBrowserURL等的值替換成以下內(nèi)容:
FCKConfig.LinkBrowserURL
= FCKConfig.BasePath + "filemanager/browser/default/browser.html?Connector=connectors/jsp/connector" ;
FCKConfig.ImageBrowserURL
= FCKConfig.BasePath + "filemanager/browser/default/browser.html?Type=Image&Connector=connectors/jsp/connector" ;
FCKConfig.FlashBrowserURL
= FCKConfig.BasePath + "filemanager/browser/default/browser.html?Type=Flash&Connector=connectors/jsp/connector" ;
FCKConfig.LinkUploadURL = FCKConfig.BasePath + 'filemanager/upload/simpleuploader?Type=File' ;
FCKConfig.FlashUploadURL = FCKConfig.BasePath + 'filemanager/upload/simpleuploader?Type=Flash' ;
FCKConfig.ImageUploadURL = FCKConfig.BasePath + 'filemanager/upload/simpleuploader?Type=Image' ;
10.其它
fckconfig.js總配置文件,可用記錄本打開,修改后將文件存為utf-8 編碼格式。找到:
FCKConfig.TabSpaces = 0 ; 改為FCKConfig.TabSpaces = 1 ; 即在編輯器域內(nèi)可以使用Tab鍵。
如果你的編輯器還用在網(wǎng)站前臺的話,比如說用于留言本或是日記回復時,那就不得不考慮安全了,
在前臺千萬不要使用Default的toolbar,要么自定義一下功能,要么就用系統(tǒng)已經(jīng)定義好的Basic,
也就是基本的toolbar,找到:
FCKConfig.ToolbarSets["Basic"] = [
['Bold','Italic','-','OrderedList','UnorderedList','-',/*'Link',*/'Unlink','-','Style','FontSize','TextColor','BGColor','-',
'Smiley','SpecialChar','Replace','Preview'] ] ;
這是改過的Basic,把圖像功能去掉,把添加鏈接功能去掉,因為圖像和鏈接和flash和圖像按鈕添加功能都能讓前臺
頁直接訪問和上傳文件, fckeditor還支持編輯域內(nèi)的鼠標右鍵功能。
FCKConfig.ContextMenu = ['Generic',/*'Link',*/'Anchor',/*'Image',*/'Flash','Select','Textarea','Checkbox','Radio','TextField','HiddenField',
/*'ImageButton',*/'Button','BulletedList','NumberedList','TableCell','Table','Form'] ;
這也是改過的把鼠標右鍵的“鏈接、圖像,F(xiàn)LASH,圖像按鈕”功能都去掉。
找到: FCKConfig.FontNames = 'Arial;Comic Sans MS;Courier New;Tahoma;Times New Roman;Verdana' ;
加上幾種我們常用的字體
FCKConfig.FontNames
= '宋體;黑體;隸書;楷體_GB2312;Arial;Comic Sans MS;Courier New;Tahoma;Times New Roman;Verdana' ;
7.添加文件 /TestFCKeditor/test.jsp:
<%@ page language="java" import="com.fredck.FCKeditor.*" %>
<%@ taglib uri="/TestFCKeditor" prefix="FCK" %>
<script type="text/javascript" src="/TestFCKeditor/FCKeditor/fckeditor.js"></script>
<%--
三種方法調(diào)用FCKeditor
1.FCKeditor自定義標簽 (必須加頭文件 <%@ taglib uri="/TestFCKeditor" prefix="FCK" %> )
2.script腳本語言調(diào)用 (必須引用 腳本文件 <script type="text/javascript" src="/TestFCKeditor/FCKeditor/fckeditor.js"></script> )
3.FCKeditor API 調(diào)用 (必須加頭文件 <%@ page language="java" import="com.fredck.FCKeditor.*" %> )
--%>
<%--
<form action="show.jsp" method="post" target="_blank">
<FCK:editor id="content" basePath="/TestFCKeditor/FCKeditor/"
width="700"
height="500"
skinPath="/TestFCKeditor/FCKeditor/editor/skins/silver/"
toolbarSet = "Default"
>
input
</FCK:editor>
<input type="submit" value="Submit">
</form>
--%>
<form action="show.jsp" method="post" target="_blank">
<table border="0" width="700"><tr><td>
<textarea id="content" name="content" style="WIDTH: 100%; HEIGHT: 400px">input</textarea>
<script type="text/javascript">
var oFCKeditor = new FCKeditor('content') ;
oFCKeditor.BasePath = "/TestFCKeditor/FCKeditor/" ;
oFCKeditor.Height = 400;
oFCKeditor.ToolbarSet = "Default" ;
oFCKeditor.ReplaceTextarea();
</script>
<input type="submit" value="Submit">
</td></tr></table>
</form>
<%--
<form action="show.jsp" method="post" target="_blank">
<%
FCKeditor oFCKeditor ;
oFCKeditor = new FCKeditor( request, "content" ) ;
oFCKeditor.setBasePath( "/TestFCKeditor/FCKeditor/" ) ;
oFCKeditor.setValue( "input" );
out.println( oFCKeditor.create() ) ;
%>
<br>
<input type="submit" value="Submit">
</form>
--%>
添加文件/TestFCKeditor/show.jsp:
<%
String content = request.getParameter("content");
out.print(content);
%>
8.瀏覽http://localhost:8080/TestFCKeditor/test.jsp
ok!
最近寫書,寫到JNDI,到處查資料,發(fā)現(xiàn)所有的中文資料都對JNDI解釋一通,配置代碼也是copy的,調(diào)了半天也沒調(diào)通,最后到SUN的網(wǎng)站參考了一下他的JNDI tutorial,終于基本上徹底明白了
和多數(shù)java服務(wù)一樣,SUN對JNDI也只提供接口,使用JNDI只需要用到JNDI接口而不必關(guān)心具體實現(xiàn):
private static Object jndiLookup() throws Exception {
InitialContext ctx = new InitialContext();
return ctx.lookup("java:comp/env/systemStartTime");
}
上述代碼在J2EE服務(wù)器環(huán)境下工作得很好,但是在main()中就會報一個NoInitialContextException,許多文章會說你創(chuàng)建InitialContext的時候還要傳一個Hashtable或者Properties,像這樣:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
env.put(Context.PROVIDER_URL,"t3://localhost:7001");
InitialContext ctx = new InitialContext(env);
這個在WebLogic環(huán)境下是對的,但是換到JBoss呢?再用JBoss的例子?
其實之所以有NoInitialContextException是因為無法從System.properties中獲得必要的JNDI參數(shù),在服務(wù)器環(huán)境下,服務(wù)器啟動時就把這些參數(shù)放到System.properties中了,于是直接new InitialContext()就搞定了,不要搞env那么麻煩,搞了env你的代碼還無法移植,弄不好管理員設(shè)置服務(wù)器用的不是標準端口還照樣拋異常。
但是在單機環(huán)境下,可沒有JNDI服務(wù)在運行,那就手動啟動一個JNDI服務(wù)。我在JDK 5的rt.jar中一共找到了4種SUN自帶的JNDI實現(xiàn):
LDAP,CORBA,RMI,DNS。
這4種JNDI要正常運行還需要底層的相應服務(wù)。一般我們沒有LDAP或CORBA服務(wù)器,也就無法啟動這兩種JNDI服務(wù),DNS用于查域名的,以后再研究,唯一可以在main()中啟動的就是基于RMI的JNDI服務(wù)。
現(xiàn)在我們就在main()中啟動基于RMI的JNDI服務(wù)并且綁一個Date對象到JNDI上:
LocateRegistry.createRegistry(1099);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext ctx = new InitialContext();
class RemoteDate extends Date implements Remote {};
ctx.bind("java:comp/env/systemStartTime", new RemoteDate());
ctx.close();
注意,我直接把JNDI的相關(guān)參數(shù)放入了System.properties中,這樣,后面的代碼如果要查JNDI,直接new InitialContext()就可以了,否則,你又得寫Hashtable env = ...
在RMI中綁JNDI的限制是,綁定的對象必須是Remote類型,所以就自己擴展一個。
其實JNDI還有兩個Context.SECURITY_PRINCIPAL和Context.SECURITY_CREDENTIAL,如果訪問JNDI需要用戶名和口令,這兩個也要提供,不過一般用不上。
在后面的代碼中查詢就簡單了:
InitialContext ctx = new InitialContext();
Date startTime = (Date) ctx.lookup("java:comp/env/systemStartTime");
在SUN的JNDI tutorial中的例子用的com.sun.jndi.fscontext.RefFSContextFactory類,但是我死活在JDK 5中沒有找到這個類,也就是NoClassDefFoundError,他也不說用的哪個擴展包,我也懶得找了。
try{
int age = 39;
String poetName = "dylan thomas";
CallableStatement proc = connection.prepareCall("{ call set_death_age(?, ?) }");
proc.setString(1, poetName);
proc.setInt(2, age);
cs.execute();
}catch (SQLException e){ // ....}
create procedure set_death_age(poet VARCHAR2, poet_age NUMBER)
poet_id NUMBER;
begin SELECT id INTO poet_id FROM poets WHERE name = poet;
INSERT INTO deaths (mort_id, age) VALUES (poet_id, poet_age);
end set_death_age;
public static void setDeathAge(Poet dyingBard, int age) throws SQLException{
Connection con = null;
CallableStatement proc = null;
try {
con = connectionPool.getConnection();
proc = con.prepareCall("{ call set_death_age(?, ?) }");
proc.setString(1, dyingBard.getName());
proc.setInt(2, age);
proc.execute();
}
finally {
try { proc.close(); }
catch (SQLException e) {}
con.close();
}
}
create function snuffed_it_when (VARCHAR) returns integer ''declare
poet_id NUMBER;
poet_age NUMBER;
begin
--first get the id associated with the poet.
SELECT id INTO poet_id FROM poets WHERE name = $1;
--get and return the age.
SELECT age INTO poet_age FROM deaths WHERE mort_id = poet_id;
return age;
end;'' language ''pl/pgsql'';
connection.setAutoCommit(false);
CallableStatement proc = connection.prepareCall("{ ? = call snuffed_it_when(?) }");
proc.registerOutParameter(1, Types.INTEGER);
proc.setString(2, poetName);
cs.execute();
int age = proc.getInt(2);
create procedure list_early_deaths () return refcursor as ''declare
toesup refcursor;
begin
open toesup for SELECT poets.name, deaths.age FROM poets, deaths -- all entries in deaths are for poets. -- but the table might become generic.
WHERE poets.id = deaths.mort_id AND deaths.age < 60;
return toesup;
end;'' language ''plpgsql'';
PrintWriter:
static void sendEarlyDeaths(PrintWriter out){
Connection con = null;
CallableStatement toesUp = null;
try {
con = ConnectionPool.getConnection();
// PostgreSQL needs a transaction to do this... con.
setAutoCommit(false); // Setup the call.
CallableStatement toesUp = connection.prepareCall("{ ? = call list_early_deaths () }");
toesUp.registerOutParameter(1, Types.OTHER);
toesUp.execute();
ResultSet rs = (ResultSet) toesUp.getObject(1);
while (rs.next()) {
String name = rs.getString(1);
int age = rs.getInt(2);
out.println(name + " was " + age + " years old.");
}
rs.close();
}
catch (SQLException e) { // We should protect these calls. toesUp.close(); con.close();
}
}
public class ProcessPoetDeaths{
public abstract void sendDeath(String name, int age);
}
static void mapEarlyDeaths(ProcessPoetDeaths mapper){
Connection con = null;
CallableStatement toesUp = null;
try {
con = ConnectionPool.getConnection();
con.setAutoCommit(false);
CallableStatement toesUp = connection.prepareCall("{ ? = call list_early_deaths () }");
toesUp.registerOutParameter(1, Types.OTHER);
toesUp.execute();
ResultSet rs = (ResultSet) toesUp.getObject(1);
while (rs.next()) {
String name = rs.getString(1);
int age = rs.getInt(2);
mapper.sendDeath(name, age);
}
rs.close();
} catch (SQLException e) { // We should protect these calls. toesUp.close();
con.close();
}
}
static void sendEarlyDeaths(final PrintWriter out){
ProcessPoetDeaths myMapper = new ProcessPoetDeaths() {
public void sendDeath(String name, int age) {
out.println(name + " was " + age + " years old.");
}
};
mapEarlyDeaths(myMapper);
}
近日學習了一下AJAX,照做了幾個例子,感覺比較新奇。
第一個就是自動完成的功能即Autocomplete,具體的例子可以在這里看:http://www.b2c-battery.co.uk
在Search框內(nèi)輸入一個產(chǎn)品型號,就可以看見效果了。
這里用到了一個開源的代碼:AutoAssist ,有興趣的可以看一下。
以下為代碼片斷:
index.htm
<script type="text/javascript" src="javascripts/prototype.js"></script>
<script type="text/javascript" src="javascripts/autoassist.js"></script>
<link rel="stylesheet" type="text/css" href="styles/autoassist.css"/>
<div>
<input type="text" name="keyword" id="keyword"/>
<script type="text/javascript">
Event.observe(window, "load", function() {
?var aa = new AutoAssist("keyword", function() {
??return "forCSV.php?q=" + this.txtBox.value;
?});
});
</script>
</div>
不知道為什么不能用keywords做文本框的名字,我試了很久,后來還是用keyword,搞得還要修改原代碼。
forCSV.php
<?php
? $keyword = $_GET['q'];
? $count = 0;
? $handle = fopen("products.csv", "r");
? while (($data = fgetcsv($handle, 1000)) !== FALSE) {
??? if (preg_match("/$keyword/i", $data[0])) {
????? if ($count++ > 10) { break; }
?>
????? <div onSelect="this.txtBox.value='<?php echo $data[0]; ?>';">
??????? <?php echo $data[0]; ?>
????? </div>
<?php
??? }
? }
? fclose($handle);
? if ($count == 0) {
?>
? : (, nothing found.
<?php
? }
?>
原來的例子中的CSV文件是根據(jù)\t來分隔的,我們也可以用空格或其它的來分隔,這取決于你的數(shù)據(jù)結(jié)構(gòu)。
當然你也可以不讀文件,改從數(shù)據(jù)庫里讀資料,就不再廢話了。
效果圖如下:
?
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=635858