介绍
在开发现代应用程序时,数据验证是确保用户输入的正确性和应用程序数据完整性的关键方面。Spring Boot 提供了强大的数据验证机制,使开发者能够轻松地执行验证操作。本文将深入介绍 Spring Boot 中的 Validation,以及如何在应用程序中正确使用它。
为什么使用数据验证?
手动数据校验的痛点
日常开发中,有些写项目可能没有采用 Spring Validator,采用的是在代码中手动校验数据。但是手动校验数据会带来代码冗余、错误处理的一致性以及业务规则的维护的一些痛点。
public ResponseEntity<String> registerUser(UserRegistrationRequest request) {
if (request == null) {
return ResponseEntity.badRequest().body("Request cannot be null");
}
if (StringUtils.isBlank(request.getUsername())) {
return ResponseEntity.badRequest().body("Username cannot be blank");
}
if (StringUtils.length(request.getPassword()) < 6) {
return ResponseEntity.badRequest().body("Password must be at least 6 characters long");
}
// 处理用户注册逻辑
return ResponseEntity.ok("User registered successfully");
}
复制代码
通过引入 Spring Validator,我们能够有效解决这些痛点,提高代码的可读性、可维护性,并确保校验逻辑的一致性。
Spring Boot 中的 Validation 概述
因 Springboot 的spring-boot-starter-web
默认内置了Hibernate-Validator
(Spring boot 2.3 以前版本),虽然Hibernate-Validator
也能做到数据校验,但是考虑到spring-boot-starter-validation
是一个抽象层,使得验证框架的具体实现变得可插拔。这意味着,除了 Hibernate Validator
,开发者可以选择其他符合 Bean Validation 规范的实现。所以我们可以手动引入spring-boot-starter-validation
实现数据验证。
<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>
复制代码
spring-boot-starter-validation
不仅支持 JSR-303(Bean Validation 1.0)规范,还提供了对 JSR-380(Bean Validation 2.0)规范的全面支持。这使得开发者可以利用 Bean Validation 2.0 的新特性,更灵活地定义验证规则,包括对集合、嵌套对象的验证等。
通过在实体类的字段上使用标准的 Bean Validation 注解(如 @NotBlank
、@Size
、@Email
等),我们能够直观地定义数据的验证规则。这些验证规则会在应用程序的不同层次(如控制器层)生效,确保输入数据的正确性。
基本用法
Spring Boot Validation 提供了一系列注解,用于在实体类中定义验证规则。以下是一些常用的校验相关的注解及其功能以及用法:
1.@NotNull
: 校验元素值不能为 null。如果元素为 null,则验证失败。通常用于字段级别的验证。
@NotNull(message = "Name cannot be null")
private String name;
复制代码
2.@NotBlank
: 校验字符串元素值不能为 null 或空字符串。必须包含至少一个非空格字符(即执行 trim()之后不为'')。如果元素为 null 或者‘‘,则验证失败。通常用于String
类型的字段校验。
@NotBlank(message = "Username cannot be blank")
private String username;
复制代码
3.NotEmpty
: 校验集合元素或数组元素或者字符串是否非空。通常作用于集合字段或数组字段,此时需要集合或者数字的元素个数大于 0。也可以作用于字符串,此时校验字符串不能为 null 或空串(可以是一个空格)。注意与@NotBlank
的使用区别。
@NotEmpty(message = "List cannot be empty")
private List<String> items;
复制代码
4.@Length
: 校验字符串元素的长度。作用于字符串。注:Hibernate-Validator
中注解,等同于spring-boot-starter-validation
中的@Size
。
@Length(min = 5, max = 20, message = "Length must be between 5 and 20 characters")
private String username;
复制代码
5.@Size
: 校验集合元素个数或字符串的长度在指定范围内。在集合或字符串字段上添加 @Size
注解。
@Size(min = 1, max = 10, message = "Number of items must be between 1 and 10")
private List<String> items;
@Size(min = 5, max = 20, message = "Length must be between 5 and 20 characters")
private String username;
复制代码
6.@Min
: 校验数字元素的最小值。
@Min(value = 18, message = "Age must be at least 18")
private int age;
复制代码
7.@Max
: 校验数字元素的最大值。
@Max(value = 100, message = "Age must not exceed 100")
private int age;
复制代码
9.@DecimalMax
: 作用于BigDecimal
类型字段, 校验字段的最大值,支持比较的值为字符串表示的十进制数。通常搭配它的inclusive()
使用,区别边界问题。value
属性表示最大值,inclusive 属性表示是否包含最大值。
@DecimalMax(value = "100.00", inclusive = true, message = "Value must be less than or equal to 100.00")
private BigDecimal amount;
复制代码
10.@DecimalMin
: 作用于BigDecimal
类型字段, 校验字段的最小值,支持比较的值为字符串表示的十进制数。通常搭配它的inclusive()
使用,区别边界问题。value
属性表示最小值,inclusive 属性表示是否包含最小值。
@DecimalMin(value = "0.00", inclusive = false, message = "Value must be greater than 0.00")
private BigDecimal amount;
复制代码
11.@Email
: 校验字符串元素是否为有效的电子邮件地址。可以通过regexp
自定义邮箱匹配正则。
@Email(message = "Invalid email address")
private String email;
复制代码
12.@Pattern
: 根据正则表达式校验字符串元素的格式。
@Pattern(regexp = "[a-zA-Z0-9]+", message = "Only alphanumeric characters are allowed")
private String username;
复制代码
13.@Digits
: 校验数字元素的整数部分和小数部分的位数。作用于BigDecimal
,BigInteger
,字符串,以及byte
, short
,int
, long
以及它们的包装类型。
@Digits(integer = 5, fraction = 2, message = "Number must have up to 5 integer digits and 2 fraction digits")
private BigDecimal amount;
复制代码
14.@Past
: 校验日期或时间元素是否在当前时间之前。即是否是过去时间。作用于 Date 相关类型的字段。
@Past(message = "Date must be in the past")
private LocalDate startDate;
复制代码
15.@Future
: 校验日期或时间元素是否在当前时间之后。即是否是未来时间。作用于 Date 相关类型的字段。
@Future(message = "Date must be in the future")
private LocalDate endDate;
复制代码
注:以上只罗列部分注解以及它们的功能,其余他们的字段属性并没有详细说明,其他注解以及详细的说明需要去看源码。
用法示例
1.定义接口入参请求参数
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/1/8 16:46
*/
@Data
public class UserCreateRequestVO {
@NotBlank(message = "请输入用户名")
@Size(max = 128, message = "用户名长度最大为128个字符")
private String userName;
@Email(message = "请填写正确的邮箱地址")
private String email;
@Min(value = 18, message = "用户年龄必须大于18岁")
@Max(value = 60, message = "用户年龄必须小于60岁")
private Integer age;
@NotEmpty(message = "请输入你的兴趣爱好")
@Size(max = 5, message = "兴趣爱好最多可以输入5个")
private List<String> hobbies;
@DecimalMin(value = "50", inclusive = false, message = "体重必须大于50KG")
private BigDecimal weight;
@Validated
@NotNull(message = "请输入地址信息")
private UserAddressRequestVO address;
}
复制代码
2.定义请求接口
@RestController
@RequestMapping("user")
@Validated
@Slf4j
public class UserController {
/**
* 创建用户
* @param requestVO
* @return
*/
@PostMapping("create")
public ResultResponse<Void> createUser(@Validated @RequestBody UserCreateRequestVO requestVO){
return ResultResponse.success(null);
}
/**
* 校验用户邮箱是否合法
* @param email
* @return
*/
@GetMapping("email")
public ResultResponse<Void> validUserEmail(@Email(message = "邮箱格式不正确") String email){
return ResultResponse.success(null);
}
}
复制代码
3.测试
我们需要捕获一下MethodArgumentNotValidException
。该部分内容请参考文章:SpringBoot统一异常处理
注:单参数校验时我们需要,在方法的类上加上@Validated
注解,否则校验不生效。
嵌套对象的校验
在UserCreateRequestVO
中增加一个address
的校验,即需要对嵌套对象进行校验
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/1/8 19:45
*/
@Data
public class UserAddressRequestVO {
@Size(max = 16, message = "地址信息中国家长度不能超过16个字符")
@NotBlank(message = "地址信息国家不能为空")
private String country;
private String city;
@Size(max = 128, message = "详细地址长度不能超过128个字符")
private String address1;
}
复制代码
在UserAddressRequestVO
中增加address
属性
@Data
public class UserCreateRequestVO {
@NotNull(message = "请输入地址信息")
private UserAddressRequestVO address;
}
复制代码
解决办法,要在嵌套对象上使用 @Valid 注解
@Data
public class UserCreateRequestVO {
@NotNull(message = "请输入地址信息")
@Valid
private UserAddressRequestVO address;
}
复制代码
试了一些其他的方式,好像都不行,有知道其他方式的,欢迎评论区留言探讨
自定义验证注解
在项目开发中,我们也可以自定义注解去完成我们的字段校验,比如某些枚举值的传递,需要校验枚举值是否合法。在创建自定义注解之前,我们需要了解一下ConstraintValidator
以及实现自定义验证注解的原理
1.ConstraintValidator 接口
ConstraintValidator
是 Java Bean Validation (JSR 380) 规范中用于自定义验证逻辑的接口。它允许你定义针对特定自定义注解的验证规则。它是一个泛型接口,需要提供两个类型参数:
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A constraintAnnotation);
boolean isValid(T value, ConstraintValidatorContext context);
}
复制代码
其中:
以下为枚举校验注解的校验规则实现
/**
* @version 1.0
* <p> </p>
* @author: 码农Academy
* @create: 2024/01/09 3:11 下午
*/
public class EnumValidator implements ConstraintValidator<EnumValid, Object> {
private Class clazz;
private String validField;
@Override
public void initialize(EnumValid constraintAnnotation) {
clazz = constraintAnnotation.enumClass();
validField = constraintAnnotation.field();
}
@SneakyThrows
@Override
public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
if (object == null || "".equals(object)){
return true;
}
if (!clazz.isEnum()){
return false;
}
Class<Enum> enumClass = (Class<Enum>)clazz;
//获取所有枚举实例
Enum[] enumConstants = enumClass.getEnumConstants();
// 需要比对的字段
Field field = enumClass.getDeclaredField(validField);
field.setAccessible(true);
for(Enum constant : enumConstants){
// 取值final修饰
Object validValue = field.get(constant);
if (validValue == null){
Method method = enumClass.getMethod(validField);
validValue = method.invoke(constant);
}
if(validValue instanceof Number) {
validValue = ((Number)validValue).intValue();
object = ((Number) object).intValue();
}
if (Objects.equals(validValue,object)){
return true;
}
}
return false;
}
}
复制代码
2.创建自定义注解
在 Java Bean Validation 中,约束注解(Constraint Annotation)是通过元注解 @Constraint
来定义的。这个注解包含了以下关键元素:
以校验枚举值的合法行为例,我们创建一个EnumValid
约束注解
@Constraint(validatedBy = {EnumValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface EnumValid {
/**
* 不合法时 抛出异常信息
*/
String message() default "值不合法";
/**
* 校验的枚举类
* @return
*/
Class enumClass() default Enum.class;
/**
* 对应枚举类中需要比对的字段
* @return
*/
String field() default "code";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
复制代码
3.注册 ConstraintValidator
在大多数情况下,不需要手动注册 ConstraintValidator
。当你使用 @Constraint(validatedBy = EnumValidator.class)
注解时,Java Bean Validation 的实现框架会自动发现并注册相应的验证器。但在一些特殊情况下,你可能需要将验证器注册为 Spring 组件或手动配置。比如
@Constraint(validatedBy = {UniqueUserValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface UniqueUser {
/**
* 不合法时 抛出异常信息
*/
String message() default "值不合法";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
复制代码
然后定义一个业务的Validator
@Slf4j
@Component
public class UniqueUserValidator implements ConstraintValidator<UniqueUser, UserCreateRequestVO> {
private UserRepository userRepository;
@Override
public boolean isValid(UserCreateRequestVO value, ConstraintValidatorContext context) {
final String userName = value.getUserName();
final UserDO userDO = userRepository.selectUserByName(userName);
final String userId = value.getUserId();
if (StringUtils.isBlank(userId)){
return userDO == null;
}
return userDO == null || Objects.equals(userDO.getUserId(), userId);
}
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
复制代码
在创建用户的接口中使用@UniqueUser
。
@RestController
@RequestMapping("user")
@Validated
@Slf4j
public class UserController {
/**
* 创建用户
* @param requestVO
* @return
*/
@PostMapping("create")
public ResultResponse<Void> createUser(@Validated @UniqueUser(message = "用户名称已存在") @RequestBody UserCreateRequestVO requestVO){
return ResultResponse.success(null);
}
}
复制代码
模拟当用户名存在时,校验不通过
此时会抛出javax.validation.ConstraintViolationException
。异常统一处理请参考:SpringBoot统一异常处理
4.自定义校验注解使用
我们创建一个性别的枚举类:
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/1/9 16:07
*/
@AllArgsConstructor
public enum SexEnum {
MAN(1, "男"),
WOMAN(2,"女");
public final Integer code;
public final String desc;
}
复制代码
然后我们在入参中增加sex
字段,并使用@EmunValid
注解
@Data
public class UserCreateRequestVO {
@NotBlank(message = "请输入用户名")
@Size(max = 128, message = "用户名长度最大为128个字符")
private String userName;
@Email(message = "请填写正确的邮箱地址")
private String email;
@Min(value = 18, message = "用户年龄必须大于18岁")
@Max(value = 60, message = "用户年龄必须小于60岁")
private Integer age;
@NotNull(message = "请输入性别")
@EnumValid(enumClass = SexEnum.class, message = "输入性别不合法")
private Integer sex;
@NotEmpty(message = "请输入你的兴趣爱好")
@Size(max = 5, message = "兴趣爱好最多可以输入5个")
private List<String> hobbies;
@DecimalMin(value = "50", inclusive = false, message = "体重必须大于50KG")
private BigDecimal weight;
}
复制代码
测试结果如下:
分组验证
在一个应用中,同一个实体类可能会被用于不同的场景,比如用户创建、用户更新、用户删除等。每个场景对于字段的要求可能不同,有些字段在某个场景下需要验证,而在另一个场景下不需要。不同的业务操作可能对同一实体的验证有不同的需求。例如,在用户创建时可能强调用户名和密码的合法性,而在用户更新时可能更关心其他信息的完整性。
开发中我们针对这种情况,在不知道分组校验的知识时,通常采取的都是对应不同的场景或者业务创建不同的入参实体,比如创建用户UserCreateRequestVO
,更新用户UserUpdateRequestVO
,删除用户UserDeleteRuquestVO
,在不同的实体中根据业务场景设置不同的校验规则。这样做虽然也可以,但是会造成类的膨胀,业务的重复实现。
而实际上用分组校验可以让你根据场景以及业务的差异性,有选择地执行特定组的验证规则。
1.定义验证分组接口
我们定义两个分组接口CreateUserGroup
(用户创建组),UpdateUserGroup
(用户更新组),分别继承javax.validation.groups.Default
,标识不同的业务场景。
public interface CreateUserGroup extends Default {
}
public interface UpdateUserGroup extends Default {
}
复制代码
2.分组校验的使用
在 Bean Validation 中,分组校验是通过在验证注解上指定 groups
属性来实现的。这个属性允许你为验证规则分配一个或多个验证组。我们设定用户创建时不传递用户 ID,其余的参数必传,用户更新接口必须传递用户 ID,可以不传递用户名,其他参数必须传递。
@Data
public class UserCreateRequestVO {
@NotBlank(message = "请选择用户", groups = UpdateUserGroup.class)
private String userId;
@NotBlank(message = "请输入用户名", groups = CreateUserGroup.class)
@Size(max = 128, message = "用户名长度最大为128个字符")
private String userName;
@Email(message = "请填写正确的邮箱地址")
private String email;
@Min(value = 18, message = "用户年龄必须大于18岁")
@Max(value = 60, message = "用户年龄必须小于60岁")
private Integer age;
@NotNull(message = "请输入性别")
@EnumValid(enumClass = SexEnum.class, message = "输入性别不合法")
private Integer sex;
@NotEmpty(message = "请输入你的兴趣爱好")
@Size(max = 5, message = "兴趣爱好最多可以输入5个")
private List<String> hobbies;
@DecimalMin(value = "50", inclusive = false, message = "体重必须大于50KG")
private BigDecimal weight;
}
复制代码
指定了分组的校验规则,分别在对应的分组校验中生效,没有指定分组使用默认分组Default
,即对所有的校验都生效。
3.在接口中使用分组
使用 @Validated
注解,并指定要执行的验证组。
@RestController
@RequestMapping("user")
@Validated
@Slf4j
public class UserController {
/**
* 创建用户
* @param requestVO
* @return
*/
@PostMapping("create")
public ResultResponse<Void> createUser(@Validated(value = CreateUserGroup.class) @RequestBody UserCreateRequestVO requestVO){
return ResultResponse.success(null);
}
/**
* 更新用户
* @param requestVO
* @return
*/
@PostMapping("update")
public ResultResponse<Void> updateUser(@Validated(value = UpdateUserGroup.class) @RequestBody UserCreateRequestVO requestVO){
return ResultResponse.success(null);
}
}
复制代码
我们指定 create 接口指定 CreateUserGroup 分组,update 接口指定 UpdateUserGroup
测试接口如下:
传递userId
,不传递userName
时,校验通过
处理验证错误
由上述测试结果中,可以看出接口抛出的一场结果并不是很友好,我们需要统一的处理一下异常以及返回结果,给予用户友好提示。具体实现,在这里不再赘述,可以移步:SpringBoot统一异常处理
总结
Spring Boot Validation 通过简化验证流程、集成 Bean Validation 规范、支持分组验证以及提供友好的错误处理,为 Java 应用开发者提供了强大而灵活的数据验证机制。最佳实践包括在控制器层使用@Validated
注解、合理利用各种验证注解、使用自定义验证注解解决特定业务需求,确保代码清晰简洁、符合规范,并提高系统的可维护性和用户体验。
文章转载自:码农Academy
原文链接:https://www.cnblogs.com/coderacademy/p/17994311
体验地址:http://www.jnpfsoft.com/?from=001
评论