• 7. Spring验证、数据绑定和类型转换


    (英文原版. Spring 3.2.14 Reference - 7 Validation, Data Binding, and Type Conversion
    (如有翻译不准确的地方,请私信我,共同提高;转载请注明出处,谢谢)

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

    7.1 介绍

    对于把校验作为业务逻辑有正面意见也有负面意见,而Spring提供的验证设计(和数据绑定)不考虑这些。特别是,验证不应该依赖于Web层,它应该很容易被调用,可以插入任何可用的验证器。基于上述的考虑,Spring提供了一个Validator接口,这是一个基础组件,可以用于应用程序的每一层。

    数据绑定机制允许将用户输入动态绑定到应用程序的域模型对象(或用来处理用户输入的任何对象)。Spring提供了DataBinder类实现数据绑定机制。

    Validator和DataBinder组成了validation包,主要但不限于在MVC framework中应用。BeanWrapper是Spring Framework中的一个基础组件,在很多地方都用到,但是开发者一般不会直接使用。因为这是一个参考文档,所以本章对BeanWrapper进行一些必要的解释。如果开发者想使用它,最可能是在数据绑定场景下使用。

    Spring的DataBinder和底层的BeanWrapper都使用PropertyEditor来解析和格式化属性值。PropertyEditor是JavaBean所特有的,在本章也会进行必要的解释。Spring 3引入了“core.convert”包,既提供了一个通用类型转换工具,也提供了高级“format”包来格式化UI字段值。Spring中的这些新的程序包用起来可能比PropertyEditor简单,在本章也会讨论。

    7.2 Spring Validator接口

    Spring 引入了Validator接口,可以用来验证对象。Validator接口与Errors对象一起使用,验证时Validator可以报告验证错误到Errors对象。
    让我们考虑一个简单数据对象。

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

    通过实现org.springframework.validation.Validator接口的两个方法为Person类提供验证:

    • supports(Class ) - Validator是否支持验证Class类型,返回true或者false。
    • validate(Object, org.springframework.validation.Errors) - 验证Object对象,如果验证错误,将错误注册到Errors对象参数。

    实现Validator比较简单,特别是借助Spring Framework提供的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");
    		}
    	}
    }
    

    正如你所看到的,如果name属性为null或者空字符串,ValidationUtils类的static rejectIfEmpty(..)方法会拒绝 name属性,并在Erros对象中返回验证失败。查阅ValidationUtils类的Javadoc文档,查看更多方法和使用。

    开发者有可能会在一个Validator类中验证复杂对象(rich object)的每一个内嵌对象,最好将每一个内嵌对象的验证逻辑封装在各自的类实现中。我们来看一个简单的“复杂对象”,Customer对象是由两个字符串(first name和second name)以及一个Address对象组成。Address对象与Custom对象是相互独立的,需要单独实现AddressValidator。如果开发者需要在CustomerValidator中重用AddressValidator中的验证逻辑,但不是通过复制粘贴代码的方式,可以使用依赖注入(DI),或者在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对象,并传递给validator对象。如果是Spring Web MVC中使用,可以使用<spring:bind />标签来注入错误消息(error message),当然你也可以自行检查errors对象。更多关于这些方法的信息可以从Javadoc中找到。

    7.3 错误码与错误消息

    我们讨论了数据绑定和验证。对应验证错误的消息输出是我们需要讨论的最后一件事。在之前的例子中,我们拒绝了name和age字段。如果我们使用MessageSource输出错误消息,我们会用到验证字段(name和age字段)错误时返回的错误码(error code)。当你调用(直接或者间接,使用ValidationUtils类)rejectValue或者Erros接口的其它reject方法,底层实现不会只注册传递过来的代码(code),还有一些额外的错误代码(error code)。注册哪些错误码是由使用的MessageCodesResolver决定的。默认使用DefaultMessageCodesResolver,它不仅注册传递过来的代码的消息,还包括传递给reject方法的字段名称。因此当你使用rejectValue("age","to.darn.old")拒绝字段时,不光是too.darn.old代码,Spring还会注册too.darn.old.age和too.darn.old.age.int(第一个包含字段名称,第二个包含字段类型);这样做是为了方便开发者定位该类错误消息。

    关于MessageCodesResolver更多信息和默认策略,可以分别查看MessageCodesResolver和DefaultMessageCodesResolver的在线Javadoc。

    7.4 Bean 处理和BeanWrapper

    org.springframework.beans包由Sun提供,遵循JavaBean标准。JavaBean是一种简单的类,包含一个默认的无参构造函数,且遵循一定的命名规范。例如,如果有一个属性名为bingoMadness,必须包含一个赋值方法setBingoMadness(...),和一个获取属性值的方法getBingoMadness()。如果希望获取更多的关于JavaBean的规范,请参考Sun网站(java.sun.com/products/javabeans)。

    org.springframework.beans包中一个非常重要的类是BeanWrapper接口和它对应的实现类(BeanWrapperImpl)。正如在Javadoc中提到,BeanWrapper提供了一些功能,包括设置和获取属性值(单个属性或者组合属性),获取属性描述,查询属性是否可读或可写。同时,BeanWrapper提供嵌套属性的支持,可以对无限制深度的子属性进行设置。这样,不需要在目标类添加支持代码,BeanWrapper即可支持在目标类上添加标准JavaBean类JavaBeanPropertyChangeListeners和VetoableChangeListeners。不止如此,BeanWrapper还为索引属性提供设置支持。BeanWrapper通常不会在应用中直接使用,而是通过DataBinder和BeanFactory来使用。

    从名称上可以看出BeanWrapper的工作机制,它封装了Bean,使得可以对Bean进行操作,例如设置和访问属性。

    7.4.1 设置和获取基本属性和嵌套属性

    使用setPropertyValues(s)和getPropertyValue(s)方法设置和获取属性值,这些方法可以负载几个变量。这些在之后的Spring Javadoc文档中有更详细的描述。重要的是,表示一个对象的属性有一些规范。表7.1为属性表示样例。

    表7.1 属性表示样例

    属性 说明
    name 表示属性name, 对应的方法为getName() 或 isName() ,以及 setName(..)
    account.name 表示属性account的嵌套属性name, 对应的方法为getAccount().setName() 或者getAccount().getName()
    account[2] 表示索引属性account的第3个元素。索引属性可以使array、list或者自然排序的collection。
    account[COMPANYNAME] 表示Map类型的属性account中key为COMPANYNAME 的Map条目的value。

    使用BeanWrapper设置和获取属性值的样例如下:

    考虑以下两个类:

    public class Company {
        private String name;
        private Employee managingDirector; 
    
        public String getName() {
            returnthis.name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Employee getManagingDirector() {
            returnthis.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 = BeanWrapperImpl(new Company());
    // 设置company的name属性..
    company.setPropertyValue("name", "Some Company Inc.");
    // ... 也可以这样设置:
    PropertyValue value = new PropertyValue("name", "Some Company Inc.");
    company.setPropertyValue(value);
    
    // 创建一个employee对象,赋值给managingDirector:
    BeanWrapper jim = BeanWrapperImpl(new Employee());
    jim.setPropertyValue("name", "Jim Stravinsky");
    company.setPropertyValue("managingDirector", jim.getWrappedInstance());
    
    // 获取company对象的managingDirector属性的salary属性(嵌套属性)
    Float salary = (Float) company.getPropertyValue("managingDirector.salary");
    

    7.4.2 内置的PropertyEditor实现

    Spring使用PropertyEditor来处理Object和String之间的转换。有时候使用PropertyEditor比使用对象本身可以更方便的表示属性。例如,可以用易读的方式(如“2007-14-09”)表示Date,也可以将其转换回原始Date对象(更进一步:将任意易读的时间格式转换为Date对象),而这种效果通过注册java.beans.PropertyEditor类型的自定义editor就可以实现。在BeanWrapper或IoC容器上注册自定义editor,可以转换对象的属性为指定的类型。如果需要获取PropertyEditor更多的信息,可以阅读由Sun提供的java.beans包的Javadoc文档。

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

    • 使用PropertyEditor设置bean属性。在XML文件中注入bean的某些属性的值为java.lang.String类型,如果该属性的赋值方法中的参数是Class类型,Spring会使用ClassEditor将String类型转换为Class类型。
    • Spring MVC框架中使用所有可用的PropertyEditor解析处理来自客户端的HTTP请求参数,开发者可以在CommandController的所有子类中手工绑定PropertyEditor。
      表7.2中列出了Spring 框架中org.springframework.beans.propertyeditors 包中内置的所有PropertyEditor。大多数(并非全部,见表说明)在BeanWrapperImpl中已经默认注册。配置了PropertyEditor之后,开发者也可以注册自定义PropertyEditor来覆盖默认类。

    表7.2. 内置的PropertyEditor清单

    说明
    ByteArrayPropertyEditor 将字符串转换为字节数组(byte[])。BeanWrapper中已经默认注册。
    ClassEditor 将字符串类名转换为java.lang.Class。当找不到对应的类时,抛出ellegalArgumentException异常。BeanWrapper中已经默认注册。
    CustomBooleanEditor Boolean类型的自定义属性编辑器, BeanWrapper中已经默认注册。可以注册自定义的实例覆盖原有实例。
    CustomCollectionEditor Collection类型的自定义属性编辑器,将Collection转换为给定T类型的Collection
    CustomDateEditor java.util.Date 类型的自定义属性编辑器,支持自定义DateFormat,BeanWrapper中没有默认注册,需要开发者自行注册。
    CustomNumberEditor Integer, Long, Float, Double等Number类型的自定义属性编辑器BeanWrapper中已经默认注册。可以注册自定义的实例覆盖原有实例。
    FileEditor java.io.File 类型的属性编辑器,解析字符串为File对象。BeanWrapper中已经默认注册。
    InputStreamEditor InputStream类型的单向属性编辑器,输入字符串,输出InputStream对象(通过ResourceEditor和Resource)。注意默认实现中Spring没有关闭InputStream,需要由开发者自行调用关闭。BeanWrapper中已经默认注册。
    LocaleEditor java.utils.Locale类型的属性编辑器。解析字符串为Locale对象,反之亦然。String字符串格式为[language][country][variant],与Locale的toString()方法一样。BeanWrapper中已经默认注册。
    PatternEditor JDK 1.5 Pattern类型的属性编辑器。解析字符串为Pattern对象,反之亦然。
    PropertiesEditor java.lang.Properties 类型的属性编辑器。解析字符串为Properties对象。String格式遵循java.lang.Properties的Javadoc定义。BeanWrapper中已经默认注册。
    StringTrimmerEditor 用于trim 字符串。开发者可以选择是否将空字符串转换为null。BeanWrapper中没有默认注册,需要开发者自行注册。
    URLEditor URL类型的属性编辑器。解析字符串为URL对象。BeanWrapper中已经默认注册。

    Spring使用java.beans.PropertyEditorManager来设置所需要的PropertyEditor实现类的查找路径。查找路径中包括sun.bean.editors,包含了Font,Color类型以及大多数的基本类型的PropertyEditor接口实现。同时注意,如果PropertyEditor类文件和属性的类文件在同一个包下面,且PropertyEditor实现类类名为属性类名加Editor后缀,那么标准JavaBeans框架会自动发现PropertyEditor类(不需要显式注册)。例如,下面这个包结构和类,FooEditor会被默认认为是Foo类型的PropertyEditor属性编辑器。

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

    注意也可以使用标准BeanInfo JavaBeans机制。下面是使用BeanInfo机制的例子,显式注册了一个或者多个PropertyEditor实例处理对应类的属性。

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

    下面是FooBeanInfo类的Java 源码参考。这里关联了一个处理age属性的CustomNumberEditor。

    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将被自动发现。

    如果需要注册其它自定义的PropertyEditor,有以下几种方法。最常用的手动方法(不方便也不推荐),如果有一个BeanFactory实现类,可以使用ConfigurableBeanFactory接口的registerCustomEditor()方法,。另一个比较简便的方法,是使用CustomEditorConfigurer这个特殊的bean工厂后置处理器。虽然bean工厂后置处理器可以与BeanFactory实现类一起使用,但是CustomEditorConfigurer存在一个嵌套属性设置,所以强烈建议在ApplicationContext中使用,可以用类似的方式部署给其它Bean,会被自动检测和应用。

    注意所有的bean工厂和应用程序上下文自动使用一些内置属性编辑器,通过使用BeanWrapper处理属性值的类型转换。上一章节罗列了BeanWrapper注册的标准的属性编辑器。另外,ApplicationConext也覆盖或者增加了额外的编辑器来处理资源查找(resource lookup)问题,以适合特定的应用上下文类型。

    PropertyEditor实例用来转换字符串表示的属性值为实际的属性类型。可以通过CustomEditorConfigurer向ApplicationContext方便的添加额外的PropertyEditor实例。

    考虑一个用户类ExoticType,以及DependsOnExoticType类,后者具有一个ExoticType的属性需要设置。

    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;
        }
    }
    

    当设置完成后,我们希望能够将字符串赋值给该属性,由PropertyEditor完成字符串到ExoticType实例的转换。

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

    PropertyEditor的实现可能类似这样:

    // 转换字符串为ExoticType;
    public class ExoticTypeEditor extends PropertyEditorSupport {
    
        public void setAsText(String text) {
            setValue(new ExoticType(text.toUpperCase()));
        }
    }
    

    最后,我们使用CustomEditorConfigurer在ApplicationContext中注册新的PropertyEditor,以便可以在需要的时候使用。

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

    使用PropertyEditorRegistrar

    另一个在Spring容器中注册属性编辑器(property editor)的机制是创建和使用PropertyEditorRegistrar接口。当需要在不同场景下重复使用一组属性编辑器时比较有效,只需要写一个registrar,重用即可。

    PropertyEditorRegistrars与PropertyEditorRegistry接口联合使用,这个接口由Spring BeanWrapper(和DataBinder)实现。

    PropertyEditorRegistrars在与CustomEditorConfigurer联合使用时特别方便(在这里有介绍),CustomEditorConfigurer接口包含一个设置PropertyEditorRegistrars的方法setPropertyEditorRegistrars(..):添加的PropertyEditorRegistrars能够很容易的共享给DataBinder和Spring MVC Controller。此外,不再需要同步自定义编辑器:每一次尝试创建bean时,PropertyEditorRegistrar会创建一个新的PropertyEditor实例。

    最好用一个例子来说明PropertyEditorRegistrars。首先,你需要创建自己的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(...)方法是如何创建每一个属性编辑器的。

    下一步我们配置CustomEditorConfigurer,注入CustomPropertyEditorRegistrars实例。

    <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 MVC web框架时,与数据绑定控制器(例如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)。

    7.5 Spring 3 类型转换

    Spring 3 引入了core.convert包,提供通用的类型转换系统。这个系统定义了一个SPI(Service Provide API)来实现类型转换逻辑,类似在运行时执行类型转换的API。在Spring容器中,该系统可以作为PropertyEditor的替代品来转换外部bean属性的字符串值为需要的属性类型。这个公共的API可以用在应用程序任何需要类型转换的地方。

    7.5.1 Converter SPI

    用来实现类型转换逻辑的SPI定义很简单但是很有效:

    package org.springframework.core.convert.converter
    public interface Converter<S,T>{
        public <T> convert(S source);
    }
    

    创建Converter,实现上述接口即可。S为转换的源类型,T为转换的目标类型。每次调用convert(S),要保证S不能为null。如果转换失败,Converter可能抛出异常。 抛出illegalArgumentException异常,表示源类型无效。请保证Converter的实现类是线程安全的。

    为方便起见,core.convert.converter包提供了一些Converter的实现类,包括String转Number和其它常见类型。我们来看下面的StringToInteger的样例:

    package org.springframework.core.convert.support;
    
    final class StringToInteger implements Converter<String, Integer> {
    
        public Integer convert(String source) {
            return Integer.valueOf(source);
        }
    
    }
    

    7.5.2 ConverterFactory

    当需要为整个类层次结构集中类型转换时,例如,当将String转换为java.lang.Enum对象时,实现ConverterFactory:

    package org.springframework.core.convert.converter
    public interface ConverterFactory<S,R>{
        <T extends R> Converter<S,T> getConverter(Class<T> targetType);
    }
    

    S为源类型,R为一系列目标类型的基类。实现getConverter(Class)方法,其中T是R的子类。

    下面是StringToEnum ConvertFactory的样例:

    package org.springframework.core.convert.support;
    
    final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
    
        public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
            return new StringToEnumConverter(targetType);
        }
    
        private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
    
            private Class<T> enumType;
    
            public StringToEnumConverter(Class<T> enumType) {
                this.enumType = enumType;
            }
    
            public T convert(String source) {
                return (T) Enum.valueOf(this.enumType, source.trim());
            }
        }
    }
    

    7.5.3 GenericConverter

    当需要实现一个比较复杂的Converter,可以考虑GenericConverter接口。更灵活,但是不是强类型,GenericConverter支持多种源类型和目标类型之间的转换。另外,当实现转换逻辑时,可以使用GenericConverter的源和目标字段上下文。这些上下文允许注解驱动的类型转换,或者在字段上声明的通用信息。

    实现GenericConverter接口,需要实现getConvertibleTypes()方法,返回支持的源和目的类型集合。需要实现convert(Object, TypeDescriptor, TypeDescriptor)方法,在方法中实现转换逻辑。其中,3个参数依次表示待转换的源、源类型,目标类型,返回值为转换后的值。使用GenericConverter接口的一个比较好的例子是在Java Array和Collection之间转换。ArrayToCollection内省(introspect)声明Collection类型的字段来解析Collection的元素类型,实现在返回Collection对象之前Array中的每一个元素都已经转换为Collection中元素的类型。

    注意:GenericConverter是一个很复杂的SPI接口,只有当你需要它的时候再使用。处理基本类型转换时,使用Favor Converter或者ConverterFactory即可。

    ConditionalGenericConverter

    有时候,你可能希望在某些条件下才执行类型转换。例如,你可能在只有当注解出现在目标字段上的时候才执行类型转换。或者,你可能在只有目标类包含一个静态方法ValueOf()的时候才能执行 类型转换。

    public interface ConditionalGenericConverter extends GenericConverter {
        boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);	
    }
    

    使用ConditionGenericConverter接口的一个比较好的例子是EntityConverter,用于转换持久化实体ID和实体引用。只有当实体类中声明了一个静态finder方法,如findAccount (Long),EntityConverter matches(TypeDescriptor,TypeDescriptor)方法才会返回true。在实现matches(TypeDescriptor,TypeDescriptor )时,需要检查finder方法是否存在。

    7.5.4 ConversionService API

    ConversionService定义了一个统一的API,用来在运行时执行类型转换逻辑。通常,Converter在下面facade接口后面执行。

    package org.springframework.core.convert;
    
    public interface ConversionService {
    
        boolean canConvert(Class<?> sourceType, Class<?> targetType);
    
        <T> T convert(Object source, Class<T> targetType);
    
        boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
    
        Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
    
    }
    

    大多数ConversionService实现类实现了ConverterRegistry,它提供SPI来注册converter。在内部,ConversionService实现类代理已注册的转换器(converter)处理类型转换逻辑。

    core.convert.support包提供了强壮的ConversionService实现。GenericConvertionService适合大多数环境下的通用实现。ConversionServiceFactory是ConversionService的工厂类,用来创建ConversionService配置。

    7.5.5 配置ConversionService

    ConversionService是一个无状态的对象,设计为应用程序启动时被加载,然后在多个线程间共享。在Spring应用程序中,你可以为每个Spring容器(或者ApplicationContext)配置一个ConversionService。该ConversionService会被Spring加载,然后在需要类型转换的时候使用。你可以注入ConversionService到任何一个bean直接调用它。

    注意,如果ConversionService没有在Spring注册,那么原始的基于PropertyEditor的系统会被使用。

    为了在Spring中注入默认的ConversionService,在XML文件中增加下面的bean,id为conversionService。

    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>
    

    默认的ConversionService可以转换String,Numbers,enums,Collections,maps和其它常用的类型。为了支持或者用自定义的转换器(converter)覆盖默认的转换器,可以设置converters属性,属性值可以被Converter,ConverterFactory或者GenericConverter接口实现。

    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    	<property name="converters">
    		<list>
    			<bean class="example.MyCustomConverter"/>
    		</list>
    	</property>
    </bean>
    

    Spring MVC中也经常使用ConversionService。7.6.5查看该部分详细内容。

    在一些情况下,可能希望在类型转换的时候进行格式转换。7.6.3查看该部分详细内容。

    7.5.6 通过编程使用ConversionService

    通过编程使用ConversionService,在你想要的任何bean的属性中注入ConversionService。

    @Service 
    public class MyService {
    
    @Autowired 
    public MyService(ConversionService conversionService) {
            this.conversionService = conversionService;
        }
    
        public void doIt() {
            this.conversionService.convert(...)
        }
    }
    

    7.6 Spring 3 字段格式化

    在之前章节里讨论过,core.convert包是一个通用的类型转换系统。它提供了一个统一的转换服务API(ConversionService API),类似强类型Converter SPI,实现了从一个类型到另一个类型的转换逻辑。Spring容器(Spring Container)使用这个系统绑定Bean属性值。另外,Spring Expression Language(SpEL)和DataBinder使用这个系统绑定字段值。例如,当调用expression.setValue(Object bean, Object value) SpEL表达式,需要尝试将Short类型强制转换为Long类型时,其强制转换工作就是由core.convert完成的。

    现在考虑典型的客户端环境下的类型转换需求,如web应用或者桌面应用。在这种环境下,常常需要将String类型进行转换来支持客户端的POST请求,以及转换为String类型来支持前端视图渲染。另外,常常需要对String类型的值进行本地化。core.convert Converter SPI并不直接响应这样的格式化。为了直接响应,Spring 3为客户端环境引入了一个方便的Formatter SPI,给PropertyEditor提供了一个简单好用的替代品。

    一般而言,当需要实现通用类型转换逻辑时,使用Converter SPI,例如需要在java.util.Date和java.lang.Long之间转换时。当在客户端环境时,使用Formatter SPI。例如Web应用,或者需要解析和打印本地化的字段值。ConversionService为这两种SPI提供一个统一的转换API接口。

    7.6.1 Formatter SPI

    Formatter SPI实现字段格式化很简单,且为强类型。

    package org.springframework.format;
    
    public interface Formatter<T> extends Printer<T>, Parser<T> {
    }
    

    Formatter SPI继承了Printer和Parser的接口。

    public interface Printer<T> {
        String print(T fieldValue, Locale locale);
    }
    
    import java.text.ParseException;
    
    public interface Parser<T> {
        T parse(String clientValue, Locale locale) throws ParseException;
    }
    

    创建自定义的Formatter时,实现Formatter接口即可。实例化T为被格式化的对象类型,例如java.util.Date。实现print()方法打印T实例,用于客户端本地显示;实现parse()方法解析T实例,用于接收从客户端传递过来的格式化后的表达式。当解析失败后,Formatter实现类应该抛出ParseException或者IllegalArgumentException异常。注意,须保证Formatter实现类线程安全。

    为方便起见,在format子包中实现了部分Formatter。number包提供了NumberFormatter,CurrencyFormatter和PercentFormatter,使用java.text.NumberFormat来格式化java.lang.Number对象。datetime包提供了DateFormatter,使用java.text.DateFormat来格式化java.util.Date对象。基于Joda Time库,datetime.joda包提供了更为复杂的datetime格式化支持。

    以DateFormatter为例,来看Formatter接口的实现:

    package org.springframework.format.datetime;
    
    public final class DateFormatter implements Formatter<Date> {
    
        private String pattern;
    
        public DateFormatter(String pattern) {
            this.pattern = pattern;
        }
    
        public String print(Date date, Locale locale) {
            if (date == null) {
                return "";
            }
            return getDateFormat(locale).format(date);
        }
    
        public Date parse(String formatted, Locale locale) throws ParseException {
            if (formatted.length() == 0) {
                return null;
            }
            return getDateFormat(locale).parse(formatted);
        }
    
        protected DateFormat getDateFormat(Locale locale) {
            DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
            dateFormat.setLenient(false);
            return dateFormat;
        }
    
    }
    

    7.6.2 注解驱动(annotation-driven)格式化

    正如您将看到,字段格式化可以通过字段类型或者注解(annotation)来配置。为了绑定注解到一个formatter,需要实现AnnotationFormatterFactory工厂接口。

    package org.springframework.format;
    
    public interface AnnotationFormatterFactory<A extends Annotation> {
    
        Set<Class<?>> getFieldTypes();
    
        Printer<?> getPrinter(A annotation, Class<?> fieldType);
    
        Parser<?> getParser(A annotation, Class<?> fieldType);
    
    }
    

    实例化泛型A为格式化逻辑相关联的字段注解类型,例如
    org.springframework.format.annotation.DateTimeFormat。

    实现getFieldTypes(),返回注解可以应用到的字段类型;实现getPrinter(),返回能够为注解字段打印字段值的Printer;实现getParser(),返回能够为注解字段解析客户端上传数据(clientValue)的Parser。

    public final class NumberFormatAnnotationFormatterFactory
            implements AnnotationFormatterFactory<NumberFormat> {
    
        public Set<Class<?>> getFieldTypes() {
            return new HashSet<Class<?>>(asList(new Class<?>[] {
                Short.class, Integer.class, Long.class, Float.class,
                Double.class, BigDecimal.class, BigInteger.class }));
        }
    
        public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
            return configureFormatterFrom(annotation, fieldType);
        }
    
        public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
            return configureFormatterFrom(annotation, fieldType);
        }
    
        private Formatter<Number> configureFormatterFrom(NumberFormat annotation,
                                                         Class<?> fieldType) {
            if (!annotation.pattern().isEmpty()) {
                return new NumberFormatter(annotation.pattern());
            } else {
                Style style = annotation.style();
                if (style == Style.PERCENT) {
                    return new PercentFormatter();
                } else if (style == Style.CURRENCY) {
                    return new CurrencyFormatter();
                } else {
                    return new NumberFormatter();
                }
            }
        }
    }
    

    为了使用格式化,用@NumberFormat注解相应的字段。

    public class MyModel {
    
        @NumberFormat(style=Style.CURRENCY)
        private BigDecimal decimal;
    
    }
    

    Format Annotation API

    格式化注解API(Format Annotation API)位于org.springframework.format.annotation包中。使用@NumberFormat来格式化java.lang.Number字段;使用@DateTimeFormat来格式化java.util.Date,java.util.Calendar,java.util.Long或者Joda Time字段。

    下面的例子使用@DateTimeFormat来格式化java.util.Date为ISO Date格式(yyyy-MM-dd):

    public class MyModel {
    
        @DateTimeFormat(iso=ISO.DATE)
        private Date date;
    
    }
    

    7.6.3 FormatterRegistry SPI

    FormatterRegistry SPI用来注册formatter和converter。

    FormattingConversionService是FormatterRegistry的一个实现类,适用于绝大多数环境,通过代码编程或者声明FormattingConversionServiceFactoryBean为Spring Bean来引入。这个类同时实现了ConversionService接口,因此配置后可以被Spring的DataBinder和SpEL直接调用。

    看一下FormatterRegistry SPI的代码:

    package org.springframework.format;
    
    public interface FormatterRegistry extends ConverterRegistry {
    
        void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
    
        void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
    
        void addFormatterForFieldType(Formatter<?> formatter);
    
        void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
    
    }
    

    正如上面显示,可以通过fieldType或者注解来注册Formatter。

    FormatterRegistry SPI允许开发者集中配置格式化(Formatting)规则,而不是在控制器(Controller)中重复这些配置。例如,如果希望按照某种规则对所有的Date字段进行格式化,或者按照某种规则对注解字段进行格式化,可以通过共享FormatterRegistry,实现“一次定义,按需应用”。

    7.6.4 FormatterRegistrar SPI

    FormatterRegistrar SPI用来利用FormatterRegistry注册formatter和converter。

    package org.springframework.format;
    
    public interface FormatterRegistrar {
    
        void registerFormatters(FormatterRegistry registry);
    
    }
    

    在为给定的一组分类注册多个相关的converter和formatter时,FormatterRegistrar比较有用,例如Date格式化。同样在声明注册不足以表示时比较有用。例如,当formatter需要在不同于它自 己的泛型的特定字段类型下建立索引,或者注册一个Printer/Parser对儿时。下一节对converter和formatter的注册提供了更多的信息。

    7.6.5 在Spring MVC中配置格式化

    Spring MVC应用程序中,开发者可以显式配置一个自定义的ConversionService实例,作为<mvc:annotation-driven />元素的一个属性。这样一来,在Controller模型绑定期间,可以随时调用ConversionService进行类型转换。如果没有显式配置,Spring MVC会为常见类型(例如数字Number和日期Date)自动注册一个默认的formatter和converter。

    为了使用默认的格式化规则,不在Spring MVC配置文件中添加自定义配置即可。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <mvc:annotation-driven/>
    
    </beans>
    

    只需要配置一行<mvc:annotation-driven />,Spring会自动加载用于数字和日期的默认formatter,包括@NumberFormat和@DateTimeFormat注解。如果classpath中包含Joda Time,那么完全支持Joda Time格式化库也被加载。

    如果使用自定义的formatter和converter,可以注入自定义的ConversionService实例。在Spring配置文件中设置conversion-service属性,类型为 FormatteringConversionServiceFactoryBean,设置自定义的converter、formatter或者FormatterRegistrar作为conversion-service的属性。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <mvc:annotation-driven conversion-service="conversionService"/>
    
        <bean id="conversionService"
              class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
            <property name="converters">
                <set>
                    <bean class="org.example.MyConverter"/>
                </set>
            </property>
            <property name="formatters">
                <set>
                    <bean class="org.example.MyFormatter"/>
                    <bean class="org.example.MyAnnotationFormatterFactory"/>
                </set>
            </property>
            <property name="formatterRegistrars">
                <set>
                    <bean class="org.example.MyFormatterRegistrar"/>
                </set>
            </property>
        </bean>
    
    </beans>
    

    7.7 配置全局date&time格式

    没有被@DateTimeFormat注解的date和time字段默认使用DateFormat.SHORT全局样式。开发者可以按照自己的需求重新定义全局格式。

    Spring没有注册默认的formatter,你需要确保手动注册所有的formatter。使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar
    还是org.springframework.format.datetime.DateFormatterRegistrar类,取决你是否使用了Joda Time库。

    例如下面的Java配置会注册一个'yyyyMMdd'的全局格式,不依赖Joda Time库。

    @Configuration
    public class AppConfig {
        @Bean
        public FormattingConversionService conversionService() {
            // Use the DefaultFormattingConversionService but do not register defaults
            DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
            // Ensure @NumberFormat is still supported
            conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());      
            // Register date conversion with a specific global format 
            DateFormatterRegistrar registrar = new DateFormatterRegistrar(); 
            registrar.setFormatter(new DateFormatter("yyyyMMdd")); 
            registrar.registerFormatters(conversionService);
            return conversionService;  
        } 
    }
    

    如果你倾向于基于XML的配置,你可以使用FormattingConversionServiceFactoryBean。下面是相同的样例,使用Joda Time。

    <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xsi:schemaLocation="         http://www.springframework.org/schema/beans         http://www.springframework.org/schema/beans/spring-beans.xsd>
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    	<property name="registerDefaultFormatters" value="false" />
    	<property name="formatters">
    		<set>
    			<bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" /> 
    		</set>
    	</property>
    	<property name="formatterRegistrars">
    		<set>
    			<bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
    				<property name="dateFormatter">
    					<bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
    						<property name="pattern" value="yyyyMMdd"/>
    					</bean>
    				</property>
    			</bean>
    		</set>
    	</property>
    </bean>
    </beans>
    

    在Spring MVC中应用的情况下,要显式配置conversion service。如果使用基于Java的@Configuration注解,需要扩展WebMvcConfigurationSupport类,覆盖mvcConversionService()方法。如果使用XML配置文件,需要使用'mvc:annotation-driven'元素的'conversion-service'属性。查阅7.6.5章节“在Spring MVC中配置格式化”获取更详细的信息。

    7.8 Spring 3 Validation

    Spring 3 增强了验证支持库。第一,完全支持JSR-303 Bean Validation API;第二,当使用代码方式时,Spring的DataBinder除了绑定对象,还可以验证对象。第三,Spring MVC现在已经支持声明式验证@Controller输入。

    7.8.1 JSR-303 Bean Validation API概览

    JSR-303 为Java平台验证定义了验证约束声明和元数据模型。使用该API,通过声明式验证约束来注解域模型(Domain Model)属性,运行时平台会强制执行。可以使用API中的内置约束,也可以定义自己的约束。

    下面用一个简单的包含两个属性的PersonForm模型演示说明。

    public class PersonForm {
         private String name;
         private int age;
    }
    

    JSR-303允许对这些属性定义声明式验证约束。

    public class PersonForm {
         @NotNull
         @Size(max=64)
         private String name;
         @Min(0)
         private int age;
    }
    

    当该类的实例被JSR-303 Validator验证时,这些约束就会执行。

    如果需要了解JSR-303的更多信息,请浏览Bean Validation Specification(Bean验证规范)。如果需要了解默认参考实现,请浏览Hibernate Validator(Hibernate验证器)。学习如何将JSR-303实现框架设置为Spring Bean,请阅读下面章节。

    7.8.2 配置Bean Validation Implementation

    Spring完全支持JSR-303 Bean Validation API。这包括支持方便的注入JSR-303实现类为Spring Bean,可以按需将javax.validation.ValidatorFactory或者javax.validation.Validator注入到应用程序中。

    使用LocalValidatorFactoryBean来配置一个默认的JSR-303实现类作为Spring Bean。

    <bean id="validator"       class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
    

    以上的基本配置会使用默认的引导机制触发JSR-303初始化。将JSR-303提供者(例如Hibernate Validation)的库文件加入classpath,它会被自动检测。

    注入Validator

    LocalValidatorFactoryBean实现了javax.validation.ValidatorFactory接口和javax.validation.Validator接口,以及Spring的org.springframework.validation.Validator接口。可以将上述任意一个接口注入到需要验证的Bean中,来启动验证逻辑。

    如果你希望直接使用JSR-303 API,注入javax.validation.Validator。

    import javax.validation.Validator;
    
    @Service
    public class MyService {
    
        @Autowired
        private Validator validator;
    

    如果你的Bean需要使用Spring Validator API,注入org.springframework.validation.Validator
    import org.springframework.validation.Validator;

    @Service
    public class MyService {
    
        @Autowired
        private Validator validator;
    
    }
    

    配置自定义的约束

    每个JSR-303验证约束包含两部分。第一部分,@Constraint注解,声明约束,以及可配置的属性。第二部分,javax.validation.ConstraintValidator接口的实现,实现约束行为。为了将实现与声明关联起来,每一个@Constraint注解指向一个对应的ValidationConstraint实现类。在运行时,当在域模型中遇到约束注解后,ConstraintValidatorFactory就实例化一个ConstraintValidator。

    默认LocalValidatorFactoryBean配置一个SpringConstraintValidatorFactory,使用Spring创建ConstraintValidator实例。与普通Spring Bean一样,可以在自定义的ConstraintValidator中使用依赖注入。

    下面是一个自定义的@Constraint声明,后面跟着相关联的ConstraintValidator实现,使用Spring进行依赖注入。

    @Target({ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy=MyConstraintValidator.class)
    public @interface MyConstraint {
    }
    import javax.validation.ConstraintValidator;
    
    public class MyConstraintValidator implements ConstraintValidator {
    
        @Autowired;
        private Foo aDependency;
    
        ...
    }
    

    正如你看到的,ConstraintValidator实现类中可以像其它Spring Bean一样使用Spring的依赖注入(@Autowired关键字)。

    额外的配置选项

    默认的LocaValidatorFactoryBean配置对于绝大多数情况是足够的。不同的JSR-303构造器还有一些其它配置项,如message interpolation和traversal resolution。对于这些选项的具体意义,可以查看LocalValidatorFactoryBean 的JavaDoc。

    7.8.3 配置DataBinder

    从Spring 3开始,DataBinder实例可以配置一个Validator。一旦配置,Validator可以通过binder.validate()触发。任何验证错误都会自动添加到binder的BindingResult。
    当使用代码方式使用DataBinder时,在绑定一个目标对象后,可以触发验证逻辑。

    Foo target = new Foo();
    DataBinder binder = new DataBinder(target);
    binder.setValidator(new FooValidator());
    
    // bind to the target object
    binder.bind(propertyValues);
    
    // validate the target object
    binder.validate();
    
    // get BindingResult that includes any validation errors
    BindingResult results = binder.getBindingResult();
    

    通过dataBinder.addValidators和dataBinder.replaceValidators,DataBinder可以配置多个Validator实例。对DataBinder实例中配置的局部Spring Validator和已配置的全局JSR-303 Bean Validator进行合并时,很有用。浏览“通过Spring MVC配置Validator”章节。

    7.8.4 Spring MVC 3 Validation

    由Spring 3开始,Spring MVC可以自动验证@Controller输入。在之前的版本,需要开发者手动触发验证逻辑。

    触发@Controller输入验证

    为了触发@Controller输入验证,为输入参数配置注解,@Valid:

    @Controller
    public class MyController {
    
        @RequestMapping("/foo", method=RequestMethod.POST)
        public void processFoo(@Valid Foo foo) { /* ... */ }
    

    在绑定之后,Spring MVC会验证@Valid对象,因为一个合适的Validator已经被配置。

    注意:@Valid注解是标准JSR-303 Bean Validation API中的一部分,不是Spring特有的。

    通过Spring MVC配置Validator

    当@Valid方法参数配置完成后,Validator实例可以通过两种方式配置。第一种,在@Controller的@InitBinder回调中调用binder.setValidator(Validator);这样做可以为每一个@Controller类配置一个 Validator实例。

    @Controller
    public class MyController {
    
        @InitBinder
        protected void initBinder(WebDataBinder binder) {
            binder.setValidator(new FooValidator());
        }
    
        @RequestMapping("/foo", method=RequestMethod.POST)
        public void processFoo(@Valid Foo foo) { ... }
    
    }
    

    第二种,可以在全局WebBindingInitializer中调用setValidator(Validator),可以为所有@Controller配置一个Validator实例。可以使用Spring MVC namespace很方便的配置。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <mvc:annotation-driven validator="globalValidator"/>
    
    </beans>
    

    同时应用全局和局部验证器(validator),按上一步骤配置全局Validator,然后按下面步骤配置局部Validator。

    @Controller
    public class MyController {
    
        @InitBinder
        protected void initBinder(WebDataBinder binder) {
            binder.addValidators(new FooValidator());
        }
    
    }
    

    通过Spring MVC配置JSR-303 Validator

    通过JSR-303,一个javax.validation.Validator实例可以验证所有声明验证约束的模型对象。只需要在classpath中增加JSR-303 Provider的包,例如Hibernate Validator,Spring MVC就会检测到,并自动为所有Controller提供JSR-303支持。

    Spring MVC配置支持JSR-303如下所示:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <!-- JSR-303 support will be detected on classpath and enabled automatically -->
        <mvc:annotation-driven/>
    
    </beans>
    

    使用最少配置,当遇到@Valid @Controller输入时,由JSR-303 provider进行验证。反过来,JSR-303会强制对声明验证的输入进行约束。当BindingResult中有错误时,ConstraintValidation会自动显示,通过标准的Spring MVC 表单标签渲染。

  • 相关阅读:
    jquery摘要
    一步一步学Linq to sql系列文章
    公布一些常用的WebServices
    ASP.NET AJAX入门系列(8):自定义异常处理
    jquery制作图片幻灯片插件
    ASP.NET AJAX入门系列(2):使用ScriptManager控件
    ClientScript.GetCallbackEventReference几个参数的使用实例
    jquery中this的使用说明
    程序员的最后归宿究竟是什么?
    英语26个字母的日语读法
  • 原文地址:https://www.cnblogs.com/vimisky/p/4860866.html
Copyright © 2020-2023  润新知