Spring笔记 - 验证、数据绑定和类型转换

1. 验证Validation

1.1 基本用法

// Bean
public class Person {
    private String name;
    private int age;
    //setter & getter, constructor略
}
// Validator
public class PersonValidator implements Validator {
    public boolean supports(Class<?> aClass) {
        return Person.class.isAssignableFrom(aClass);
    }

    public void validate(Object o, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "person.name.null", "Person name cannot be null.");
        Person person = (Person) o;
        if (person.getAge() < 0)
            errors.reject("person.age.negative", "Age cannot be negative.");
    }
}
// 验证
@Test
public void testPersonValidator() {
    Person person = new Person("张三", -10);
    PersonValidator validator = new PersonValidator();
    Assert.assertTrue(validator.supports(Person.class));
    Errors errors = new DirectFieldBindingResult(person, "person");
    //亦可使用ValidationUtils.invokeValidator(...)
    validator.validate(person, errors);
	
    for (ObjectError objectError : errors.getAllErrors()) {
        System.out.println("Error Code is: " + objectError.getCode() + " | " + "Error DefaultMessage is: " +  objectError.getDefaultMessage());
    }
}

// web容器环境的用法在SpringMVC部分介绍


1.2 在消息格式化中进行验证

[参考]

#properties消息文件
person.age.negative=\u5e74\u9f84\u4e0d\u80fd\u5c0f\u4e8e\u0030+
person.name.null=\u7528\u6237\u540d\u4e0d\u80fd\u4e3a\u7a7a
// 自定义MessageCodesResolver 
@Component
public class MyMessageCodesResolver implements MessageCodesResolver {
    @Autowired
    ApplicationContext context;

    public String[] resolveMessageCodes(String errorCode, String objectName) {
        return new String[]{context.getMessage(errorCode, null, null)};
    }

    public String[] resolveMessageCodes(String errorCode, String objectName, String field, Class<?> fieldType) {
        return resolveMessageCodes(errorCode, objectName);
    }
}
// 验证
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(person, "person");
bindingResult.setMessageCodesResolver(myMessageCodesResolver);
ValidationUtils.invokeValidator(new PersonValidator(), person, bindingResult);

for (ObjectError objectError : bindingResult.getAllErrors()) {
    System.out.println("Error Code is: " + objectError.getCode() + " | " + "Error DefaultMessage is: " +  objectError.getDefaultMessage());
}


1.3 BeanValidation规范JSR303, JSR409的支持

- Spring完整支持JSR303 (BeanValidation 1.0),该规范定义了基于注解方式的JavaBean验证元数据模型和API,也可以通过XML进行元数据定义,但注解将覆盖XML的元数据定义。

- Spring也支持JSR409 (BeanValidation 1.1),该规范标准化了Java平台的约束定义、描述和验证,其实现例如Hibernate Validator

- JSR303主要是对JavaBean进行验证,如方法级别(方法参数/返回值)、依赖注入等的验证是没有指定的。因此又有了JSR-349规范的产生。

[参考]

1.3.1 JSR303

JSR-303原生支持的限制有如下几种

限制 说明
@Null
@NotNull
@AssertFalse
@AssertTrue
@DecimalMax(value) 不大于指定值的数字
@DecimalMin(value) 不小于指定值的数字
@Digits(integer,fraction) 小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Max(value) 不大于指定值的数字
@Min(value) 不小于指定值的数字
@Pattern(value) 符合指定的正则表达式
@Size(max,min) 字符长度必须在min到max之间
@Future 将来的日期
@Past 过去的日期

1.3.2 自定义限制

// 定义限制
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MoneyValidator.class)
public @interface Money {
    String message() default "不是金额形式";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 定义限制验证器
public class MoneyValidator implements ConstraintValidator<Money, Double> {
    private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金额的正则表达式
    private Pattern moneyPattern = Pattern.compile(moneyReg);
 
