细说Java Validation Api
概述
日常开发中经常需要对接口的入参进行参数校验,使用Java Validation API来进行校验参数,我们只需要在bean的字段上加上所需要的注解即可完成校验。
这里Java Validation API指的是
规范中的Bean Validation 2.0。该规范中定义了许多约束性注解,如@NotBlank,@Size,@Max,@Email等,以方便对bean的字段进行对应的校验
依赖
注意,JSR 380只是定义了规范,体现在代码中就是一些注解,其并没有对应的实现。如果不使用Spring等相关框架的话,我们需要选择适合自己的第三方实现。
javax.validation maven坐标
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
这是javax.validation的官方maven坐标。如果我们使用Spring,我们并不需要额外导入上面的依赖,Spring已经内置好了。
使用Springboot validation starter
在Springboot项目中,使用spring-boot-starter-validation的依赖即可完成依赖的引入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>java-validation</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- 添加下面这个依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>
Spring boot项目直接使用parent标签继承Spring官方的pom,并加上spring-boot-starter-validation依赖即可。
Idea中可以按ctrl查看spring-boot-starter-validation的官方pom配置,可发现Spring官方其实使用的是hibernate-validator实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.5.RELEASE</version>
<name>spring-boot-starter-validation</name>
<description>Starter for using Java Bean Validation with Hibernate Validator</description>
<url>https://spring.io/projects/spring-boot</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>https://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection>
<developerConnection>scm:git:ssh://[email protected]/spring-projects/spring-boot.git</developerConnection>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/spring-projects/spring-boot/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>3.0.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<!-- hibernate-validator实现-->
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.6.Final</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
使用校验注解
JSR 380标准定义了许多注解,从字面上就可以推断出意思:
- @NotNull 校验字段是否不为null
- @AssertTrue 校验字段值是否为true
- @Size 校验字段值是否在设置的min和max之间。可以作用于String,Collection,Map,array数组类型。
- @Min 校验字段值是否不小于设置的value
- @Max 校验字段值是否不大于设置的value
- @Email 校验字段值是否是有效邮箱地址
- @NotEmpty 校验字段值不是否不为null或空。可以作用于String,Collection,Map或Array类型。
- @NotBlank 只能作用于字符串类型,校验字段是否不为空串。和StringuUtils.isNotBlank类似。
- @Positive and @PositiveOrZero 作用于数字。校验字段值是否是整数或0。
- @Negative and @NegativeOrZero 作用于数字。校验字段值是否是负数或0。
- @Past and @PastOrPresent 校验日期是否已过或包括当前日期。
- @Future and @FutureOrPresent 校验日期是否没到或包括当前日期。
校验的注解可以作用于集合中的元素:
1
List<@NotBlank String> preferences;
这种情况下所有被加进preferences的元素都会进行校验
@Past和@Future 可以作用于Java8新增的LocalDate类型
1
2
3
4
5
private LocalDate dateOfBirth;
public Optional<@Past LocalDate> getDateOfBirth() {
return Optional.of(dateOfBirth);
}
这里校验框架会自动拿出Optional里面的值进行校验。一般日常开发中以下两种使用方式会比较频繁
- 在Controller参数上添加校验注解
- 在Controller参数的bean类上添加校验注解,比如VO,DTO类
编程式校验
Springboot环境下直接在方法的参数或bean的字段上添加对应的校验注解即可自动完成校验。下面来介绍一下如何手动进行校验。
1
2
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
使用ValidatorFactory工厂来生产一个Validator。
定义bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {
@Positive
@Min(value = 1, message = "年龄不能小于{value}")
private int age;
@NotBlank()
private String name;
@Email(message = "Email ${validatedValue} 不合法")
private String email;
// standard setters and getters
}
创建一个User
1
2
3
4
User user = new User();
user.setAge(0);
user.setName("");
user.setEmail("demoemail.com");
message属性占位符
在定义message错误消息时,可以使用{注解属性}来获得注解的属性值。通过${validatedValue}来获得被注解字段的值。
校验bean
1
2
Set<ConstraintViolation<User>> validate = validator.validate(user);
validate.forEach(userConstraintViolation -> System.out.println(userConstraintViolation.getMessage()));
validate方法返回一个set,其中包含了所有的校验错误信息。遍历打印结果:
1
2
3
4
Email demoemail.com 不合法
不能为空
年龄不能小于1
必须是正数
自定义校验注解
JSR380标准中的注解并不能完全满足自己的需求。可以根据需求写自己的校验注解,并需要实现一个校验器搭配食用。下面以校验ip地址为例介绍。
注解
定义一个名为IpAddress的注解,代码如下
1
2
3
4
5
6
7
8
9
10
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IpAddressValidator.class)
public @interface IpAddress {
String message() default "ip地址无效";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Constraint需要传入使用的校验器,下文会继续介绍。message()的default值有两种写法
- 像上面那样写死,缺点是不支持国际化
- 定义一个key,这样会从properties文件中读取该key的value来替换。比较优雅,支持国际化。下文会详细介绍
采用第二种比较优雅,扩展性强。默认的message,在使用该注解时也可以再传入mesage进行覆盖。
校验器
自定义的校验器实现ConstraintValidator<A extends Annotation, T>接口即可。两个泛型A是自己写的注解名,T是该注解支持的校验字段类型。IpAddressValidator的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class IpAddressValidator implements ConstraintValidator<IpAddress, String> {
private static final Pattern PATTERN = Pattern.compile("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$");
@Override
public void initialize(IpAddress constraintAnnotation) {
}
@Override
public boolean isValid(String ip, ConstraintValidatorContext constraintValidatorContext) {
return PATTERN.matcher(ip).matches();
}
}
isValid方法可以定义自己的校验逻辑。这里用正则表达式来校验ip地址的格式。
现在在user上添加ip属性,并添加上ipaddress注解
1
2
@IpAddress()
private String ip;
程序运行结果
ip地址无效 必须是正数 年龄不能小于1 不是一个合法的电子邮件地址 不能为空
message扩展与国际化
文件位置
message中定义的key,都在ValidationMessages.properties文件中。使用hibernate validator的实现的话,文件位置为
1
D:\maven\repository\org\hibernate\validator\hibernate-validator\6.1.6.Final\hibernate-validator-6.1.6.Final.jar!\org\hibernate\validator\ValidationMessages.properties
可以看到,不同语言的文件用下划线和国家isocode区分分别存储
扩展message properties文件
可以自行编写messages.properties文件来扩展内置的消息。
1
ipaddress.invalid=ip address invalid
国际化i18n
不同语言的message文件用国家的isocode前面加上下划线来区分。例如messages_zh.properties的内容如下
1
ipaddress.invalid=ip地址无效
文件结构如下
SpringBoot环境集成
properties文件写好了,Springboot环境下需要配置一下方可读取。
bean配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ValidationConfiguration {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource
= new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean
public LocalValidatorFactoryBean getValidator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
}
主要配置两个bean,配置mesageSource以可以读取咱们自己的messages.properties文件。配置LocalValidatorFactoryBean以使用咱们自己的messageSource
创建容器并获取validator
这里为了方便,使用手动创建ApplicationContext的方式来创建Spring容器。
1
2
3
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfiguration.class);
LocalValidatorFactoryBean bean = context.getAutowireCapableBeanFactory().getBean(LocalValidatorFactoryBean.class);
Validator validator = bean.getValidator();
接着就可以和之前一样愉快得校验User对象了。
1
2
Set<ConstraintViolation<User>> validate = validator.validate(user);
validate.forEach(userConstraintViolation -> System.out.println(userConstraintViolation.getMessage()));
运行代码,可以发现messages_zh.properties文件已被正常读取。
不能为空 不是一个合法的电子邮件地址 必须是正数 年龄不能小于1 ip地址无效
Comments powered by Disqus.