20191105 《Spring5高级编程》笔记-第10章

第10章 使用类型转换和格式化进行验证

在应用程序开发中,数据验证通常与转换和格式化一起被提及。因为数据源的格式很可能与应用程序中所使用的格式不同。

名词缩写:

SPI(Service Provider Interface):服务提供接口

10.1 依赖项

<!--validation-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

10.4 Spring类型转换介绍

在Spring3中,引入了一个通用类型转换系统,该系统位于org.springframework.core.convert包中。除了提供PropertyEditor所支持的替代方法之外,还可以配置类型转换系统,从而在任何Java类型和POJO之间进行转换(PropertyEditor专注于将属性文件中的String表示转换为Java类型)。

10.4.1 实现自定义转换器

使用PropertyEditor

//-----JavaBean-----------------//
@Data
public class Singer {
    private String firstName;
    private String lastName;
    private DateTime birthDate;
    private URL personalSite;
}

//-----PropertyEdito相关-----------------//
public class DateTimeEditorRegistrar implements PropertyEditorRegistrar {
    private DateTimeFormatter dateTimeFormatter;

    public DateTimeEditorRegistrar(String dateFormatPattern) {
        dateTimeFormatter = DateTimeFormat.forPattern(dateFormatPattern);
    }

    @Override
    public void registerCustomEditors(PropertyEditorRegistry registry) {
        registry.registerCustomEditor(DateTime.class, new DateTimeEditor(dateTimeFormatter));
    }

    private static class DateTimeEditor extends PropertyEditorSupport {
        private DateTimeFormatter dateTimeFormatter;

        public DateTimeEditor(DateTimeFormatter dateTimeFormatter) {
            this.dateTimeFormatter = dateTimeFormatter;
        }

        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            setValue(DateTime.parse(text, dateTimeFormatter));
        }
    }
}

//-----XML配置-----------------//
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:p="http://www.springframework.org/schema/p"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
">

    <context:annotation-config/>

    <context:property-placeholder location="classpath:chapter10/editor.properties"/>

    <bean id="customEditorConfigurer" class="org.springframework.beans.factory.config.CustomEditorConfigurer"
          p:propertyEditorRegistrars-ref="propertyEditorRegistrarsList"/>

    <util:list id="propertyEditorRegistrarsList">
        <bean class="study.hwj.chapter10.editor.DateTimeEditorRegistrar">
            <constructor-arg value="${date.format.pattern}"/>
        </bean>
    </util:list>

    <bean id="eric" class="study.hwj.chapter10.entities.Singer" p:firstName="Eric" p:lastName="Clapton"
          p:birthDate="1945-03-30" p:personalSite="http://www.ericclapton.com"></bean>

    <bean id="countrySinger" class="study.hwj.chapter10.entities.Singer"
          p:firstName="${countrySinger.firstName}" p:lastName="${countrySinger.lastName}"
          p:birthDate="${countrySinger.birthDate}" p:personalSite="${countrySinger.personalSite}"></bean>
</beans>

//-----属性配置-----------------//
date.format.pattern=yyyy-MM-dd

countrySinger.firstName=John
countrySinger.lastName=Mayer
countrySinger.birthDate=1997-10-16
countrySinger.personalSite=http://johnmayer.com/

//-----测试程序-----------------//
@Slf4j
public class PropEditorDemo {
    public static void main(String[] args) {
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("classpath:chapter10/ac_editor.xml");

        Singer eric = ctx.getBean("eric", Singer.class);
        log.info("Eric==={}", eric);

        Singer countrySinger = ctx.getBean("countrySinger", Singer.class);
        log.info("countrySinger==={}", countrySinger);
    }
}

10.4.2 配置ConversionService

使用ConversionService

//-----Converter-----------------//
public class StringToDateTimeConverter implements Converter<String, DateTime> {
    private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
    private DateTimeFormatter dateFormat;
    private String datePattern = DEFAULT_DATE_PATTERN;

    public String getDatePattern() {
        return datePattern;
    }

    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }

    @PostConstruct
    public void init() {
        System.out.println("StringToDateTimeConverter...init...");
        dateFormat = DateTimeFormat.forPattern(datePattern);
    }

    @Override
    public DateTime convert(String source) {
        return dateFormat.parseDateTime(source);
    }
}

//-----Java配置类-----------------//
@PropertySource("classpath:chapter10/editor.properties")
@Configuration
public class ConversionConfig {
    @Value("${date.format.pattern}")
    private String dateFormatPattern;

