Spring Framework Validation、Data Binding, and Type Conversion
in with 0 comment

Spring Framework Validation、Data Binding, and Type Conversion

in with 0 comment

注意

注意: 本章的很多内容需要结合 Spring Framework Web MVC 进行了解,这里仅介绍 Spring Framework 中的实现逻辑,更多实际使用方式请后续查阅 Web MVC

参数校验

基础案例

Spring 提供了对 javax.validation 规范的整合,但需要你引入如下依赖。
其中 validation-apiJava 规定的接口
hibernate-validator 是对接口的实现

<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
  <version>1.1.0.Final</version>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>5.2.0.Final</version>
</dependency>

引入依赖后,你需要进行如下配置,LocalValidatorFactoryBean 是 Spring 提供的默认验证器,其作用是集成 javax.validation 功能

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

配置好之后,您可以采用较为原始的方式使用参数校验功能

注意: 这里使用的是 javax.validation.Validator;

@Service
public class TestService {

    @Resource
    private TestBean testBean;

    @Resource
    private Validator validator;

    public void running () {
        System.out.println(validator.validate(testBean, Default.class));
    }
}

@Getter
@Setter
@ToString
@Component
public class TestBean {

    @Size(min = 50, max = 100)
    @Value("${bean-message}")
    private String message;

}

输出结果

[ConstraintViolationImpl{interpolatedMessage='个数必须在50和100之间', propertyPath=message, rootBeanClass=class org.example.demo.model.TestBean, messageTemplate='{javax.validation.constraints.Size.message}'}]

自定义校验规则

我们需要创建如下注解, 其中 @Constraint 是由 java 提供,表示该注解用于自定义校验

案例中的每一个参数,如 message 等都不可缺少,否则会报错

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy= MyConstraintValidator.class)
public @interface MyConstraint {

    String message() default "校验错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

我们还需要一个具体的校验逻辑实现

public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {

    @Override
    public void initialize(MyConstraint constraintAnnotation) {}

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return false;
    }
}

最终使用

@Getter
@Setter
@ToString
@Component
public class TestBean {

    @MyConstraint
    private String message;

}

@Service
public class TestService {

    @Resource
    private TestBean testBean;

    @Resource
    private Validator validator;

    public void running () {
        System.out.println(validator.validate(testBean, Default.class));
    }
}

输出结果

[ConstraintViolationImpl{interpolatedMessage='校验错误', propertyPath=message, rootBeanClass=class org.example.demo.model.TestBean, messageTemplate='校验错误'}]

开启方法级校验

该功能依赖于 aop 功能,由于 spring-aop 已被 IOC 关联依赖,所以这里仅引入 aspects

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.3.15</version>
</dependency>

首先我们需要配置开启方法级校验

@Configuration
public class AppConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

之后进行使用,这里有两个关键注解

@Service
@Validated
public class TestService {

    public void running (@Valid TestBean testBean) {
        System.out.println(testBean);
    }
}

@Configuration
@Import(TestConfiguration.class)
public class TestApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestApplication.class);

        TestService service = context.getBean("testService", TestService.class);

        try {
            service.running(new TestBean());
        } catch (ConstraintViolationException e) {
            System.out.println(e.getConstraintViolations());
        }

        context.close();
    }
}

输出结果

[ConstraintViolationImpl{interpolatedMessage='校验错误', propertyPath=running.arg0.message, rootBeanClass=class org.example.demo.service.TestService, messageTemplate='校验错误'}]

DataBinder

从 Spring3 开始,你可以使用 DataBinder 实现基于 org.springframework.validation.Validator 的校验逻辑

class TestBeanValidator implements Validator {

    /**
     * 表示该校验器所支持的类型
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return TestBean.class.equals(clazz);
    }

    /**
     * 校验逻辑
     */
    @Override
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmptyOrWhitespace(e, "message", "message.required");
    }

}

