springboot+Validation做数据校验太优雅了

数据验证机制-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) 被注释的元素必须符合指定的正则表达式。
@Email 被注释的元素必须是电子邮件地址
@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;
}

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值