    public boolean isValid(Double value, ConstraintValidatorContext arg1) {
       if (value == null)
           return true;
       return moneyPattern.matcher(value.toString()).matches();
    }
}

1.3.3 配置Bean Validation Provider

<!-- 配置LocalValidatorFactoryBean,Spring会自动查找并加载类路径下的provider,例如Hibernate Validator -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

<!-- 配置后,就可以通过注入validator的方式使用validator -->

1.3.4 Spring驱动的方法验证

对方法的参数、返回值进行验证 [参考]

// 没有方法验证时的做法
public UserModel get(Integer uuid) {
    //前置条件
    Assert.notNull(uuid);
    Assert.isTrue(uuid > 0, "uuid must lt 0");
    //获取 User Model
    UserModel user = getUserModel(uuid); //从数据库获取
    //后置条件
    Assert.notNull(user);
    return user;
}

// 有方法验证时的做法
// a. 使用方法验证
@Validated // 告诉MethodValidationPostProcessor此Bean需要开启方法级别验证支持
public class UserService {
    public @NotNull UserModel getUserModel(@NotNull @Min(value = 1) Integer uuid) { //声明前置条件/后置条件
        if(uuid > 100) {//方便后置添加的判断(此处假设传入的uuid>100 则返回null)
            return null;
        }
        return getUserModel(uuid); //从数据库获取
    }
}
// b. 配置方法验证的后处理器
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
// c. 测试用例
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:spring-config-method-validator.xml"})
public class MethodValidatorTest {
    @Autowired
    UserService userService;
    @Test
    public void testConditionSuccess() { // 正常流程 
        userService.getUserModel(1);
    }
    @Test(expected = org.hibernate.validator.method.MethodConstraintViolationException.class)
    public void testPreCondtionFail() { // 错误的uuid(即前置条件不满足)
        userService.getUserModel(0);
    }
    @Test(expected = org.hibernate.validator.method.MethodConstraintViolationException.class)
    public void testPostCondtionFail() { // 不满足后置条件的返回值
        userService.getUserModel(10000);
    }
}


1.4 在数据绑定中使用validator

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator1());
binder.addValidators(new FooValidator2());
binder.replaceValidators(new FooValidator3());
// 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();


2. 操作Bean

2.1 BeanWrapper

- 封装一个bean的行为,诸如设置和获取属性值等

- 根据JavaDoc中的说明,BeanWrapper提供了设置和获取属性值(单个的或者是批量的),获取属性描述信息、查询只读或者可写属性等功能。不仅如此,BeanWrapper还支持嵌套属性,设置子属性的值。BeanWrapper无需任何辅助代码就可以支持标准JavaBean的PropertyChangeListeners和VetoableChangeListeners。此外,还支持设置索引属性。通常不直接使用BeanWrapper而是使用DataBinder 和BeanFactory

// 被操作的类
class Engine {
    private String name;
    //setter & getter ...
}
// 被操作的类
class Car {
    private String name;
    private Engine engine;
    //setter & getter ...
}
// 创建并设置Bean
BeanWrapper engine = BeanWrapperImpl(new Engine());
engine.setPropertyValue("name", "N73B68A"); // 也可以这样设置 engine.setPropertyValue(new PropertyValue("name", "N73B68A");
// 创建并设置Bean
BeanWrapper car = BeanWrapperImpl(new Car());
car.setPropertyValue("name", "劳斯莱斯幻影");
car.setPropertyValue("engine", engine.getWrappedInstance());

// 获取嵌套属性
String engineName = (String) car.getPropertyValue("engine.name");

2.2 PropertyEditor

- Spring大量使用了PropertyEditor以在Object和 String之间进行转化

- PE本来是Java为IDE可视化设置JavaBean属性而准备的,Spring对此进行了封装以简化使用[参考]

2.2.1 Spring内建属性编辑器

类型 内建PropertyEditor (是否已在BeanWrapperImpl注册)
基础数据

ByteArrayProperty (Y)、CustomBoolean (Y)、CustomDate (N)、CustomNumber (Y)

集合 CustomCollection (?)
资源 Class (Y)、File (Y)、InputStream (Y)、Locale (Y)、Pattern (?)、Properties (Y)、URL (Y)、StringTrimmer (N)

2.2.2 自定义属性编辑器

2.2.2.1 扩展PropertyEditorSupport 

// 定义Editor
class EngineEditor extends PropertyEditorSupport {
    public void setAsText(String text){
        if(text == null)
            throw new IllegalArgumentException("设置的字符串格式不正确");
        Engine engine = new Engine();
        engine.setName(text);
        setValue(engine);
    }
}
// 注册Editor
// 如果自定义Editor和被处理的类在同一包下面,则无需xml注册,会被自动识别
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="x.y.z.Engine" value="x.y.z.EngineEditor"/>
        </map>
    </property>
</bean>

// 使用Editor
<bean id="car" class="x.y.z.Car" p:name="劳斯莱斯幻影">
    <property name="engine" value="N73B68A" />
