Hibernate 能够满足我们的验证需求
尽管在Web应用程序中尽可能多的层次中构建数据验证非常重要,但是这样做却非常耗时,以至于很多开发人员都会干脆忽略这个步骤——这可能会导致今后大量问题的产生。但是随着最新版本的Java平台中引入了注释,验证变得简单得多了。在本文中,TedBergeron将向您介绍如何使用HibernateAnnotations的Validator组件在Web应用程序中轻松构建并维护验证逻辑。
有时会有一种工具,它可以真正满足开发人员和架构师的需求。开发人员在第一次下载这种工具当天就可以在自己的应用程序中开始使用这种工具。理论上来说,这种工具在开发人员花费大量时间来掌握其用法之前就可以从中获益。架构师也很喜欢这种工具,因为它可以将开发人员导向更高理论层次的实现。HibernateAnnotations的Validator组件就是一种这样的工具。
开始之前需要了解的内容
在阅读本文之前,应该对Java平台版本5(尤其是注释)、JSP2.0(因为本文中创建了一些标签文件,并在TLD中定义了一些函数,它们都是JSP2.0的新特性)和Hibernate及Spring框架有一个基本的了解。请注意即使不使用Hibernate来实现持久性,也可以在自己的应用程序中使用HibernateValidator。
JavaSE5为Java语言提供了很多需要的增强功能,不过其他增强功能可能都不如注释这样潜力巨大。使用注释,我们就终于具有了一个标准、一级的元数据框架为Java类使用。Hibernate用户手工编写*.hbm.xml文件已经很多年了(或者使用XDoclet来自动实现这个任务)。如果手工创建了XML文件,那就必须对每个所需要的持久属性都更新这两个文件(类定义和XML映射文档)。使用HibernateDoclet可以简化这个过程(请参看清单1给出的例子),但是这需要我们确认自己的HibernateDoclet版本支持要使用的Hibernate的版本。doclet信息在运行时也是不可用的,因为它被编写到了Javadoc风格的注释中了。HibernateAnnotations,如图2所示,通过提供一个标准、简明的映射类的方法和所添加的运行时可用性来对这些方式进行改进。
清单1.使用HibernateDoclet的Hibernate映射代码
/**
*@hibernate.propertycolumn="NAME"length="60"not-null="true"
*/
publicStringgetName(){
returnthis.name;
}
/**
*@hibernate.many-to-onecolumn="AGENT_ID"not-null="true"cascade="none"
*outer-join="false"lazy="true"
*/
publicAgentgetAgent(){
returnagent;
}
/**
*@hibernate.setlazy="true"inverse="true"cascade="all"table="DEPARTMENT"
*@hibernate.collection-one-to-manyclass="com.triview.model.Department"
*@hibernate.collection-keycolumn="DEPARTMENT_ID"not-null="true"
*/
publicList<Department>getDepartment(){
returndepartment;
}
清单2.使用HibernateAnnotations的Hibernate映射代码
@NotNull
@Column(name="name")
@Length(min=1,max=NAME_LENGTH)//NAME_LENGTHisaconstantdeclaredelsewhere
publicStringgetName(){
returnname;
}
@NotNull
@ManyToOne(cascade={CascadeType.MERGE},fetch=FetchType.LAZY)
@JoinColumn(name="agent_id")
publicAgentgetAgent(){
returnagent;
}
@OneToMany(mappedBy="customer",fetch=FetchType.LAZY)
publicList<Department>getDepartment(){
returndepartment;
}
如果使用HibernateDoclet,那么直到生成XML文件或运行时才能捕获错误。使用注释,在编译时就可以检测出很多错误;或者如果在编辑时使用了很好的IDE,那么在编辑时就可以检测出部分错误。在从头创建应用程序时,可以利用hbm2ddl工具为自己的数据库从hbm.xml文件中生成DDL。一些重要的信息——比如name属性的最大长度必须是60个字符,或者DDL应该添加非空约束——都被从HibernateDoclet项添加到DDL中。当使用注释时,我们可以以类似的方式自动生成DDL。
尽管这两种代码映射方式都可以使用,不过注释的优势更为明显。使用注释,可以用一些常量来指定长度或其他值。编译循环的速度更快,并且不需要生成XML文件。其中最大的优势是可以访问一些有用信息,例如运行时的非空注释或长度。除了清单2给出的注释之外,还可以指定一些验证的约束。所包含的部分约束如下:
@Max(value=100)
@Min(value=0)
@Past
@Future
在适当条件下,这些注释会引起由DDL生成检查约束。(显然,@Future并不是一个适当的条件。)还可以根据需要创建定制约束注释。
验证和应用程序层
编写验证代码是一个烦人且耗时的过程。通常,很多开发人员都会放弃在特定的层进行有效性验证,从而可以节省一些时间;但是所节省的时间是否能够弥补在这个地方因忽略部分功能所引起的缺陷却非常值得探讨。如果在所有应用程序层中创建并维护验证所需要的时间可以极大地减少,那么争论的焦点就会转向是否要在多个层次中进行有效性验证。假设有一个应用程序,它让用户使用一个用户名、密码和信用卡号来创建一个帐号。在这个应用程序中所希望进行验证的组件如下:
视图:通过JavaScript进行验证可以避免与服务器反复进行交互,这样可以提供更好的用户体验。用户可以禁用JavaScript,因此这个层次的验证最好要有,但是却并不可靠。对所需要的域进行简单的验证是必须的。
控制器:验证必须在服务器端的逻辑中进行处理。这个层次中的代码可以以适合某个特定用途的方式处理验证。例如,在添加新用户时,控制器可以在进行处理之前检查指定的用户名是否已经存在。
服务:相对复杂的业务逻辑验证通常都最适合放到服务层中。例如,一旦有一个信用卡对象看起来有效,就应该使用信用卡处理服务对这个信用卡的信息进行确认。
DAO:在数据到达这个层次时,应该已经是有效的了。尽管如此,执行一次快速检查从而确保所需要的域都非空并且值也都在特定的范围或遵循特定的格式(例如e-mail地址域就应该包含一个有效的e-mail地址)也是非常有益的。在此处捕获错误总比产生可以避免的SQLException错误要好。
DBMS:这是通常可以忽略验证的地方。即使当前正在构建的应用程序是数据库的惟一客户机,将来还可能会添加其他客户机。如果应用程序有一些bug(大部分应用程序都可能会有bug),那么无效的数据也可能会被发送给数据库。在这种情况中,如果走运,就可以找到无效的数据,并且需要分析这些数据是否可以清除,以及如何清除。
模型:这是进行验证的一个理想地方,它不需要访问外部服务,也不需要了解持久性数据。例如,某业务逻辑可能会要求用户至少提供一个联系信息,这可以是一个电话号码也可以是一个e-mail地址;可以使用模型层的验证来确保用户的确提供了这种信息。
进行验证的一种典型方法是对简单的验证使用CommonsValidator,并在控制器中编写其他一些验证逻辑。CommonsValidator可以生成JavaScript来对视图中的验证进行处理。但是CommonsValidator也有自己的缺陷:它只能处理简单的验证问题,并且将验证的信息都保存到了XML文件中。CommonsValidator被设计用来与Struts一起使用,而且没有提供一种简单的方法在应用程序层间重用验证的声明。
在规划有效性验证策略时,选择在错误发生时简单地处理这些错误是远远不够的。一种良好的设计同时还要通过生成一个友好的用户界面来防止出现错误。采用预先进行的方法进行验证可以极大地增强用户对于应用程序的理解。不幸的是,CommonsValidator并没有对此提供支持。假设希望HTML文件设置文本域的maxlength属性来与验证匹配,或者在文本域之后放上一个百分号(%)来表示要输入百分比的值。通常,这些信息都被硬编写到HTML文档中了。如果决定修改name属性来支持75个字符,而不是60个字符,那么需要改动多少地方呢?在很多应用程序中,通常都需要:
更新DDL来增大数据库列的长度(通过HibernateDoclet、hbm.xml或HibernateAnnotations)。
更新CommonsValidatorXML文件将最大值增加到75。
更新所有与这个域有关的HTML表单,以修改maxlength属性。
更好的方法是使用HibernateValidator。验证的定义都被通过注释添加到了模型层中,同时还有对所包含的验证处理的支持。如果选择充分利用所有的Hibernate,这个Validator就可以在DAO和DBMS层也提供验证。在下面给出的样例代码中,将使用reflection和JSP2.0标签文件多执行一个步骤,从而充分利用注释为视图层动态生成代码。这可以清除在视图中使用的硬编写的业务逻辑。
在清单3中,dateOfBirth被注释为NotNull和过去的日期。Hibernate的DDL生成代码对这个列添加了一个非空约束,以及一个要求日期必须是之前日期的检查约束。e-mail地址也是非空的,必须匹配e-mail地址的格式。这会生成一个非空约束,但是不会生成匹配这种格式的检查约束。
清单3.通过HibernateAnnotations进行映射的简单联系方式
/**
*ASimplifiedobjectthatstorescontactinformation.
*
*@authorTedBergeron
*@version$Id:Contact.java,v1.12006/04/2403:39:34tedExp$
*/
@MappedSuperclass
@Embeddable
publicclassContactimplementsSerializable{
publicstaticfinalintMAX_FIRST_NAME=30;
publicstaticfinalintMAX_MIDDLE_NAME=1;
publicstaticfinalintMAX_LAST_NAME=30;
privateStringfname;
privateStringmi;
privateStringlname;
privateDatedateOfBirth;
privateStringemailAddress;
privateAddressaddress;
publicContact(){
this.address=newAddress();
}
@Valid
@Embedded
publicAddressgetAddress(){
returnaddress;
}
publicvoidsetAddress(Addressa){
if(a==null){
address=newAddress();
}else{
address=a;
}
}
@NotNull
@Length(min=1,max=MAX_FIRST_NAME)
@Column(name="fname")
publicStringgetFirstname(){
returnfname;
}
publicvoidsetFirstname(Stringfname){
this.fname=fname;
}
@Length(min=1,max=MAX_MIDDLE_NAME)
@Column(name="mi")
publicStringgetMi(){
returnmi;
}
publicvoidsetMi(Stringmi){
this.mi=mi;
}
@NotNull
@Length(min=1,max=MAX_LAST_NAME)
@Column(name="lname")
publicStringgetLastname(){
returnlname;
}
publicvoidsetLastname(Stringlname){
this.lname=lname;
}
@NotNull
@Past
@Column(name="dob")
publicDategetDateOfBirth(){
returndateOfBirth;
}
publicvoidsetDateOfBirth(DatedateOfBirth){
this.dateOfBirth=dateOfBirth;
}
@NotNull
@Column(name="email")
publicStringgetEmailAddress(){
returnemailAddress;
}
publicvoidsetEmailAddress(StringemailAddress){
this.emailAddress=emailAddress;
}
样例应用程序
在下载一节,您可以下载一个样例应用程序,它展示了本文中采用的设计思想和代码。由于这是一个可以工作的应用程序,因此代码比本文中讨论的的内容更为复杂。例如,清单9就节选于标签文件text.tag;这个样例应用程序具有标签文件使用的所有代码,以及其他三个类似的标签文件使用的代码(用于选择、隐藏和检查框的HTML元素)。由于这是一个可以工作的应用程序,它包含了一个在这种类型的应用程序中都可以找到的架构。还有一个Ant构建文件、Spring和HibernateXML封装代码,以及log4j配置。虽然这些都不是本文介绍的重点,但是您会发现仔细研究一下这个样例应用程序的源代码是非常有用的。
如果需要,HibernateDAO实现也可以使用ValidationAnnotations。所需做的是在hibernate.cfg.xml文件中指定基于Hibernate事件的验证规则。(更多信息请参考HibernateValidator的文档;可以在参考资料一节中找到相关的链接)。如果真地希望抄近路,您可以只捕获服务或控制器中的InvalidStateException异常,并循环遍历InvalidValue数组。
对控制器添加验证
要执行验证,需要创建一个Hibernate的ClassValidator实例。这个类进行实例化的代价可能会很高,因此最好只对希望进行验证的每个类来进行实例化。一种方法是创建一个实用工具类,对每个模型对象存储一个ClassValidator实例,如清单4所示:
清单4.处理验证的实用工具类
/**
*HandlesvalidationsbasedontheHibernateAnnotationsValidatorframework.
*@authorTedBergeron
*@version$Id:AnnotationValidator.java,v1.52006/01/2017:34:09tedExp$
*/
publicclassAnnotationValidator{
privatestaticLoglog=LogFactory.getLog(AnnotationValidator.class);
//Itisconsideredagoodpracticetoexecutetheselinesonceand
//cachethevalidatorinstances.
publicstaticfinalClassValidator<Customer>CUSTOMER_VALIDATOR=
newClassValidator<Customer>(Customer.class);
publicstaticfinalClassValidator<CreditCard>CREDIT_CARD_VALIDATOR=
newClassValidator<CreditCard>(CreditCard.class);
privatestaticClassValidator<?extendsBaseObject>getValidator(Class<?
extendsBaseObject>clazz){
if(Customer.class.equals(clazz)){
returnCUSTOMER_VALIDATOR;
}elseif(CreditCard.class.equals(clazz)){
returnCREDIT_CARD_VALIDATOR;
}else{
thrownewIllegalArgumentException("Unsupportedclasswaspassed.");
}
}
publicstaticInvalidValue[]getInvalidValues(BaseObjectmodelObject){
StringnullProperty=null;
returngetInvalidValues(modelObject,nullProperty);
}
publicstaticInvalidValue[]getInvalidValues(BaseObjectmodelObject,
Stringproperty){
Class<?extendsBaseObject>clazz=modelObject.getClass();
ClassValidatorvalidator=getValidator(clazz);
InvalidValue[]validationMessages;
if(property==null){
validationMessages=validator.getInvalidValues(modelObject);
}else{
//onlygetinvalidvaluesforspecifiedproperty.
//Forexample,"city"appliestogetCity()method.
validationMessages=validator.getInvalidValues(modelObject,property);
}
returnvalidationMessages;
}
}
在清单4中,创建了两个ClassValidator,一个用于Customer,另外一个用于CreditCard。这两个希望进行验证的类可以调用getInvalidValues(BaseObjectmodelObject),会返回InvalidValue[]。这则会返回一个包含模型对象实例错误的数组。另外,这个方法也可以通过提供一个特定的属性名来调用,这样做会只返回与该域有关的错误。
在使用SpringMVC和HibernateValidator时,为信用卡创建一个验证过程变得非常简单,如清单5所示:
清单5.SpringMVC控制器使用的CreditCardValidator
/**
*PerformsvalidationofaCreditCardinSpringMVC.
*
*@authorTedBergeron
*@version$Id:CreditCardValidator.java,v1.22006/02/1021:53:50tedExp$
*/
publicclassCreditCardValidatorimplementsValidator{
privateCreditCardServicecreditCardService;
publicvoidsetCreditCardService(CreditCardServiceservice){
this.creditCardService=service;
}
publicbooleansupports(Classclazz){
returnCreditCard.class.isAssignableFrom(clazz);
}
publicvoidvalidate(Objectobject,Errorserrors){
CreditCardcreditCard=(CreditCard)object;
InvalidValue[]invalids=AnnotationValidator.getInvalidValues(creditCard);
//Perform"expensive"validationonlyifnosimpleerrorsfoundabove.
if(invalids==null||invalids.length==0){
booleanvalidCard=creditCardService.validateCreditCard(creditCard);
if(!validCard){
errors.reject("error.creditcard.invalid");
}
}else{
for(InvalidValueinvalidValue:invalids){
errors.rejectValue(invalidValue.getPropertyPath(),
null,invalidValue.getMessage());
}
}
}
}
validate()方法只需要将creditCard实例传递给这个验证过程,从而返回InvalidValue数组。如果发现了一个或多个这种简单错误,那么就可以将Hibernate的InvalidValue数组转换成Spring的Errors对象。如果用户已经创建了这个信用卡并且没有出现任何简单错误,就可以将更加彻底的验证委托给服务层进行。这一层可以与商业服务提供者一起对信用卡进行验证。
现在我们已经看到这个简单的模型层注释是如何平衡到控制器、DAO和DBMS层的验证的。在HibernateDoclet和CommonsValidator中发现的验证逻辑的重合现在都已经统一到模型中了。尽管这是一个非常受欢迎的改进,但是视图层传统上来说一直是最需要进行详细验证的地方。
为视图添加验证
在下面的例子中,使用了SpringMVC和JSP2.0标签文件。JSP2.0允许在TLD文件中对定制函数进行注册,并在一个标签文件中进行调用。标签文件类似于taglibs,但是它们是使用JSP代码编写的,而不是使用Java代码编写的。采用这种方法,使用Java语言写好的代码就可以封装成函数,而使用JSP写好的代码则可以放入标签文件中。在这种情况中,对注释的处理需要使用映像,这会由几个函数来执行。绑定Spring或呈现XHTML的代码也是标签文件的一部分。
清单6中节选的TLD代码定义text.tag文件可以使用,并定义了一个名为required的函数。
清单6.创建表单TLD
<?xmlversion="1.0"encoding="ISO-8859-1"?>
<taglibxmlns="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>determineiffieldisrequiredfromAnnotations</description>
<name>required</name>
<function-class>com.triview.web.Utilities</function-class>
<function-signature>Booleanrequired(java.lang.Object,java.lang.String)
</function-signature>
</function>
</taglib>
清单7节选自Utilities类,其中包含了标签文件使用的所有函数。在前文中我们曾经说过,最适合使用Java代码编写的代码都被放到了几个TLD可以映射的函数中,这样标签文件就可以使用它们了;这些函数都是在Utilities类中进行编码的。因此,我们需要三样东西:定义这些类的TLD文件、Utilities中的函数,以及标签文件本身,后者要使用这些函数。(第四样应该是使用这个标签文件的JSP页面。)
在清单7中,给出了在TLD中引用的函数和另外一个表示给定属性是否是Date的方法。在这个类中要涉及到比较多的代码,但是本文限于篇幅,不会给出所有的代码;不过需要注意findGetterMethod()除了将表达式语言(ExpressionLanguage,EL)方法表示(customer.contact)转换成Java表示(customer.getContact())之外,还执行了基本的映像操作。
清单7.Utilities节选
publicstaticBooleanrequired(Objectobject,StringpropertyPath){
MethodgetMethod=findGetterMethod(object,propertyPath);
if(getMethod==null){
returnnull;
}else{
returngetMethod.isAnnotationPresent(NotNull.class);
}
}
publicstaticBooleanisDate(Objectobject,StringpropertyPath){
returnjava.util.Date.class.equals(getReturnType(object,propertyPath));
}
publicstaticClassgetReturnType(Objectobject,StringpropertyPath){
MethodgetMethod=findGetterMethod(object,propertyPath);
if(getMethod==null){
returnnull;
}else{
returngetMethod.getReturnType();
}
}
此处可以清楚地看到在运行时使用Validationannotations是多么容易。可以简单地引用对象的getter方法,并检查这个方法是否有相关的给定的注释。
清单8中给出的JSP例子进行了简化,这样就可以着重查看相关的部分了。此处,这里有一个表单,它有一个选择框和两个输入域。所有这些域都是通过在form.tld文件中声明的标签文件进行呈现的。标签文件被设计成使用智能缺省值,这样就可以根据需要允许简单编码的JSP可以有定义更多信息的选项。关键的属性是propertyPath,它使用EL符号将这个域映射为模型层属性,就像是使用SpringMVC的bind标签一样。
清单8.一个包含表单的简单JSP页面
<%@taglibtagdir="/WEB-INF/tags/form"prefix="form"%>
<formmethod="post"action="<c:urlvalue="/signup/customer.edit"/>">
<form:selectpropertyPath="creditCard.type"collection="${creditCardTypeCollection}"
required="true"labelKey="prompt.creditcard.type"/>
<form:textpropertyPath="creditCard.number"labelKey="prompt.creditcard.number">
<imgsrc="<c:urlvalue="/images/icons/help.png"/>"alt="Help"
onclick="newEffect.SlideDown('creditCardHelp')"/>
</form:text>
<form:textpropertyPath="creditCard.expirationDate"/>
</form>
text.tag文件的完整源代码太大了,不好放在这儿,因此清单9给出了其中关键的部分:
清单9.标签文件text.tag节选
<%@attributename="propertyPath"required="true"%>
<%@attributename="size"required="false"type="java.lang.Integer"%>
<%@attributename="maxlength"required="false"type="java.lang.Integer"%>
<%@attributename="required"required="false"type="java.lang.Boolean"%>
<%@tagliburi="http://www.springframework.org/tags"prefix="spring"%>
<%@tagliburi="formtags"prefix="form"%>
<c:setvar="objectPath"value="${form:getObjectPath(propertyPath)}"/>
<spring:bindpath="${objectPath}">
<c:setvar="object"value="${status.value}"/>
<c:iftest="${object==null}">
<%--Bindignoresthecommandobjectprefix,sosimplepropertiesofthecommandobject
returnnullabove.--%>
<c:setvar="object"value="${commandObject}"/>
<%--Wedependonthecontrolleraddingthistorequest.--%>
</c:if>
</spring:bind>
<%--Ifuserdidnotspecifywhetherthisfieldisrequired,
querytheobjectforthisinfo.--%>
<c:iftest="${required==null}">
<c:setvar="required"value="${form:required(object,propertyPath)}"/>
</c:if>
<c:choose>
<c:whentest="${required==null||required==false}">
<c:setvar="labelClass"value="optional"/>
</c:when>
<c:otherwise>
<c:setvar="labelClass"value="required"/>
</c:otherwise>
</c:choose>
<c:iftest="${maxlength==null}">
<c:setvar="maxlength"value="${form:maxLength(object,propertyPath)}"/>
</c:if>
<c:setvar="isDate"value="${form:isDate(object,propertyPath)}"/>
<c:setvar="cssClass"value="input_text"/>
<c:iftest="${isDate}">
<c:setvar="cssClass"value="input_date"/>
</c:if>
<divclass="field">
<spring:bindpath="${propertyPath}">
<labelfor="${status.expression}"class="${labelClass}"><fmt:message
key="prompt.${propertyPath}"/></label>
<inputtype="text"name="${status.expression}"value="${status.value}"
id="${status.expression}"<c:iftest="${size!=null}">size="${size}"</c:if>
<c:iftest="${maxlength!=null}">maxlength="${maxlength}"</c:if>
class="${cssClass}"/>
<c:iftest="${isDate}">
<imgid="${status.expression}_button"
src="<c:urlvalue="/images/icons/calendar.png"/>"alt="calendar"
style="cursor:pointer;"/>
<scripttype="text/javascript">
Calendar.setup(
{
inputField:"${status.expression}",//IDoftheinputfield
ifFormat:"%m/%d/%Y",//thedateformat
button:"${status.expression}_button"//IDofthebutton
}
);
</script>
</c:if>
<spanclass="icons"><jsp:doBody/></span>
<c:iftest="${status.errorMessage!=null&&status.errorMessage!=''}">
<pclass="fieldError"><imgid="${status.expression}_error"
src="<c:urlvalue="/images/icons/error.png"/>"
alt="error"/>${status.errorMessage}</p>
</c:if>
</spring:bind>
</div>
我们马上就可以看出propertyPath是惟一需要的属性。size、maxlength和required都可以忽略。objectPathvar被设置为在propertyPath中引用的属性的父对象。因此,如果propertyPath是customer.contact.fax.number,那么objectPath就应该被设置为customer.contact.fax。我们现在就使用Spring的bind标签绑定到了包含属性的对象上。这会将对象变量设置成对包含属性的实例的引用。接下来,检查这个标签的用户是否已经指定他/她们是否希望属性是必须的。允许表单开发人员覆盖从注释中返回的值是非常重要的,因为有时他/她们希望让控制器为所需要的域设置缺省值,而用户可能并不希望为这个域提供值。如果表单开发人员没有为required指定值,那么就可以调用这个表单TLD的required函数。这个函数调用了在TLD文件中映射的方法。这个方法简单地检查@NotNull注释;如果它发现某个属性具有这个注释,就将labelClass变量设置为必须的。可以类似地确定正确的maxlength以及这个域是否是一个Date。
接下来使用Spring来绑定到propertyPath上,而不是像前面一样只绑定到包含这个属性的对象上。这允许在生成label和inputHTML标签时使用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验证程序
<scripttype="text/javascript">
functioncheckRequired(form){
varrequiredLabels=document.getElementsByClassName("required",form);
for(i=0;i<requiredLabels.length;i++){
varlabelText=requiredLabels[i].firstChild.nodeValue;//Getthelabel'stext
varlabelFor=requiredLabels[i].getAttribute("for");//Grabtheforattribute
varinputTag=document.getElementById(labelFor);//Gettheinputtag
if(inputTag.value==null||inputTag.value==""){
alert("Pleasemakesureallrequiredfieldshavebeenentered.");
returnfalse;//AbortSubmit
}
}
returntrue;
}
</script>
这个JavaScript是通过为表单声明添加onsubmit="returncheckRequired(this);"被调用的。这个脚本简单地获取具有所需要的类的表单中的所有元素。由于我们的习惯是在标签标记中使用这个类,因此代码会通过for属性来查找与这个标签连接在一起的输入域。如果任何输入域为空,就会生成一条简单的警告消息,表单提交就会取消。可以简单地对这个脚本进行扩充,使其扫描多个类,并相应地进行验证。
对于基于JavaScript的综合的验证集合来说,最好是使用开源实现,而不是自行开发。在清单8中您可能已经注意到下面的代码:
onclick="newEffect.SlideDown('creditCardHelp')"
这个函数是Script.aculo.us库的一部分,这个库提供了很多高级的效果。如果正在使用Script.aculo.us,就需要对所构建的内容使用Prototype库。JavaScript验证库的一个例子是由AndrewTetlaw在Prototype基础上构建的。(请参看参考资料一节中的链接。)他的框架依赖于被添加到输入域的类:
<inputclass="requiredvalidate-number"id="field1"name="field1"/>
可以简单地修改text.tag的逻辑在input标签中插入几个类。将class="required"添加到输入标签和label标签中不会影响CSS规则,但会破坏清单10中给出的简单JavaScript验证程序。如果要混合使用框架中的代码和简单的JavaScript代码,最好使用不同的类名,或在使用类名搜索元素时确保类名有效并检查标签类型。
最后的考虑
本文已经介绍了模型层的注释如何充分用来在视图、控制器、DAO和DBMS层中创建验证。必须手工创建服务层的验证,例如信用卡验证。其他模型层的验证,例如强制属性C是必须的,而属性A和B都处于指定的状态,这也是一个手工任务。然而,使用HibernateAnnotations的Validator组件,就可以轻松地声明并应用大多数验证。
展望
不论是简单例子还是所引用框架的JavaScript验证都可以对简单的条件进行检查,例如域必须要填写,或者客户机端代码中的数据类型必须要匹配预期的类型。需要用到服务器端逻辑的验证可以使用Ajax添加到JavaScript验证程序中。您可以使用一个用户注册界面来让用户可以选择用户名。文本标签可以进行增强来检查@Column(unique=true)注释。在找到这个注释时,标签可以添加一个用来触发Ajax调用的类。
现在您不需要在应用程序层间维护重复的验证逻辑了,这样就可以节省出大量的开发时间。想像一下您最终可以为应用程序所能添加的增强功能!
http://www.pcjx.com/Java/Hibernate/71326.html