public void running () {
    DataBinder binder = new DataBinder(new TestBean());

    binder.setValidator(new TestBeanValidator());
    binder.validate();

    BindingResult results = binder.getBindingResult();
    System.out.println(results);
}

BeanWrapper

BeanWrapper 是一个用于封装 Bean 的机制

@Getter
@Setter
@ToString
public class TestBeanWrapper {

    private String name;

    private int age;

    private TestBean bean;

}

public void running () {
    TestBeanWrapper testBeanWrapper = new TestBeanWrapper();

    BeanWrapper beanWrapper = new BeanWrapperImpl(testBeanWrapper);
    beanWrapper.setPropertyValue("name", "this is a test name");
    beanWrapper.setPropertyValue(new PropertyValue("age", 13));
    beanWrapper.setPropertyValue("bean", new TestBean());
    beanWrapper.setPropertyValue("bean.message", "this is a test message");

    System.out.println(testBeanWrapper);
}

PropertyEditor

PropertyEditor 是一个用于 Bean 参数转换的机制

@Data
class Test {

    private Integer age;

}

public void test () {
    BeanWrapper beanWrapper = new BeanWrapperImpl(new Test());
    beanWrapper.registerCustomEditor(Integer.class, new PropertyEditorSupport() {
        @Override
        public void setValue(Object value) {
            super.setValue(50);
        }
    });
    beanWrapper.setPropertyValue("age", 20);

    System.out.println(beanWrapper.getWrappedInstance());
}

在上述案例中,我们为 age 字段设置了值为 20,但由于我们自定义 Integer 的解析器,所以最终 age 字段被设置为了 50

类型转换 Converter

Spring 提供了一些通用的类型转换功能,你可以在任何地方使用

使用 Converter 及 ConverterFactory

在下述案例中,我们实现了一个转换器 Converter
之后又实现了一个转换器工厂 StringToEnumConverterFactory
从而利用工厂模式将 Integer 转换为了 String

class StringToEnumConverterFactory implements ConverterFactory<Integer, String> {

    @Override
    public <T extends String> Converter<Integer, T> getConverter(Class<T> targetType) {
        return new Converter<Integer, T>() {

            @Override
            public T convert(Integer source) {
                return (T) source.toString();
            }
        };
    }

}

public void test () {
    String str = new StringToEnumConverterFactory().getConverter(String.class).convert(50);
    System.out.println(str);
}

除了原始的 Converter, 你也可以使用 GenericConverterConditionalGenericConverter ,他们可以提供更加复杂的逻辑

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

使用 ConversionService

ConversionService 结构如下

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

我们可以在 @Configuration 中配置一个全局的类型转换器,然后可以在任何地方使用它。

如下案例中,我们注册了一个全局 ConversionService ,并且为它添加了一个 ConverterFactory。(当然你也不添加,DefaultConversionService 提供了很多转换器,基本覆盖了日常使用的类型,具体提供了什么,点开源码查阅吧,这里的源代码很简单)

@Configuration
@ComponentScan("org.example.demo")
@PropertySource("classpath:/application.properties")
public class TestConfiguration {

    @Bean
    public ConversionService conversionService () {
        DefaultConversionService service = new DefaultConversionService();

        service.addConverterFactory(new ConverterFactory<Integer, String>() {
            @Override
            public <T extends String> Converter<Integer, T> getConverter(Class<T> targetType) {
                return new Converter<Integer, T>() {
                    @Override
                    public T convert(Integer source) {
                        return (T) source.toString();
                    }
                };
            }
        });

        return service;
    }

}

@Service
public class TestService {

    @Resource
    private ConversionService conversionService;

    public void running () {
        System.out.println(conversionService.convert(50, String.class));
    }
}

格式化 Formatter

除了 ConverterSpring 还提供了 Formatter 用于格式化数据

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}
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 的使用方式与 Conversion 类似,这里不进行赘述,仅展示一个笼统的案例,具体案例请查阅官方

@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 JSR-310 date conversion with a specific global format
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        // Register date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}