第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验证。

20191105 《Spring5高级编程》笔记-第10章的更多相关文章

  1. C#高级编程笔记之第二章:核心C#

    变量的初始化和作用域 C#的预定义数据类型 流控制 枚举 名称空间 预处理命令 C#编程的推荐规则和约定 变量的初始化和作用域 初始化 C#有两个方法可以一确保变量在使用前进行了初始化: 变量是字段, ...

  2. C#高级编程笔记之第一章:.NET体系结构

    1.1 C#与.NET的关系 C#不能孤立地使用,必须与.NET Framework一起使用一起考虑. (1)C#的体系结构和方法论反映了.NET基础方法论. (2)多数情况下,C#的特定语言功能取决 ...

  3. 20191105 《Spring5高级编程》笔记-【目录】

    背景 开始时间:2019/09/18 21:30 Spring5高级编程 版次:2019-01-01(第5版) Spring5最新版本:5.1.9 CURRENT GA 官方文档 Spring Fra ...

  4. 读《C#高级编程》第1章问题

    读<C#高级编程>第1章 .Net机构体系笔记 网红的话:爸爸说我将来会是一个牛逼的程序员,因为我有一个梦,虽然脑壳笨但是做事情很能坚持. 本章主要是了解.Net的结构,都是一些概念,并没 ...

  5. Android高级编程笔记(四)深入探讨Activity(转)

    在应用程序中至少包含一个用来处理应用程序的主UI功能的主界面屏幕.这个主界面一般由多个Fragment组成,并由一组次要Activity支持.要在屏幕之间切换,就必须要启动一个新的Activity.一 ...

  6. C#高级编程9 第17章 使用VS2013-C#特性

    C#高级编程9 第17章 使用VS2013 编辑定位到 如果默认勾选了这项,请去掉勾选,因为勾选之后解决方案的目录会根据当前文件选中. 可以设置项目并行生成数 版本控制软件设置 所有文本编辑器行号显示 ...

  7. C#高级编程9 第18章 部署

    C#高级编程9 第18章 部署 使用 XCopy 进行部署 本主题演示如何通过将应用程序文件从一台计算机复制到另一台计算机来部署应用程序. 1.将项目中生成的程序集复制到目标计算机,生成的程序集位于项 ...

  8. C#高级编程9 第16章 错误和异常

    C#高级编程9 第16章 错误和异常 了解这章可以学会如何处理系统异常以及错误信息. System.Exception类是.NET运行库抛出的异常,可以继承它定义自己的异常类. try块代码包含的代码 ...

  9. C#高级编程笔记 (6至10章节)运算符/委托/字符/正则/集合

    数学的复习,4^-2即是1/4/4的意思, 4^2是1*2*2的意思,而10^-2为0.01! 7.2运算符 符号 说明 例   ++ 操作数加1 int i=3; j=i++; 运算后i的值为4,j ...

随机推荐

  1. Html5+ 开发APP 后台运行代码

    function backRunning(){ if(plus.os.name == 'Android'){ var main = plus.android.runtimeMainActivity() ...

  2. java实现一个简单的计数器

    package com.fengunion.sf; import org.junit.platform.commons.util.StringUtils; import java.util.HashM ...

  3. bzoj4764 弹飞大爷 LCT

    题目传送门 https://lydsy.com/JudgeOnline/problem.php?id=4764 题解 如果 \(a_i > 0\) 的话,那么就是 bzoj2002 的原题.直接 ...

  4. Spring Boot日志处理

    2.4 日志处理 2.4.1 记录日志内容 请求url 访问者ip 调用方法classMethod 参数args 返回内容 2.4.2 新建包aspect,新建日志切面处理类 package com. ...

  5. 用selenium启动chrome浏览器

    python 3.7 pycharm 1.安装selenium pip3 install selenium 2.下载与chrome匹配的chromdriver.exe,放到项目的解释器路径下,跟pyt ...

  6. 【leetcode】1110. Delete Nodes And Return Forest

    题目如下: Given the root of a binary tree, each node in the tree has a distinct value. After deleting al ...

  7. Shell-03

    Shell-03 编程原理 编程介绍 最开始的编程 机械码(16进制)—CPU会识别 计算机只能识别二进制指令 程序 = 指令 + 数据 驱动: 硬件默认是不能使用的 驱动程序----不同的厂家硬件设 ...

  8. php array_flip()函数 语法

    php array_flip()函数 语法 作用:用于反转/交换数组中所有的键名以及它们关联的键值.富瑞联华 语法:array_flip(array); 参数: 参数 描述 array 必需.规定需进 ...

  9. HDU-6703 array

    Description You are given an array a1,a2,...,an(∀i∈[1,n],1≤ai≤n). Initially, each element of the arr ...

  10. Leetcode 1. Two Sum(hash+stl)

    Given an array of integers, return indices of the two numbers such that they add up to a specific ta ...