    /**
     * 将${date.format.pattern}解析成yyyy-MM-dd
     * @return
     */
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public Singer john(@Value("${countrySinger.firstName}") String firstName, @Value("${countrySinger.lastName}") String lastName, @Value("${countrySinger.birthDate}") DateTime birthDate, @Value("${countrySinger.personalSite}") URL personalSite) {
        Singer singer = new Singer();
        singer.setFirstName(firstName);
        singer.setLastName(lastName);
        singer.setBirthDate(birthDate);
        singer.setPersonalSite(personalSite);
        return singer;
    }

    @Bean
    public ConversionServiceFactoryBean conversionService() {
        ConversionServiceFactoryBean conversionServiceFactoryBean = new ConversionServiceFactoryBean();
        Set<Converter> convs = new HashSet<>();
        convs.add(converter());
        conversionServiceFactoryBean.setConverters(convs);
        return conversionServiceFactoryBean;
    }

    @Bean
    public StringToDateTimeConverter converter() {
        StringToDateTimeConverter conv = new StringToDateTimeConverter();
        conv.setDatePattern(dateFormatPattern);
        return conv;
    }
}

//-----测试程序-----------------//
public class ConvServDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConversionConfig.class);
        Singer john = ctx.getBean("john", Singer.class);
        System.out.println(john);
        ctx.close();
    }
}

通过使用类ConversionServiceFactoryBean声明一个conversionService bean,从而指示Spring使用类型转换系统,如果没有定义转换服务bean,Spring将使用基于PropertyEditor的系统。
默认情况下,类型转换服务支持常用类型的转换,包括字符串、数字、枚举、集合、映射等。还支持基于PropertyEditor的系统将String转换为Java类型。

ConversionService接口的默认实现为org.springframework.core.convert.support.DefaultConversionService

容器添加PropertyEditor支持在refresh()prepareBeanFactory(beanFactory);
容器添加ConversionService支持在refresh()finishBeanFactoryInitialization(beanFactory);

10.4.3 任意类型之间的转换

//-----定义Converter -----------------//
public class SingerToAnotherSingerConverter implements Converter<Singer, AnotherSinger> {
    @Override
    public AnotherSinger convert(Singer singer) {
        AnotherSinger anotherSinger = new AnotherSinger();
        anotherSinger.setFirstName(singer.getLastName());
        anotherSinger.setLastName(singer.getFirstName());
        anotherSinger.setBirthDate(singer.getBirthDate());
        anotherSinger.setPersonalSite(singer.getPersonalSite());
        return anotherSinger;
    }
}

//-----Java配置类 -----------------//
@Configuration
public class ConvertObjectConfig {

    @Bean
    public Singer john() throws MalformedURLException {
        Singer singer = new Singer();
        singer.setFirstName("John");
        singer.setLastName("Mayer");
        singer.setBirthDate(converter().convert("1977-10-16"));
        singer.setPersonalSite(new URL("http://johnmayer.com/"));
        return singer;
    }

    @Bean
    public ConversionServiceFactoryBean conversionService() {
        ConversionServiceFactoryBean conversionServiceFactoryBean = new ConversionServiceFactoryBean();
        Set<Converter> convs = new HashSet<>();
        convs.add(converter());
        convs.add(singerConverter());
        conversionServiceFactoryBean.setConverters(convs);
        return conversionServiceFactoryBean;
    }

    @Bean
    public StringToDateTimeConverter converter() {
        return new StringToDateTimeConverter();
    }

    @Bean
    public SingerToAnotherSingerConverter singerConverter() {
        return new SingerToAnotherSingerConverter();
    }
}

//-----测试程序-----------------//
public class ConvertObjectDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConvertObjectConfig.class);

        Singer john = ctx.getBean("john", Singer.class);

        System.out.println(john);

        ConversionService conversionService = ctx.getBean(ConversionService.class);

        // 对象转换
        AnotherSinger anotherSinger = conversionService.convert(john, AnotherSinger.class);

        System.out.println(anotherSinger);

        // 字符串转数组
        String[] stringArray = conversionService.convert("a,b,c", String[].class);
        System.out.println(Arrays.toString(stringArray));

        // List转Set
        ImmutableList<String> list = ImmutableList.of("d", "e", "f");
        HashSet setString = conversionService.convert(list, HashSet.class);

        System.out.println(setString);
    }
}

10.5 Spring中的字段格式化

除类型转换系统外,Spring带来另一个重要功能 Formatter SPI 帮助配置字段格式化。
在 Formatter SPI 中,实现格式化器的主要接口是 org.springframework.format.Formatter 。Spring提供了一些常用类型的实现。