</bean>

2.2.2.2 实现PropertyEditorRegistrar接口

在不同情况下(如编写一个相应的注册器然后在多种情况下重用它)需要使用相同的属性编辑器时该接口特别有用

// 现在Java代码里面注册
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
    public void registerCustomEditors(PropertyEditorRegistry registry) {
        registry.registerCustomEditor(Engine.class, new EngineEditor());
    }
}
<!-- 再到XML注册 -->
<bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>


3. 类型转换

3.1 概述

- Spring容器使用转换器进行属性注入;SpEL和DataBinder使用转换器绑定值

- 不同于Property的Object和String之间的转换,类型转换可以有更多的格式互转

- 转换器Converter可实现以下接口:

  • 非泛型的强类型转换用Converter接口

  • 一个类型转成指定基类的子类型用ConverterFactory接口

  • 多个源类型转成多个目标类型用GenericConverter接口,优先用前面两个

- ConversionService接口实现作为无状态的工具在运行时提供转换服务,可供多个线程共享,可在其中设置各种转换器.


3.2 Converter接口

将一个源类型转换成一个目标类型

public interface Converter<S, T> {
    T convert(S source);
}

final class StringToCarConverter implements Converter<String, Car> {
    public Car convert(String source) {
        return new Car(source);
    }
}


3.3 ConverterFactory接口

一个源类型转成指定基类的子类型

public interface ConverterFactory<S, R> {
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

// given Car & Truck extends Auto
final class StringToAutoConverterFactory implements ConverterFactory<String, Auto> {
    public <T extends Auto> Converter<String, T> getConverter(Class<T> targetType) {
        if(targetType.getType() == Car.class)
            return new StringToCarConverter();
        else if(targetType.getType() == Truck.class)
            return new StringToTruckConverter();
        else
            throw new ConversionFailedException(String.getType(), targetType, null, new Throwable("不支持的转换类型"));
    }
    private final class StringToCarConverter implements Converter<String, Car> {
        public Car convert(String source) {
            return new Car(source);
        }
    }
    // StringToTruckConverter ...
}


3.4 GenericConverter接口

- 多个源类型转成多个目标类型

- ConditionalGenericConverter接口继承该接口,增加了boolean matches(...)方法

public interface GenericConverter {
    Set<ConvertiblePair> getConvertibleTypes();
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
    
    public static final class ConvertiblePair {
        public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
            Assert.notNull(sourceType, "Source type must not be null");
            Assert.notNull(targetType, "Target type must not be null");
            this.sourceType = sourceType;
            this.targetType = targetType;
        }
        private final Class<?> sourceType;
        private final Class<?> targetType;
        // getter & setter
    }
 
}

final class GiftGenericConverter implements GenericConverter {
   public Set<ConvertiblePair> getConvertibleTypes() {
       Set<ConvertiblePair> pairs = new HashSet<ConvertiblePair>();
       paris.add(new ConvertiblePair(String.class, Flower.class));
       paris.add(new ConvertiblePair(Integer.class, Toy.class));
       return pairs;
   }
   Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
      if (source == null || sourceType == TypeDescriptor.NULL || targetType == TypeDescriptor.NULL) {  
          throw new ConversionFailedException(sourceType, targetType, null,
              new IllegalArgumentException("A null value cannot be assigned to a primitive type"));
      }
      if(targetType.getType() == String.class)
          return new Flower(source);
      else if(targetType.getType() == Integer.class)
          return new Toy(source);
   }
}


3.5 Conversion Service

- 无状态的工具在运行时提供转换服务,可供多个线程共享,可在其中设置各种转换器

- 内置GenericConversionService实现ConvensionService接口

- 也可以配置ConversionServiceFactoryBean提供转换服务

3.5.1 GenericConversionService

