第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
类(该类支持类型转换系统),以及根据每个字段类型所定义的格式化规则完成字段格式化。
FormattingConversionService
是 ConversionService
的实现类。
//------定义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.Validator
和 org.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验证。