《Spring 5 官方文档》5. 验证、数据绑定和类型转换(一)

    xiaoxiao2024-02-23  120

    5 验证、数据绑定和类型转换

    5.1 介绍

    JSR-303/JSR-349 Bean Validation

    在设置支持方面,Spring Framework 4.0支持Bean Validation 1.0(JSR-303)和Bean Validation 1.1(JSR-349),也将其改写成了Spring的Validator接口。

    正如5.8 Spring验证所述,应用程序可以选择一次性全局启用Bean验证,并使其专门用于所有的验证需求。

    正如5.8.3 配置DataBinder所述,应用程序也可以为每个DataBinder实例注册额外的Spring Validator实例,这可能有助于不通过使用注解而插入验证逻辑。

    考虑将验证作为业务逻辑是有利有弊的,Spring提供了一种不排除利弊的用于验证(和数据绑定)的设计。具体的验证不应该捆绑在web层,应该容易本地化并且它应该能够插入任何可用的验证器。考虑到以上这些,Spring想出了一个Validator接口,它在应用程序的每一层基本都是可用的。数据绑定对于将用户输入动态绑定到应用程序的领域模型上(或者任何你用于处理用户输入的对象)是非常有用的。Spring提供了所谓的DataBinder来处理这个。Validator和DataBinder组成了validation包,其主要用于但并不局限于MVC框架。

    BeanWrapper是Spring框架中的一个基本概念且在很多地方使用。然而,你可能并不需要直接使用BeanWrapper。尽管这是参考文档,我们仍然觉得有一些说明需要一步步来。我们将会在本章中解释BeanWrapper,因为你极有可能会在尝试将数据绑定到对象的时候使用它。

    Spring的DataBinder和底层的BeanWrapper都使用PropertyEditor来解析和格式化属性值。PropertyEditor概念是JavaBeans规范的一部分,并会在本章进行说明。Spring 3不仅引入了”core.convert”包来提供一套通用类型转换工具,还有一个高层次的”format”包用于格式化UI字段值。可以将这些新包视作更简单的PropertyEditor替代方式来使用,本章还会对此进行讨论。

    5.2 使用Spring的验证器接口进行验证

    Spring具有一个Validator接口可以让你用于验证对象。Validator接口在工作时需要使用一个Errors对象,以便于在验证过程中,验证器可以将验证失败的信息报告给这个Errors对象。

    让我们考虑一个小的数据对象:

    public class Person { private String name; private int age; // the usual getters and setters... }

    通过实现org.springframework.validation.Validator的下列两个接口,我们打算为Person类提供验证行为:

    support(Class) – 这个Validator是否可以验证给定Class的实例 validate(Object,org.springframework.validation.Errors) – 验证给定的对象并且万一验证错误,可以将这些错误注册到给定的Errors对象

    实现一个Validator是相当简单的,特别是当你知道Spring框架还提供了ValidationUtils辅助类:

    public class PersonValidator implements Validator { /** * This Validator validates *just* Person instances */ public boolean supports(Class clazz) { return Person.class.equals(clazz); } public void validate(Object obj, Errors e) { ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); Person p = (Person) obj; if (p.getAge() < 0) { e.rejectValue("age", "negativevalue"); } else if (p.getAge() > 110) { e.rejectValue("age", "too.darn.old"); } } }

    正如你看到的,ValidationUtils类的静态方法rejectIfEmpty(..)被用于拒绝那些值为null或者空字符串的'name'属性。除了上面展示的例子之外,去看一看ValidationUtils的java文档有助于了解它提供的功能。

    通过实现单个的Validator类来逐个验证富对象中的嵌套对象当然是有可能的,然而将验证逻辑封装在每个嵌套类对象自身的Validator实现中可能是一种更好的选择。Customer就是一个‘富’对象的简单示例,它由两个字符串属性(姓和名)以及一个复杂对象Address组成。Address对象可能独立于Customer对象使用,因此已经实现了一个独特的AddressValidator。如果你想要你的CustomerValidator不借助于复制粘贴而重用包含在AddressValidator中的逻辑,那么你可以通过依赖注入或者实例化你的CustomerValidator中的AddressValidator,然后像这样使用它:

    public class CustomerValidator implements Validator { private final Validator addressValidator; public CustomerValidator(Validator addressValidator) { if (addressValidator == null) { throw new IllegalArgumentException("The supplied [Validator] is " + "required and must not be null."); } if (!addressValidator.supports(Address.class)) { throw new IllegalArgumentException("The supplied [Validator] must " + "support the validation of [Address] instances."); } this.addressValidator = addressValidator; } /** * This Validator validates Customer instances, and any subclasses of Customer too */ public boolean supports(Class clazz) { return Customer.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required"); Customer customer = (Customer) target; try { errors.pushNestedPath("address"); ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors); } finally { errors.popNestedPath(); } } }

    验证错误被报告给传递到验证器的Errors对象。在使用Spring Web MVC的情况下,你可以使用<spring:bind/>标签来检查错误信息,不过当然你也可以自己检查错误对象。有关它提供的方法的更多信息可以在java文档中找到。

    5.3 将代码解析成错误消息

    在之前我们已经谈论了数据绑定和验证,最后一件值得讨论的事情是输出对应于验证错误的消息。在我们上面展示的例子里,我们拒绝了name和age字段。如果我们要使用MessageSource来输出错误消息,我们将会使用我们在拒绝该字段(这个情况下是’姓名’和’年龄’)时给出的错误代码。当你调用(不管是直接调用还是间接通过使用ValidationUtils类调用)来自Errors接口的rejectValue或者其他reject方法时,其底层实现不仅会注册你传入的代码,还会注册一些额外的错误代码。注册怎样的错误代码取决于它所使用的MessageCodesResolver,默认情况下,会使用DefaultMessageCodesResolver,其不仅会使用你提供的代码注册消息,还会注册包含你传递给拒绝方法的字段名称的消息。所以如果你使用rejectValue("age", "too.darn.old")来拒绝一个字段,除了too.darn.old代码,Spring还会注册too.darn.old.age和too.darn.old.age.int(第一个会包含字段名称且第二个会包含字段类型)。这样做是为了方便开发人员来定位错误消息等。

    有关MessageCodesResolver和其默认策略的更多信息可以分别在MessageCodesResolver以及DefaultMessageCodesResolver的在线java文档中找到。

    5.4 Bean操作和BeanWrapper

    org.springframework.beans包遵循Oracle提供的JavaBeans标准。一个JavaBean只是一个包含默认无参构造器的类,它遵循一个命名约定(通过一个例子):一个名为bingoMadness属性将有一个设置方法setBingoMadness(..)和一个获取方法getBingoMadness(..)。有关JavaBeans和其规范的更多信息,请参考Oracle的网站(javabeans)。

    beans包里一个非常重要的类是BeanWrapper接口和它的相应实现(BeanWrapperImpl)。引用自java文档,BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定它们是可读还是可写的功能。BeanWrapper还提供对嵌套属性的支持,能够不受嵌套深度的限制启用子属性的属性设置。然后,BeanWrapper提供了无需目标类代码的支持就能够添加标准JavaBeans的PropertyChangeListeners和VetoableChangeListeners的能力。最后然而并非最不重要的是,BeanWrapper提供了对索引属性设置的支持。BeanWrapper通常不会被应用程序的代码直接使用,而是由DataBinder和BeanFactory使用。

    BeanWrapper的名字已经部分暗示了它的工作方式:它包装一个bean以对其执行操作,比如设置和获取属性。

    5.4.1 设置并获取基本和嵌套属性

    使用setPropertyValue(s)和getPropertyValue(s)可以设置并获取属性,两者都带有几个重载方法。在Spring自带的java文档中对它们有更详细的描述。重要的是要知道对象属性指示的几个约定。几个例子:

    表 5.1. 属性示例

    表达式说明name表示属性name与方法getName()或isName()和setName()相对应account.name表示属性account的嵌套属性name与方法getAccount().setName()或getAccount().getName()相对应account[2]表示索引属性account的第三个元素。索引属性可以是array、list或其他自然排序的集合account[COMPANYNAME]表示映射属性account被键COMPANYNAME索引到的映射项的值

    下面你会发现一些使用BeanWrapper来获取和设置属性的例子。

    (如果你不打算直接使用BeanWrapper,那么下一部分对你来说并不重要。如果你仅使用DataBinder和BeanFactory以及它们开箱即用的实现,你应该跳到关于PropertyEditor部分的开头)。

    考虑下面两个类:

    public class Company { private String name; private Employee managingDirector; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Employee getManagingDirector() { return this.managingDirector; } public void setManagingDirector(Employee managingDirector) { this.managingDirector = managingDirector; } } public class Employee { private String name; private float salary; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public float getSalary() { return salary; } public void setSalary(float salary) { this.salary = salary; } }

    以下的代码片段展示了如何检索和操纵实例化的Companies和Employees的某些属性:

    BeanWrapper company = new BeanWrapperImpl(new Company()); // setting the company name.. company.setPropertyValue("name", "Some Company Inc."); // ... can also be done like this: PropertyValue value = new PropertyValue("name", "Some Company Inc."); company.setPropertyValue(value); // ok, let's create the director and tie it to the company: BeanWrapper jim = new BeanWrapperImpl(new Employee()); jim.setPropertyValue("name", "Jim Stravinsky"); company.setPropertyValue("managingDirector", jim.getWrappedInstance()); // retrieving the salary of the managingDirector through the company Float salary = (Float) company.getPropertyValue("managingDirector.salary");

    5.4.2 内置PropertyEditor实现

    Spring使用PropertyEditor的概念来实现Object和String之间的转换。如果你考虑到它,有时候换另一种方式表示属性可能比对象本身更方便。举个例子,一个Date可以以人类可读的方式表示(如String '2007-14-09'),同时我们依然能把人类可读的形式转换回原始的时间(甚至可能更好:将任何以人类可读形式输入的时间转换回Date对象)。这种行为可以通过注册类型为PropertyEditor的自定义编辑器来实现。在BeanWrapper或上一章提到的特定IoC容器中注册自定义编辑器,可以使其了解如何将属性转换为期望的类型。请阅读Oracle为java.beans包提供的java文档来获取更多关于PropertyEditor的信息。

    这是Spring使用属性编辑的两个例子:

    使用PropertyEditor来完成bean的属性设置。当提到将java.lang.String作为你在XML文件中声明的某些bean的属性值时,Spring将会(如果相应的属性的设置方法具有一个Class参数)使用ClassEditor尝试将参数解析成Class对象。在Spring的MVC框架中解析HTTP请求的参数是由各种PropertyEditor完成的,你可以把它们手动绑定到CommandController的所有子类。

    Spring有一些内置的PropertyEditor使生活变得轻松。它们中的每一个都已列在下面,并且它们都被放在org.springframework.beans.propertyeditors包中。大部分但并不是全部(如下所示),默认情况下会由BeanWrapperImpl注册。在某种方式下属性编辑器是可配置的,那么理所当然,你可以注册你自己的变种来覆盖默认编辑器:

    Table 5.2. 内置PropertyEditor

    类说明ByteArrayPropertyEditor针对字节数组的编辑器。字符串会简单地转换成相应的字节表示。默认情况下由BeanWrapperImpl注册。ClassEditor将类的字符串表示形式解析成实际的类形式并且也能返回实际类的字符串表示形式。如果找不到类,会抛出一个IllegalArgumentException。默认情况下由BeanWrapperImpl注册。CustomBooleanEditor针对Boolean属性的可定制的属性编辑器。默认情况下由BeanWrapperImpl注册,但是可以作为一种自定义编辑器通过注册其自定义实例来进行覆盖。CustomCollectionEditor针对集合的属性编辑器,可以将原始的Collection转换成给定的目标Collection类型。CustomDateEditor针对java.util.Date的可定制的属性编辑器,支持自定义的时间格式。不会被默认注册,用户必须使用适当格式进行注册。CustomNumberEditor针对任何Number子类(比如Integer、Long、Float、Double)的可定制的属性编辑器。默认情况下由BeanWrapperImpl注册,但是可以作为一种自定义编辑器通过注册其自定义实例来进行覆盖。FileEditor能够将字符串解析成java.io.File对象。默认情况下由BeanWrapperImpl注册。InputStreamEditor一次性的属性编辑器,能够读取文本字符串并生成(通过中间的ResourceEditor以及Resource)一个InputStream对象,因此InputStream类型的属性可以直接以字符串设置。请注意默认的使用方式不会为你关闭InputStream!默认情况下由BeanWrapperImpl注册。LocaleEditor能够将字符串解析成Locale对象,反之亦然(字符串格式是[country][variant],这与Locale提供的toString()方法是一样的)。默认情况下由BeanWrapperImpl注册。PatternEditor能够将字符串解析成java.util.regex.Pattern对象,反之亦然。PropertiesEditor能够将字符串(按照java.util.Properties类的java文档定义的格式进行格式化)解析成Properties对象。默认情况下由BeanWrapperImpl注册。StringTrimmerEditor用于缩减字符串的属性编辑器。有选择性允许将一个空字符串转变成null值。不会进行默认注册,需要在用户有需要的时候注册。URLEditor能够将一个URL的字符串表示解析成实际的URL对象。默认情况下由BeanWrapperImpl注册。

    Spring使用java.beans.PropertyEditorManager来设置可能需要的属性编辑器的搜索路径。搜索路径中还包括了sun.bean.editors,这个包里面包含如Font、Color类型以及其他大部分基本类型的PropertyEditor实现。还要注意的是,如果PropertyEditor类与它们所处理的类位于同一个包并且除了’Editor’后缀之外拥有相同的名字,那么标准的JavaBeans基础设施会自动发现这些它们(不需要你显式的注册它们)。例如,有人可能会有以下的类和包结构,这已经足够识别出FooEditor类并将其作为Foo类型属性的PropertyEditor。

    com chank pop Foo FooEditor // the PropertyEditor for the Foo class

    要注意的是在这里你也可以使用标准JavaBeans机制的BeanInfo(在in not-amazing-detail here有描述)。在下面的示例中,你可以看到使用BeanInfo机制为一个关联类的属性显式注册一个或多个PropertyEditor实例。

    com chank pop Foo FooBeanInfo // the BeanInfo for the Foo class

    这是被引用到的FooBeanInfo类的Java源代码。它会将一个CustomNumberEditor同Foo类的age属性关联。

    public class FooBeanInfo extends SimpleBeanInfo { public PropertyDescriptor[] getPropertyDescriptors() { try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) { public PropertyEditor createPropertyEditor(Object bean) { return numberPE; }; }; return new PropertyDescriptor[] { ageDescriptor }; } catch (IntrospectionException ex) { throw new Error(ex.toString()); } } }

     

    注册额外的自定义PropertyEditor

    当bean属性设置成一个字符串值时,Spring IoC容器最终会使用标准JavaBeans的PropertyEditor将这些字符串转换成复杂类型的属性。Spring预先注册了一些自定义PropertyEditor(例如将一个以字符串表示的类名转换成真正的Class对象)。此外,Java的标准JavaBeans PropertyEditor查找机制允许一个PropertyEditor只需要恰当的命名并同它支持的类位于相同的包,就能够自动发现它。

    如果需要注册其他自定义的PropertyEditor,还有几种可用机制。假设你有一个BeanFactory引用,最人工化的方式(但通常并不方便或者推荐)是直接使用ConfigurableBeanFactory接口的registerCustomEditor()方法。另一种略为方便的机制是使用一个被称为CustomEditorConfigurer的特殊的bean factory后处理器(post-processor)。虽然bean factory后处理器可以与BeanFactory实现一起使用,但是因为CustomEditorConfigurer有一个嵌套属性设置过程,所以强烈推荐它与ApplicationContext一起使用,这样就可以采用与其他bean类似的方式来部署它,并自动检测和应用。

    请注意所有的bean工厂和应用上下文都会自动地使用一些内置属性编辑器,这些编辑器通过一个被称为BeanWrapper的接口来处理属性转换。BeanWrapper注册的那些标准属性编辑器已经列在上一部分。 此外,针对特定的应用程序上下文类型,ApplicationContext会用适当的方法覆盖或添加一些额外的编辑器来处理资源查找。

    标准的JavaBeans PropertyEditor实例用于将字符串表示的属性值转换成实际的复杂类型属性。CustomEditorConfigurer,一个bean factory后处理器,可以为添加额外的PropertyEditor到ApplicationContext提供便利支持。

    考虑一个用户类ExoticType和另外一个需要将ExoticType设为属性的类DependsOnExoticType:

    package example; public class ExoticType { private String name; public ExoticType(String name) { this.name = name; } } public class DependsOnExoticType { private ExoticType type; public void setType(ExoticType type) { this.type = type; } }

    当东西都被正确设置时,我们希望能够分配字符串给type属性,而PropertyEditor会在背后将其转换成实际的ExoticType实例:

    <bean id="sample" class="example.DependsOnExoticType"> <property name="type" value="aNameForExoticType"/> </bean>

    PropertyEditor实现可能与此类似:

    // converts string representation to ExoticType object package example; public class ExoticTypeEditor extends PropertyEditorSupport { public void setAsText(String text) { setValue(new ExoticType(text.toUpperCase())); } }

    最后,我们使用CustomEditorConfigurer将一个新的PropertyEditor注册到ApplicationContext,那么在需要的时候就能够使用它:

    <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="customEditors"> <map> <entry key="example.ExoticType" value="example.ExoticTypeEditor"/> </map> </property> </bean>

    使用PropertyEditorRegistrar

    另一种将属性编辑器注册到Spring容器的机制是创建和使用一个PropertyEditorRegistrar。当你需要在几个不同场景里使用同一组属性编辑器,这个接口会特别有用:编写一个相应的registrar并在每个用例里重用。PropertyEditorRegistrar与一个被称为PropertyEditorRegistry的接口配合工作,后者被Spring的BeanWrapper(以及DataBinder)实现。当与CustomEditorConfigurer配合使用的时候,PropertyEditorRegistrar特别方便(这里有介绍),因为前者暴露了一个方法setPropertyEditorRegistrars(..):以这种方式添加到CustomEditorConfigurerd的PropertyEditorRegistrar可以很容易地在DataBinder和Spring MVC Controllers之间共享。另外,它避免了在自定义编辑器上的同步需求:一个PropertyEditorRegistrar可以为每一次bean创建尝试创建新的PropertyEditor实例。

    使用PropertyEditorRegistrar可能最好还是以一个例子来说明。首先,你需要创建你自己的PropertyEditorRegistrar实现:

    package com.foo.editors.spring; public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { public void registerCustomEditors(PropertyEditorRegistry registry) { // it is expected that new PropertyEditor instances are created registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor()); // you could register as many custom property editors as are required here... } }

    也可以查看org.springframework.beans.support.ResourceEditorRegistrar当作一个PropertyEditorRegistrar实现的示例。注意在它的registerCustomEditors(..)方法实现里是如何为每个属性编辑器创建新的实例的。

    接着我们配置了一个CustomEditorConfigurerd并将我们的CustomPropertyEditorRegistrar注入其中:

    <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="propertyEditorRegistrars"> <list> <ref bean="customPropertyEditorRegistrar"/> </list> </property> </bean> <bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>

    最后,有点偏离本章的重点,针对你们之中使用Spring’s MVC web framework的那些人,使用PropertyEditorRegistrar与数据绑定的Controller(比如SimpleFormController)配合使用会非常方便。下面是一个在initBinder(..)方法的实现里使用PropertyEditorRegistrar的例子:

    public final class RegisterUserController extends SimpleFormController { private final PropertyEditorRegistrar customPropertyEditorRegistrar; public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { this.customPropertyEditorRegistrar = propertyEditorRegistrar; } protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { this.customPropertyEditorRegistrar.registerCustomEditors(binder); } // other methods to do with registering a User }

    这种PropertyEditor注册的风格可以导致简洁的代码(initBinder(..)的实现仅仅只有一行!),同时也允许将通用的PropertyEditor注册代码封装到一个类里然后根据需要在尽可能多的Controller之间共享。

    转载自 并发编程网 - ifeve.com    

    相关资源:Spring 5 英文文档全套.7z
    最新回复(0)