<bean id="conversionService" class="org.springframework.core.convert.support.GenericConversionService"/>
// 注入转换Bean就可以使用转换服务了
@Autowired
ConversionService conversionService;
public void doSth() {
    conversionService.convert(source, targetType);
    List<Integer> input = ....
    conversionService.convert(input,
        TypeDescriptor.forObject(input), // List<Integer> type descriptor
        TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
}

// 也可以创建转换器实例,通常不需要

3.5.2 ConversionServiceFactoryBean

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="x.y.z.StringToCarConverter"/>
            <bean class="x.y.z.StringToAutoConverterFactory"/>
            <bean class="x.y.z.GiftGenericConverter"/>
        </set>
    </property>
</bean>
// 使用方法同GenericConversionService


4. 格式化

4.1 概述

Spring格式化Formatter与Converter类似,但可以指定Locale,根据不同的Locale进行不同的双向格式化 (print/parse)


4.2 Formatter接口

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

public final class MyDateFormatter implements Formatter<Date> {
    public String print(Date object, Locale locale) {
        return new SimpleDateFormat("yyyy-MM-dd", locale).format(object);
    }
    public Date parse(String text, Locale locale) throws ParseException {
        return new SimpleDateFormat("yyyy-MM-dd", locale).parse(text);
    }
}


4.3 基于注解的格式化

[参考]

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

// a. 创建注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
}

// b. 要格式化的模型
public class PhoneNumberModel {
    private int areaCode, userNumber;
    // getter & setter, constructor ... 
}

// c. 实现工厂接口
public class PhoneNumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<PhoneNumber> {
    public Set<Class<?>> getFieldTypes() {
        return fieldTypes;
    }
    public Parser<?> getParser(PhoneNumber annotation, Class<?> fieldType) {
        return formatter;
    }    
    public Printer<?> getPrinter(PhoneNumber annotation, Class<?> fieldType) {
        return formatter;
    }
    
    private final Set<Class<?>> fieldTypes;
    private final PhoneNumberFormatter formatter;
    public PhoneNumberFormatAnnotationFormatterFactory() {
        Set<Class<?>> set = new HashSet<Class<?>>();
        set.add(PhoneNumberModel.class);
        this.fieldTypes = set;
        this.formatter = new PhoneNumberFormatter(); // 之前定义的Formatter实现
    }
}

// d. 在需要格式化的字段前面加注解
public class contact {
    @PhoneNumber
    private PhoneNumberModel phoneNumber;
    // other fields ...
}

// e. 测试使用。这个用例有些牵强,更多是注册后,在属性注入、SpEL、数据绑定时,由容器调用自动完成格式化
@Test
public void test() throws SecurityException, NoSuchFieldException {
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); // 创建格式化服务
    conversionService.addFormatterForFieldAnnotation(new PhoneNumberFormatAnnotationFormatterFactory()); // 添加自定义的注解格式化工厂
        
    TypeDescriptor phoneNumberDescriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("phoneNumber"));
    TypeDescriptor stringDescriptor = TypeDescriptor.valueOf(String.class);
    
    PhoneNumberModel value = (PhoneNumberModel) conversionService.convert("010-12345678", stringDescriptor, phoneNumberDescriptor); // 解析字符串"010-12345678"--> PhoneNumberModel
    ContactModel contact = new ContactModel();
    contact.setPhoneNumber(value);
        
    Assert.assertEquals("010-12345678", conversionService.convert(contact.getPhoneNumber(), phoneNumberDescriptor, stringDescriptor)); // 格式化PhoneNumberModel-->"010-12345678"
}


4.4 注册Formatter

- 实现FormatterRegistry接口,或使用内置实现FormattingConversionService,通常使用FormattingConversionServiceFactoryBean进行配置

- 实现FormatterRegistrar接口

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <list>
            <bean class="x.y.z.PhoneNumberFormatAnnotationFormatterFactory"/>
        </list>
    </property>
    <property name="converters">
        <set>
            <bean class="org.example.MyConverter"/>
        </set>
    </property>
    <property name="formatterRegistrars">
        <set>
            <bean class="org.example.MyFormatterRegistrar"/>
        </set>
    </property>
</bean>


4.5 注册全局日期和时间格式

Spring默认使用DateFormat.SHORT进行日期和时间格式化,在DefaultFormattingConversionService未被注册的情况下,可以自定义全局日期和时间格式

// a. 以JavaConfig的方式注册全局日期格式yyyyMMdd
@Bean
public FormattingConversionService conversionService() {
    // 使用但不注册DefaultFormattingConversionService
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
    // 确保@NumberFormat被支持
    conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
    // 注册全局格式
    DateFormatterRegistrar registrar = new DateFormatterRegistrar();
    registrar.setFormatter(new DateFormatter("yyyyMMdd"));
    registrar.registerFormatters(conversionService);

    return conversionService;
}
<!-- b. 以xml的方式注册全局日期格式yyyyMMdd,且用到了Joda-Time第三方库
<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>


你可能感兴趣的:(Spring笔记 - 验证、数据绑定和类型转换)