10.5.1 实现自定义格式化器

扩展 org.springframework.format.support.FormattingConversionServiceFactoryBean 类并提供自定义格式化器。 FormattingConversionServiceFactoryBean 是一个工厂类,可以方便的访问底层 FormattingConversionService 类(该类支持类型转换系统),以及根据每个字段类型所定义的格式化规则完成字段格式化。

FormattingConversionServiceConversionService 的实现类。

//------定义FormattingConversionServiceFactoryBean子类-------------------------//
@Component("conversionService")
public class ApplicationConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean {
    private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
    private DateTimeFormatter dateTimeFormatter;
    private String datePattern = DEFAULT_DATE_PATTERN;

    private Set<Formatter<?>> formatters = new HashSet<>();

    public String getDatePattern() {
        return datePattern;
    }

    @Autowired(required = false)
    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }

    @PostConstruct
    public void init() {
        dateTimeFormatter = DateTimeFormat.forPattern(datePattern);
        formatters.add(getDateTimeFormatter());
        setFormatters(formatters);
    }

    public Formatter<DateTime> getDateTimeFormatter() {
        return new Formatter<DateTime>() {
            @Override
            public DateTime parse(String text, Locale locale) throws ParseException {
                System.out.println(text);
                return dateTimeFormatter.parseDateTime(text);
            }

            @Override
            public String print(DateTime dateTime, Locale locale) {
                return dateTimeFormatter.print(dateTime);
            }
        };
    }
}

//------定义Java配置类-------------------------//
@Configuration
@Import({ApplicationConversionServiceFactoryBean.class})
public class FormatterConfig {
    @Autowired
    private ApplicationConversionServiceFactoryBean conversionService;

    @Bean
    public Singer john() throws MalformedURLException, ParseException {
        Singer singer = new Singer();
        singer.setFirstName("John");
        singer.setLastName("Mayer");
        singer.setPersonalSite(new URL("http://johnmayer.com"));
        singer.setBirthDate(conversionService.getDateTimeFormatter().parse("1977-10-16", Locale.ENGLISH));
        return singer;
    }
}

//------测试程序-------------------------//
public class ConvFormatServDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FormatterConfig.class);

        Singer john = ctx.getBean("john", Singer.class);
        System.out.println(john);

        ConversionService conversionService = ctx.getBean("conversionService", ConversionService.class);
        System.out.println(conversionService.convert(john.getBirthDate(), String.class));
    }
}

10.6 Spring中的验证

Spring支持两种主要类型的验证。第一种验证类型是由Spring提供的,可以通过实现 org.springframework.validation.Validator 接口来创建自定义验证器。另一种类型是通过Spring对JSR-349(Bean Validation)的支持实现的。

10.6.1 使用Spring Validator接口

//-----------定义Validator--------//
@Configuration
@Component("singerValidator")
public class SingerValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Singer.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "firstName", "firstName.empty");
    }
}

//-----------测试程序--------//
public class SpringValidatorDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SingerValidator.class);

        Singer singer = new Singer();
        singer.setFirstName(null);
        singer.setLastName("Mayer");

        SingerValidator singerValidator = ctx.getBean("singerValidator", SingerValidator.class);
        BeanPropertyBindingResult result = new BeanPropertyBindingResult(singer, "John");

        ValidationUtils.invokeValidator(singerValidator, singer, result);

        List<ObjectError> errors = result.getAllErrors();
        errors.forEach(System.out::println);

    }
}

10.6.2 使用JSR-349 Bean Validation

Spring4开始对JSR-349(Bean Validation)提供全面支持。Bean Validation API在包 javax.validation.constraints 中以Java注解的形式定义了一组可应用于域对象的约束。另外,可以使用注解开发和应用自定义验证器(例如,类级验证器)。

通过使用Bean Validation API,可以避免耦合到特定的验证服务提供程序。

10.6.3 在Spring中配置Bean Validation支持

为了在Spring的ApplicationContext中配置对Bean Validation API的支持,可以在Spring的配置中定义一个类型为 org.springframework.validation.beanvalidation.LocalValidatorFactoryBean 的bean。

注意区分:javax.validation.Validatororg.springframework.validation.Validator

// -------定义要验证的JavaBean,加上验证注解---------------------------- //
@Data
public class VSinger {

    @NotNull
    @Size(min = 2, max = 60)
    private String firstName;

    private String lastName;

    @NotNull
    private Genre genre;

