Spring Validation使用指南
Bean Validation
jsr303 -> jsr349 -> jsr380
在日常的项目开发中,为了防止非法参数对业务造成的影响,需要对接口的参数做合法性校验。
Bean Validation中提供的constraints:
注解 | 说明 |
---|---|
@AssertFalse | 被注释的元素必须为false,支持boolean或Boolean,null元素被认为是有效的。 |
@AssertTrue | 被注释的元素必须为true,支持boolean或Boolean,null元素被认为是有效的。 |
@DecimalMax | 被注释的元素必须是一个数字,值必须小于或等于指定最大值的。支持BigDecimal、BigInteger、CharSequence、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。 |
@DecimalMin | 被注释的元素必须是一个数字,其值必须大于或等于指定的最小值。支持BigDecimal、BigInteger、CharSequence、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。 |
@Digits | 被注释的元素必须是可接受范围内的数字。支持BigDecimal、BigInteger、CharSequence、(byte、 short、 int、long及他们包装类),null元素被认为是有效的。 |
字符串必须是格式正确的电子邮件地址,接受CharSequence,null元素被认为是有效的。 | |
@Future | 被注释的元素必须是将来的instant,date或time。null元素被认为是有效的。支持的类型: java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate |
@FutureOrPresent | 被注释的元素必须是当前或未来的instant,date或time。null元素被认为是有效的。支持的类型: java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate |
@Max | 被注释的元素必须是一个值必须小于或等于指定最大值的数字。支持BigDecimal、BigInteger、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。 |
@Min | 被注释的元素必须是一个数值,其值必须大于或等于指定的最小值。支持BigDecimal、BigInteger、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。 |
@Negative | 被注释的元素必须为负数(即0被视为无效值)。支持BigDecimal、BigInteger、(byte、short、int、long、float、double及他们的包装类),null元素被认为是有效的。 |
@NegativeOrZero | 被注释的元素必须是负数或0。支持BigDecimal、BigInteger、(byte、short、int、long、float、double及他们的包装类),null元素被认为是有效的。 |
@NotBlank | 被注释的元素不能为null,并且必须至少包含一个非空白字符。接收CharSequence类型。 |
@NotEmpty | 被注释的元素不能为null或empty。支持的类型: CharSequence (统计字符序列的长度) Collection (统计集合大小) Map (统计Map大小) Array (统计数组长度) |
@NotNull | 被注释的元素不能为null。接受任何类型。 |
@Null | 被注释的元素必须为空。接受任何类型。 |
@Past | 被注释的元素必须是过去的instant、date或time。null元素被认为是有效的。支持类型: java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate |
@PastOrPresent | 带注释的元素必须是过去或现在的instant、date或time。null元素被认为是有效的。支持类型: java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate |
@Pattern | 被注释的CharSequence必须与指定的正则表达式匹配。正则表达式遵循Java正则表达式约定。接收CharSequence。null元素被认为是有效的。 |
@Positive | 被注释的元素必须是正数(即0被视为无效值)。支持BigDecimal、BigInteger、(byte, short, int, long, float, double及他们的包装类)。null元素被认为是有效的。 |
@PositiveOrZero | 被注释的元素必须是正数或0。支持BigDecimal、BigInteger、(byte, short, int, long, float, double及他们的包装类)。null元素被认为是有效的。 |
@Size | 被注释的元素大小必须在指定边界(包括)之间。null元素被认为是有效的。支持的类型: CharSequence (统计字符序列的长度) Collection (统计集合大小) Map (统计Map大小) Array (统计数组长度) |
从Spring Boot 2.3.X开始,spring-boot-starter-web不再引入spring-boot-starter-validation,所以需要额外手动引入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@Validated与@Valid区别:
@Validated是只用Spring Validator校验机制使用,是@Valid 的一次封装。
两者最大的区别在于@Validated支持分组,@Valid支持用于成员属性上。
其次:
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性上,不支持嵌套检测;
@Valid:可以用在方法、构造方法、方法参数和成员属性上,支持嵌套检测。
这里提供一个Spring Validation具体示例:
创建一个SpringBoot项目,版本2.6.12,pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<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 https://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.6.12</version>
<relativePath/>
</parent>
<groupId>com.lwy.it</groupId>
<artifactId>spring-validation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-validation</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
实体主要包括书籍、作者、出版社,关系及属性如下:
全局返回结果统一如下(使用ResultDTO来规范返回值):
import com.fasterxml.jackson.annotation.JsonInclude;
import com.lwy.it.enums.ErrorCodeEnum;
import lombok.Data;
import java.io.Serializable;
/**
* 使用ResultDTO来规范返回值
*
* @author 帅喵
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResultDTO implements Serializable {
/**
* 后端是否处理成功
*/
private boolean success;
/**
* 给前端的返回值
*/
private Object data;
/**
* 错误码
*/
private String code;
/**
* 错误消息
*/
private String errorMessage;
/**
* 成功返回结果,无数据
*
* @return
*/
public static final ResultDTO success() {
ResultDTO ResultDTO = new ResultDTO();
ResultDTO.setSuccess(true);
return ResultDTO;
}
/**
* 成功返回结果,包含数据
*
* @param data 需要返回的数据
* @return
*/
public static final ResultDTO success(Object data) {
ResultDTO ResultDTO = new ResultDTO();
ResultDTO.setSuccess(true);
ResultDTO.setData(data);
return ResultDTO;
}
/**
* 失败返回结果,无数据
*
* @param errorCodeEnum 错误枚举定义
* @return
*/
public static final ResultDTO failure(ErrorCodeEnum errorCodeEnum) {
ResultDTO ResultDTO = new ResultDTO();
ResultDTO.setSuccess(false);
ResultDTO.setCode(errorCodeEnum.getCode());
ResultDTO.setErrorMessage(errorCodeEnum.getMessage());
return ResultDTO;
}
/**
* 失败返回结果,包含数据
*
* @param errorCodeEnum 错误枚举定义
* @param data 需要返回的数据
* @return
*/
public static final ResultDTO failure(ErrorCodeEnum errorCodeEnum, Object data) {
ResultDTO ResultDTO = new ResultDTO();
ResultDTO.setSuccess(false);
ResultDTO.setCode(errorCodeEnum.getCode());
ResultDTO.setErrorMessage(errorCodeEnum.getMessage());
ResultDTO.setData(data);
return ResultDTO;
}
}
ErrorCodeEnum为定义异常码和信息的枚举类:
package com.lwy.it.enums;
public enum ErrorCodeEnum {
PARAMETER_ERROR("E1000", "参数输入错误");
private String code;
private String message;
ErrorCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
全局异常处理如下(主要是为了捕获校验出的异常,会把具体的校验异常在data中返回):
package com.lwy.it.exception.handler;
import com.lwy.it.dto.ResultDTO;
import com.lwy.it.enums.ErrorCodeEnum;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 使用 @ControllerAdvice 结合 @ExceptionHandler 实现全局异常处理
*
* @ControllerAdvice 用来开启全局的异常捕获
*/
@ControllerAdvice(basePackages = "com.lwy.it.controller")
@ResponseBody
public class GlobalExceptionHandler {
/**
* 说明捕获哪些异常,对哪些异常进行处理。
* 专门用来捕获和处理Controller层的MethodArgumentNotValidException异常
*
* @param exception MethodArgumentNotValidException异常
* @return 统一返回结果格式
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultDTO validationExceptionHandler(MethodArgumentNotValidException exception) {
Map<String, String> map =
exception.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,
FieldError::getDefaultMessage));
ResultDTO resultDto = ResultDTO.failure(ErrorCodeEnum.PARAMETER_ERROR, map);
return resultDto;
}
}
书籍校验BookDTO
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.List;
/**
* id:不能为空,值要在100000以上
* bookName:长度在1个到10个字符之间
* bookAuthor:至少有1个元素
* description:不能为空
* publishDate:不能为空,早于当前时间
* publishingHouseDTO:包含出版社名称,地址,邮件均不能空
*/
@Data
public class BookDTO implements Serializable {
@NotNull(message = "id不能为空")
private Integer id;
@NotBlank(message = "书名不能为空")
@Size(min = 1, max = 10, message = "书名长度1~10个字符")
private String bookName;
/**
* 注意 @Valid AuthorDTO 嵌套验证校验的是List里面的内容
*/
@NotEmpty(message = "必须包含一个作者信息")
private List<@Valid AuthorDTO> bookAuthor;
@NotBlank(message = "描述不能为空")
private String description;
/**
* @DateTimeFormat是spring包里面的注解,作用是接收从页面传到后台的日期值。注意:url拼接的参数才生效
* @JsonFormat是jackson包里面的注解,作用是从后台向前台传递日期值。可以设置时区timezone=“GMT+8”
*/
@NotNull(message = "出版日期不能为空")
@Past(message = "出版日期不能超过当前日期")
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
private LocalDate publishDate;
/**
* 需要使用@Valid,才能实现嵌套验证
*/
@NotNull(message = "出版社信息不能为空")
@Valid
private PublishingHouseDTO publishingHouseDTO;
}
对应作者信息AuthorDTO:
package com.lwy.it.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
@Data
public class AuthorDTO implements Serializable {
@NotBlank(message = "身份证号不能为空")
@Size(min = 18, max = 18, message = "身份证号必须18位数字")
private String id;
@NotBlank(message = "作者名不能为空")
private String name;
}
出版社信息PublishingHouseDTO:
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class PublishingHouseDTO implements Serializable {
@NotBlank(message = "出版社名称不能为空")
private String name;
@NotBlank(message = "出版社地址不能为空")
private String address;
@NotNull(message = "出版社邮箱地址不能为空")
@Email(message = "必填是邮箱格式")
private String email;
}
下面来写一个controller来进行验证:
@RestController
/**
* @Validated 对本类中的方法,开启验证功能
*/
@Validated
public class BookController {
@PostMapping("/addBookInfo")
public ResultDTO addBookInfo(@RequestBody @Valid BookDTO bookDTO){
System.out.println(bookDTO);
return ResultDTO.success();
}
}
注意:
- 方法的返回值也可以做验证
- 在Service层中做参数验证时,全局异常处理@ExceptionHandler捕获的异常类型(ConstraintViolationException)与Controller的异常类型(MethodArgumentNotValidException)不一致
- 如果使用了接口,那么验证要写在接口上
分组验证
实际项目中经常遇到这种情况,由于某些ID是自动生成的,所以创建时为空,修改时不为空。
示例如下:
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.io.Serializable;
/**
* 分组校验,对于id来说:
* 1. 添加过程不能有值
* 2. 修改过程id必须有值
*/
@Data
public class PublishingHouseDTO implements Serializable {
// 在添加的时候生效
@Null(groups = {CreatePublishingHouse.class}, message = "添加时id为空")
// 在修改的时候生效
@NotNull(groups = {UpdatePublishingHouse.class}, message = "修改时id不能为空")
private String id;
@NotBlank(message = "出版社名称不能为空")
private String name;
@NotBlank(message = "出版社地址不能为空")
private String address;
@NotNull(message = "出版社邮箱地址不能为空")
@Email(message = "必填是邮箱格式")
private String email;
public interface CreatePublishingHouse {
}
public interface UpdatePublishingHouse {
}
}
对应校验Controller如下:
import com.lwy.it.dto.PublishingHouseDTO;
import com.lwy.it.dto.ResultDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.groups.Default;
@RestController
/**
* @Validated 对本类中的方法,开启验证功能
*/
@Validated
public class PublishingHouseController {
/**
* @Valid 校验后面的参数
* 分组需要使用 @Validated 注解
* 如果指定了验证组,那么该参数就只属于 指定的验证组
* 如果没有指定了验证组,那么该参数就属于默认组 Default.class
* 如果不添加 Default.class,默认组参数不会校验
*/
@PostMapping("/create")
public ResultDTO create(@RequestBody @Validated({PublishingHouseDTO.CreatePublishingHouse.class, Default.class}) PublishingHouseDTO dto) {
System.out.println(dto);
return ResultDTO.success();
}
/**
* 同时校验UpdatePublishingHouse和Default组
*/
@PostMapping("/update")
public ResultDTO update(@RequestBody @Validated({PublishingHouseDTO.UpdatePublishingHouse.class, Default.class}) PublishingHouseDTO dto) {
System.out.println(dto);
return ResultDTO.success();
}
}
自定义注解
场景如下:在AuthorDTO中增加一个国籍字段nationality,业务要求:nationality值必须为中国、美国、韩国 才能通过。
/**
* 使用自定义注解 @Nationality 进行校验
* 业务逻辑:nationality值必须为中国、美国、韩国 才能通过
*/
@Data
public class AuthorDTO implements Serializable {
@NotBlank(message = "身份证号不能为空")
@Size(min = 18, max = 18, message = "身份证号必须18位数字")
private String id;
@NotBlank(message = "作者名不能为空")
private String name;
@Nationality(message = "国家填写不符合规范")
private String nationality;
}
自定义注解@Nationality定义:
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {NationalityConstraintValidator.class})
public @interface Nationality {
String message() default "参数国家限制:中国、美国、韩国";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
这里NationalityConstraintValidator.class指向了实现类:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* ConstraintValidator<要验证哪个注解,验证的类型>
*/
public class NationalityConstraintValidator implements ConstraintValidator<Nationality, String> {
private final Set<String> set = new HashSet<String>() {{
add("中国");
add("美国");
add("韩国");
}};
//会在校验实例化后被调用,一般用于做些初始化工作。
@Override
public void initialize(Nationality constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 按照默认逻辑,null值不进行校验
if (value == null) {
return true;
}
if (set.contains(value)) {
return true;
}
return false;
}
}
在Controller中添加访问方法:
@PostMapping("/addAuthor")
public ResultDTO author(@RequestBody @Valid @NotNull AuthorDTO authorDTO){
System.out.println(authorDTO);
return ResultDTO.success();
}
非Web环境下使用
环境搭建
引入依赖
<!-- 引入bean validation -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<!-- 引入hibernate-validator,包含jakarta.validation-api -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>
<!-- 引入el规范和Tomcat的实现,用于解析message里面的表达式 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>10.1.0</version>
</dependency>
使用示例
原理
Bean Validation与Hibernate Validation 实现原理,通过SPI机制(在hibernate-validator包下classpath路径下META-INF/services目录下jakarta.validation.spi.ValidationProvider文件,Service Provider为:org.hibernate.validator.HibernateValidator)
示例:以用户信息为例,创建UserInfoDTO类。
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import java.time.LocalDate;
@Data
public class UserInfoDTO {
private Long id;
@NotBlank(message = "姓名必填")
private String name;
@NotNull(message = "年龄必填")
@Min(value = 1, message = "年龄最小为1岁")
@Max(value = 200, message = "年龄最大为200岁")
/**
* 与使用 @Range(min = 1, max = 200, message = "年龄范围在1岁至200岁之间") 同样效果
*/
private Integer age;
@NotBlank(message = "邮箱必填")
@Email(message = "必须为邮箱格式")
private String email;
@NotBlank(message = "手机号码必填")
@Length(min = 11, max = 11, message = "手机号码必须为11位数字")
@Pattern(regexp = "^1(3|4|5|7|8|9)\\d{9}$", message = "手机号码必须符合规范")
private String phone;
@NotNull(message = "出生日期必填")
@Past(message = "出生日期不能超过当前时间")
private LocalDate birthday;
@NotBlank(message = "个人主页地址必填")
@URL(message = "个人主页必须合法")
private String personalPage;
}
提供一个校验工具类:
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class ValidationUtil {
// 线程安全的校验对象
private static Validator validator;
static {
// 通过静态初始化块初始化validator
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
// 校验方法
public static List<ValidErrorResult> valid(Object object) {
/**
* 通过validate方法校验,返回校验不满足的结果
* 如果被校验对象没有校验通过,则set里面就有校验信息
*/
Set<ConstraintViolation<Object>> set = validator.validate(object);
List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
ValidErrorResult result = new ValidErrorResult();
result.setField(mapper.getPropertyPath().toString());
result.setValue(mapper.getInvalidValue());
result.setMessage(mapper.getMessage());
return result;
}).collect(Collectors.toList());
return validErrorResults;
}
}
校验结果:
import lombok.Data;
import java.io.Serializable;
@Data
public class ValidErrorResult implements Serializable {
// 验证的字段
private String field;
// 该字段的值
private Object value;
// 错误信息
private String message;
}
Main方法示例:
import com.lwy.it.dto.UserInfoDTO;
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;
import java.time.LocalDate;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 通过构造UserInfoDTO的各种参数来进行验证
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setAge(18);
userInfoDTO.setBirthday(LocalDate.now().minusYears(18));
userInfoDTO.setEmail("123456789@qq.com");
userInfoDTO.setId(1L);
userInfoDTO.setPhone("18888888888");
userInfoDTO.setName("法外狂徒张三");
userInfoDTO.setPersonalPage("https://baidu.com");
List<ValidErrorResult> list = ValidationUtil.valid(userInfoDTO);
System.out.println(list);
}
}
约束与校验类的绑定原理
XXValidator来校验XX约束注解,如:org.hibernate.validator.internal.constraintvalidators.hv.NotBlankValidator来校验@NotBlank注解。注解和注解的校验器绑定原理:org.hibernate.validator.internal.metadata.core.ConstraintHelper类中实现
if ( enabledBuiltinConstraints.contains( JAKARTA_VALIDATION_CONSTRAINTS_NOT_BLANK ) ) {
putBuiltinConstraint( tmpConstraints, NotBlank.class, NotBlankValidator.class );
}
注意:一个注解约束,可能对应多个约束Validator。
自定义消息与消息模板
自定义消息,通过el表达式
@NotNull(message = "年龄必填")
@Min(value = 18, message = "年龄最小为{value}岁")
@Max(value = 100, message = "年龄最大为{value}岁")
private Integer age;
el表达式还没解析成值的时候就为消息模板
分组校验
示例代码升级:
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import java.time.LocalDate;
@Data
public class UserInfoDTO {
/**
* 默认的组:jakarta.validation.groups.Default
*/
@Null(groups = {AddUser.class}, message = "新增用户id必须为空") // 只适用于新增场景
@NotNull(groups = {UpdateUser.class}, message = "修改用户id必须不为空") //只适用于修改场景
private Long id;
@NotBlank(message = "姓名必填")
private String name;
@NotNull(message = "年龄必填")
@Min(value = 1, message = "年龄最小为{value}岁")
@Max(value = 200, message = "年龄最大为{value}岁")
/**
* 与使用 @Range(min = 1, max = 200, message = "年龄范围在1岁至200岁之间")同样效果
*/
private Integer age;
@NotBlank(message = "邮箱必填")
@Email(message = "必须为邮箱格式")
private String email;
@NotBlank(message = "手机号码必填")
@Length(min = 11, max = 11, message = "手机号码必须为11位数字")
@Pattern(regexp = "^1(3|4|5|7|8|9)\\d{9}$", message = "手机号码必须符合规范")
private String phone;
@NotNull(message = "出生日期必填")
@Past(message = "出生日期不能超过当前时间")
private LocalDate birthday;
@NotBlank(message = "个人主页地址必填")
@URL(message = "个人主页必须合法")
private String personalPage;
// 标记接口,新增组
public interface AddUser {
}
// 标记接口,修改组
public interface UpdateUser {
}
}
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class ValidationUtil {
// 线程安全的校验对象
private static Validator validator;
static {
// 通过静态初始化块初始化validator
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
// 校验方法,新增groups参数
public static List<ValidErrorResult> valid(Object object, Class<?>... groups) {
/**
* 通过validate方法校验,返回校验不满足的结果
* 如果被校验对象没有校验通过,则set里面就有校验信息
*/
Set<ConstraintViolation<Object>> set = validator.validate(object, groups);
List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
ValidErrorResult result = new ValidErrorResult();
result.setField(mapper.getPropertyPath().toString());
result.setValue(mapper.getInvalidValue());
result.setMessage(mapper.getMessage());
return result;
}).collect(Collectors.toList());
return validErrorResults;
}
}
import com.lwy.it.dto.UserInfoDTO;
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;
import jakarta.validation.groups.Default;
import java.time.LocalDate;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 通过构造UserInfoDTO的各种参数来进行验证
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setAge(18);
userInfoDTO.setBirthday(LocalDate.now().minusYears(18));
userInfoDTO.setEmail("123456789@qq.com");
userInfoDTO.setId(1L);
userInfoDTO.setPhone("18888888888");
userInfoDTO.setName("法外狂徒张三");
userInfoDTO.setPersonalPage("https://baidu.com");
// 注意添加默认组
List<ValidErrorResult> list = ValidationUtil.valid(userInfoDTO, UserInfoDTO.AddUser.class, Default.class);
System.out.println(list);
}
}
自定义注解
import com.lwy.it.validator.NationalityValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 定义一个国籍注解
*/
@Documented
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = {NationalityValidator.class}) //说明当前注解要被谁来完成校验工作
public @interface Nationality {
/**
* @return the error message template
*/
String message() default "国籍必须为:中国、韩国、美国";
/**
* @return the groups the constraint belongs to
*/
Class<?>[] groups() default {};
/**
* @return the payload associated to the constraint
*/
Class<? extends Payload>[] payload() default {};
}
import com.lwy.it.annotation.Nationality;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* 该怎么绑定要校验的约束注解呢?
*/
public class NationalityValidator implements ConstraintValidator<Nationality, String> {
@Override
public void initialize(Nationality constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 值为null时不进行校验
if (Objects.isNull(value)) {
return true;
}
Set<String> set = new HashSet<String>(3) {{
add("中国");
add("韩国");
add("美国");
}};
if (set.contains(value)) {
return true;
}
return false;
}
}
增加国籍字段校验逻辑:
@NotBlank
// 国籍字段,假设必须为中国、韩国、美国之一
@Nationality(message = "国籍不正确")
private String nationality;
快速失败
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class ValidationUtil {
// 线程安全的校验对象
private static Validator validator;
// 快速失败的
private static Validator failFastValidator;
static {
// 通过静态初始化块初始化validator
validator = Validation.buildDefaultValidatorFactory().getValidator();
failFastValidator = Validation.byProvider(HibernateValidator.class)
.configure()
// 配置快速失败
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
/**
* 快速失败的
*/
public static List<ValidErrorResult> validFailFase(Object object, Class<?>... groups) {
// 如果被校验对象没有校验通过,则set里面就有校验信息
Set<ConstraintViolation<Object>> set = failFastValidator.validate(object, groups);
List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
ValidErrorResult result = new ValidErrorResult();
result.setField(mapper.getPropertyPath().toString());
result.setValue(mapper.getInvalidValue());
result.setMessage(mapper.getMessage());
return result;
}).collect(Collectors.toList());
return validErrorResults;
}
// 校验方法
public static List<ValidErrorResult> valid(Object object, Class<?>... groups) {
/**
* 通过validate方法校验,返回校验不满足的结果
* 如果被校验对象没有校验通过,则set里面就有校验信息
*/
Set<ConstraintViolation<Object>> set = validator.validate(object, groups);
List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
ValidErrorResult result = new ValidErrorResult();
result.setField(mapper.getPropertyPath().toString());
result.setValue(mapper.getInvalidValue());
result.setMessage(mapper.getMessage());
return result;
}).collect(Collectors.toList());
return validErrorResults;
}
}
调用方法:
import com.lwy.it.dto.UserInfoDTO;
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;
import jakarta.validation.groups.Default;
import java.time.LocalDate;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 通过构造UserInfoDTO的各种参数来进行验证
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setId(1L);
userInfoDTO.setAge(18);
userInfoDTO.setBirthday(LocalDate.now().minusYears(18));
userInfoDTO.setEmail("123456789@qq.com");
userInfoDTO.setPhone("18888888888");
userInfoDTO.setName("法外狂徒张三");
userInfoDTO.setPersonalPage("https://baidu.com");
userInfoDTO.setNationality("日本");
// 注意添加默认组
List<ValidErrorResult> list = ValidationUtil.validFailFase(userInfoDTO, UserInfoDTO.AddUser.class,
Default.class);
System.out.println(list);
}
}
非Bean校验
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.executable.ExecutableValidator;
import org.hibernate.validator.HibernateValidator;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class ValidationUtil {
// 线程安全的校验对象
private static Validator validator;
// 快速失败的
private static Validator failFastValidator;
// 非Bean的
private static ExecutableValidator executableValidator;
static {
// 通过静态初始化块初始化validator
validator = Validation.buildDefaultValidatorFactory().getValidator();
failFastValidator = Validation.byProvider(HibernateValidator.class)
.configure()
// 配置快速失败
.failFast(true)
.buildValidatorFactory()
.getValidator();
// 校验入参或返回值的
executableValidator = validator.forExecutables();
}
/**
* executableValidator
* validateParameters():验证放置在给定方法的参数上的所有约束。
* validateReturnValue():验证给定方法的所有返回值约束。
* validateConstructorParameters():验证放置在给定构造函数参数上的所有约束。
* validateConstructorReturnValue():验证给定构造函数的所有返回值约束。
*/
public static <T> List<ValidErrorResult> validNotBean(T object,
Method method,
Object[] parameterValues,
Class<?>... groups) {
Set<ConstraintViolation<T>> set = executableValidator.validateParameters(object, method,
parameterValues, groups);
List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
ValidErrorResult result = new ValidErrorResult();
result.setField(mapper.getPropertyPath().toString());
result.setValue(mapper.getInvalidValue());
result.setMessage(mapper.getMessage());
return result;
}).collect(Collectors.toList());
return validErrorResults;
}
/**
* 快速失败的
*
* @param object
* @param groups
* @return
*/
public static List<ValidErrorResult> validFailFase(Object object, Class<?>... groups) {
// 如果被校验对象没有校验通过,则set里面就有校验信息
Set<ConstraintViolation<Object>> set = failFastValidator.validate(object, groups);
List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
ValidErrorResult result = new ValidErrorResult();
result.setField(mapper.getPropertyPath().toString());
result.setValue(mapper.getInvalidValue());
result.setMessage(mapper.getMessage());
return result;
}).collect(Collectors.toList());
return validErrorResults;
}
// 校验方法
public static List<ValidErrorResult> valid(Object object, Class<?>... groups) {
/**
* 通过validate方法校验,返回校验不满足的结果
* 如果被校验对象没有校验通过,则set里面就有校验信息
*/
Set<ConstraintViolation<Object>> set = validator.validate(object, groups);
List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
ValidErrorResult result = new ValidErrorResult();
result.setField(mapper.getPropertyPath().toString());
result.setValue(mapper.getInvalidValue());
result.setMessage(mapper.getMessage());
return result;
}).collect(Collectors.toList());
return validErrorResults;
}
}
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;
import jakarta.validation.constraints.NotBlank;
import java.lang.reflect.Method;
import java.util.List;
public class UserInfoService {
/**
* 方法非bean类型的入参校验
* 1.方法参数前加注解
* 2.执行入参校验
*/
public String getUserByName(@NotBlank(message = "姓名参数不能为空") String name) {
// 也可以通过反射方式获取
StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[1];
String methodName = stackTraceElement.getMethodName();
Method method = null;
try {
method = this.getClass().getDeclaredMethod(methodName, String.class);
} catch (Exception exception) {
exception.printStackTrace();
}
List<ValidErrorResult> validErrorResults = ValidationUtil.validNotBean(this, method, new Object[]{name});
System.out.println(validErrorResults);
return "SUCCESS";
}
}
public static void main(String[] args) {
UserInfoService userInfoService = new UserInfoService();
userInfoService.getUserByName("");
}
使用时可以看到非常的繁琐,可以结合AOP的方式进行使用。