数据验证机制-SpringBoot Validation
日常开发中,项目采用的是在代码中手动校验数据。但是手动校验数据会带来代码冗余、错误处理的一致性以及业务规则的维护的一些痛点。
- 代码冗余的手动校验逻辑,导致代码中大量的if-else
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,我们能够有效解决这些痛点,提高代码的可读性、可维护性,并确保校验逻辑的一致性。
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 提供了一系列注解,用于在实体类中定义验证规则。以下是一些常用的校验相关的注解及其功能以及用法:
我们需要使用全局异常类捕获一下MethodArgumentNotValidException
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(com.gzist.component.GlobalExceptionHandler.class);
@ExceptionHandler(MethodArgumentNotValidException.class)
public HttpResultResponse MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
e.printStackTrace();
return HttpResultResponse.error("MethodArgumentNotValidException");
}
}
还有我们的全局返回结果类
import java.util.List;
import com.dji.sample.common.model.page.TableDataInfo;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "The data format of the http response.")
public class HttpResultResponse<T> {
public static final int CODE_SUCCESS = 0;
public static final int CODE_FAILED = -1;
public static final String MESSAGE_SUCCESS = "success";
public static final String MESSAGE_FAILED = "failed";
@Schema(description = "0 means success, non-zero means error.", example = "0")
private int code;
@Schema(description = "The response message.", example = MESSAGE_SUCCESS)
private String message;
@Schema(description = "The response data.")
private T data;
public HttpResultResponse() {
}
public static TableDataInfo getDataTable(List<?> list) {
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(CODE_SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list);
// rspData.setTotal(new PageInfo(list).getTotal());
return rspData;
}
@Override
public String toString() {
return "HttpResultResponse{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
'}';
}
public int getCode() {
return code;
}
public HttpResultResponse<T> setCode(int code) {
this.code = code;
return this;
}
public String getMessage() {
return message;
}
public HttpResultResponse<T> setMessage(String message) {
this.message = message;;
return this;
}
public T getData() {
return data;
}
public HttpResultResponse<T> setData(T data) {
this.data = data;
return this;
}
public static HttpResultResponse success() {
return new HttpResultResponse()
.setCode(CODE_SUCCESS)
.setMessage(MESSAGE_SUCCESS)
.setData("");
}
public static <T> HttpResultResponse<T> success(T data) {
return HttpResultResponse.success().setData(data);
}
public static HttpResultResponse error() {
return new HttpResultResponse()
.setCode(CODE_FAILED)
.setMessage(MESSAGE_FAILED);
}
public static HttpResultResponse error(String message) {
return new HttpResultResponse()
.setCode(CODE_FAILED)
.setMessage(message);
}
public static HttpResultResponse error(int code, String message) {
return new HttpResultResponse()
.setCode(code)
.setMessage(message);
}
public static HttpResultResponse error(IErrorInfo errorInfo) {
return new HttpResultResponse()
.setCode(errorInfo.getCode())
.setMessage(errorInfo.getMessage());
}
}
Spring Boot Validation
提供了一系列注解,用于在实体类中定义验证规则。
@Null
被注释的元素必须为null
@NotNull
被注释的元素不能为null,可以为空字符串
@AssertTrue
被注释的元素必须为true
@AssertFalse
被注释的元素必须为false
@Min(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max,min)
被注释的元素的大小必须在指定的范围内。
@Digits(integer,fraction)
被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past
被注释的元素必须是一个过去的日期
@Future
被注释的元素必须是一个将来的日期
@Pattern(value)
被注释的元素必须符合指定的正则表达式。
@Length
被注释的字符串的大小必须在指定的范围内
@Range
被注释的元素必须在合适的范围内
@NotEmpty
用在集合类上,不能为null,并且长度必须大于0
@NotBlank
只能作用在String上,不能为null,而且调用trim()后,长度必须大于0
用法示例:
@NotNull(message = "Name cannot be null")
private String name;
@NotBlank(message = "Username cannot be blank")
private String username;
@NotEmpty(message = "List cannot be empty")
private List<String> items;
@Length(min = 5, max = 20, message = "Length must be between 5 and 20 characters")
private String username;
@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;
@Min(value = 18, message = "Age must be at least 18")
private int age;
@Max(value = 100, message = "Age must not exceed 100")
private int age;
@DecimalMax(value = "100.00", inclusive = true, message = "Value must be less than or equal to 100.00")
private BigDecimal amount;
@DecimalMin(value = "0.00", inclusive = false, message = "Value must be greater than 0.00")
private BigDecimal amount;
@Email(message = "Invalid email address")
private String email;
@Pattern(regexp = "[a-zA-Z0-9]+", message = "Only alphanumeric characters are allowed")
private String username;
@Digits(integer = 5, fraction = 2, message = "Number must have up to 5 integer digits and 2 fraction digits")
private BigDecimal amount;
@Past(message = "Date must be in the past")
private LocalDate startDate;
@Future(message = "Date must be in the future")
private LocalDate endDate;
1.定义接口入参请求参数
@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);
}
}
注:单参数校验时我们需要,在方法的类上加上@Validated
注解,否则校验不生效。
3.嵌套校验
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
// 嵌套验证必须用 @Valid
@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
注意:嵌套验证必须在子参数上用 @Valid。
4.分组校验
场景:多个 Restfull 接口共用一个标准 Bean,每个接口的参数相同,但是需要校验的参数(必输项)却不完全相同,这样的场景可以使用 @Validated,因为它提供了分组校验的功能。
隐式分组:
1.没有显式分组的默认都是 Default 组;
2.显式分组之后,剩下的那些没有被划分到自建组的字段都属于 Default 组;
3.平常我们写 @Validated
注解的时候,不写分组的话默认就是 @Validated(group = {Default.class});
显式分组:
1.自定义interface接口的分组,属于自建组;
2.自建组可以继承 Default.class,也可以不继承 Default.class,两者意义不同;
3.多个分组可以一起实用;
4.分组机制让我们可以很灵活的使用对象里面的某些字段,以实现高权限等级参数传递校验等操作。
//只能在Delete和Update的时候才能够进行生效.
@Min(value = 1,message = "ID不能小于1",groups = {ValidGroup.Delete.class,ValidGroup.Update.class})
private int id;
@NotBlank(message = "用户名不能为空",groups = {ValidGroup.Update.class,ValidGroup.Insert.class})
private String username;
@NotBlank(message = "密码不能为空",groups = {ValidGroup.Update.class,ValidGroup.Insert.class})
@Length(min = 6,max = 20,message = "密码长度在6-20之间")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不合理")
private String email;
- 新建分组
public class ValidGroup {
// 新增使用(配合spring的@Validated功能分组使用)
public interface Insert{}
// 更新使用(配合spring的@Validated功能分组使用)
public interface Update{}
// 删除使用(配合spring的@Validated功能分组使用)
public interface Delete{}
// 属性必须有这两个分组的才验证(配合spring的@Validated功能分组使用)
@GroupSequence({Insert.class, Update.class,Delete.class})
public interface All{}
}
Controller接口测试
@RequestMapping("/saveUserInfo")
public UserInfo saveUserInfo(@Validated() UserInfo userInfo){
//save userInfo:将userInfo进行保存
//userInfoService.save(userInfo);
return userInfo;
}
@RequestMapping("/updateUserInfo")
public UserInfo updateUserInfo(@Validated({ValidGroup.Update.class}) UserInfo userInfo){
//save userInfo:将userInfo进行保存
//userInfoService.update(userInfo);
return userInfo;
}
@RequestMapping("/deleteUserInfo")
public UserInfo deleteUserInfo(@Validated({ValidGroup.Delete.class}) UserInfo userInfo){
//save userInfo:将userInfo进行保存
//userInfoService.delete(userInfo);
return userInfo;
}