    private Gender gender;
}

// -------定义Service类---------------------------- //
@Service
public class SingerValidationService {
    @Autowired
    private Validator validator;

    public Set<ConstraintViolation<VSinger>> validateSinger(VSinger singer) {
        return validator.validate(singer);
    }
}

// -------定义Java配置类---------------------------- //
@Configuration
@Import({SingerValidationService.class})
public class ValidatorConfig {
    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

// -------测试程序---------------------------- //
public class Jsr349Demo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ValidatorConfig.class);

        SingerValidationService service = ctx.getBean(SingerValidationService.class);

        VSinger singer = new VSinger();
        singer.setFirstName("J");
        singer.setLastName("Mayer");
        singer.setGenre(null);

        validateSinger(singer, service);

        ctx.close();
    }

    private static void validateSinger(VSinger singer, SingerValidationService service) {
        Set<ConstraintViolation<VSinger>> violations = service.validateSinger(singer);
        listViolations(violations);
    }

    private static void listViolations(Set<ConstraintViolation<VSinger>> violations) {
        violations.forEach(violation -> {
            System.out.println("Validation error for property: 【" + violation.getPropertyPath() + "】 with value: 【" + violation.getInvalidValue() + "】 with error message: 【" + violation.getMessage() + "】");
        });
    }
}

10.6.4 创建自定义验证器

除了进行属性级验证之外,还可以应用类级验证。在Bean Validation API中,开发一个自定义验证器分两步。第一步是为验证器创建要给注解类型;第二步是开发实现验证逻辑的类。

第一步:
注解类型包含三个属性:

  • message属性定义违反约束条件时返回的消息(或错误代码。也可以在注解中提供默认消息。
  • group属性指定适用的验证组。可以将验证器分配给不同的组,并对特定组执行验证。
  • payload属性指定其他有效载荷对象(即实现了javax.validation.Payload 接口的类)。它允许将附加消息附加到约束上(例如,有效载荷对象可以指明违反约束的严重性)。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Constraint(validatedBy = CountrySingerValidator.class)
public @interface CheckCountrySinger {
    String message() default "Country Singer should have gender and xxx";

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

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

第二步:

public class CountrySingerValidator implements ConstraintValidator<CheckCountrySinger, VSinger> {
    @Override
    public void initialize(CheckCountrySinger constraintAnnotation) {

    }

    @Override
    public boolean isValid(VSinger singer, ConstraintValidatorContext context) {
        if (singer.getGenre() != null && (singer.isCountrySinger() && (singer.getLastName() == null || singer.getGender() == null))) {
            return false;
        }
        return true;
    }
}

10.7 使用AssertTrue进行自定义验证

除了实现自定义验证器外,在 Bean Validation API 中应用自定义验证的另一种方法是使用 @AssertTrue@AssertFalse 注解。

@Data
public class VSinger {

    @NotNull
    @Size(min = 2, max = 60)
    private String firstName;

    private String lastName;

    @NotNull
    private Genre genre;

    private Gender gender;

    @AssertTrue(message = "AssertTrue xxx")
    public boolean isCountrySinger(){
        if (genre != null && (genre == Genre.COUNTRY && (getLastName() == null || getGender() == null))) {
            return false;
        }
        return false;
    }
}

10.8 自定义验证的注意事项

对于JSR-349中的自定义验证,应该使用哪种方法:自定义验证器还是@AssertTure注解?
通常,@AssertTure方法实现起来更简单,可以在域对象的代码中看到验证规则。但是,对于具有更复杂逻辑的验证器(例如,需要注入一个服务类,访问数据库并检查有效值),实现自定义验证器是不错的方法,因为你可能并不像将服务层对象添加到域对象中。而且,自定义验证器可以在相似的域对象中重用。

10.9 决定使用哪种验证API

Spring的Validator接口以及JSR-349(Bean Validation API),更应该使用JSR-349(Bean Validation API)。
主要原因:

  • JSR-349是JEE标准,得到很多前后端框架的广泛支持。
  • JSR-349提供了标准验证API隐藏了底层提供程序,不受限于特定的提供程序。
  • Spring从版本4开始与JSR-349紧密集成。例如,在Spring MVC Web 控制器中,可以使用@Valid注解(javax.validation.Valid)来注解入参,Spring 将在数据绑定过程中自动调用JSR-349验证。
  • 如果使用的是JPA2,那么提供程序会在吃就会之前自动对实体执行JSR-349验证。
原文地址:https://www.cnblogs.com/huangwenjie/p/11798213.html