0%

细说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坐标。日常开发中我们并不需要导入上面的依赖。

使用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://git@github.com/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地址无效