第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验证。
05-12 05:15