Top
首页 > 老文章 > 正文

Hibernate 能够满足我们的验证需求(上)

Hibernate 能够满足我们的验证需求(上)

Ted Bergeron 将向您介绍如何使用 Hibernate Annotations 的 Validator 组件在 Web 应用程序中轻松构建并维护验证逻辑。
发布时间:2006-10-15 17:55        来源:        作者:
尽管在 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 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 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_VALIDATOR = new ClassValidator(Customer.class); public static final ClassValidator CREDIT_CARD_VALIDATOR = new ClassValidator(CreditCard.class); private static ClassValidator getValidator(Class 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) { Classclazz = 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 所示:
加载更多

专题访谈

合作站点
stat