他们彼此深信,是瞬间迸发的热情让他们相遇。这样的确定是美丽的,但变幻无常更为美丽

留言簿

公告

最新日志

最新评论

搜索

登陆

友情连接

统计

2006/10/11 15:21:00
Hibernate 能够满足我们的验证需求
    尽管在 Web 应用程序中尽可能多的层次中构建数据验证非常重要,但是这样做却非常耗时,以至于很多开发人员都会干脆忽略这个步骤 —— 这可能会导致今后大量问题的产生。但是随着最新版本的 Java 平台中引入了注释,验证变得简单得多了。在本文中,Ted Bergeron 将向您介绍如何使用 Hibernate Annotations 的 Validator 组件在 Web 应用程序中轻松构建并维护验证逻辑。
    有时会有一种工具,它可以真正满足开发人员和架构师的需求。开发人员在第一次下载这种工具当天就可以在自己的应用程序中开始使用这种工具。理论上来说,这种工具在开发人员花费大量时间来掌握其用法之前就可以从中获益。架构师也很喜欢这种工具,因为它可以将开发人员导向更高理论层次的实现。Hibernate Annotations 的 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;
    }

如果需要,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 LanguageEL)方法表示(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="/blog/${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="/blog/${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

 

 &nb

posted @ 2006/10/11 15:21:00 bskyhero 阅读全文 | 回复(0) | 引用通告 | 编辑 | 收藏该日志

发表评论:

    昵称:
    密码:
    主页:
    标题: