SpringBoot 中的 Validation

238 阅读7分钟

SpringBoot 中的 Validation

jarkarta bean validation 是javaEE规范之一,是为了使代码更简洁

hibernate-validator bean validation 规范的最佳实现

Spring validation:spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中

beanvalidation官网 beanvalidation.org/

hibernate-validator官网: hibernate.org/validator/

1、validation starter

<!--validation依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2、约束注解

2.1 校验空值

  • @Null:验证对象是否为 null
  • @NotNull:验证对象是否不为 null
  • @NotEmpty:验证对象不为 null,且长度(数组、集合、字符串等)大于 0
  • @NotBlank:验证字符串不为 null,且去除两端空白字符后长度大于 0

2.2 校验大小

  • @Size(min=, max=):验证对象(数组、集合、字符串等)长度是否在给定的范围之内
  • @Min(value):验证数值(整数或浮点数)是否大于等于指定的最小值
  • @Max(value):验证数值是否小于等于指定的最大值

2.3 校验布尔值

  • @AssertTrue:验证 Boolean 对象是否为 true
  • @AssertFalse:验证 Boolean 对象是否为 false

2.4 校验日期和时间

  • @Past:验证 Date 和 Calendar 对象是否在当前时间之前
  • @Future:验证 Date 和 Calendar 对象是否在当前时间之后
  • @PastOrPresent:验证日期是否是过去或现在的时间
  • @FutureOrPresent:验证日期是否是现在或将来的时间

2.5 正则表达式

  • @Pattern(regexp=, flags=):验证 String 对象是否符合正则表达式的规则

2.6 Hibernate Validation 拓展

  • @Length(min=, max=):验证字符串的大小是否在指定的范围内
  • @Range(min=, max=):验证数值是否在合适的范围内
  • @UniqueElements:校验集合中的值是否唯一,依赖于 equals 方法
  • @ScriptAssert:利用脚本进行校验

3、分组校验

  • 定义分组,
  • 定义校验项时指定归属的分组
  • 校验时指定要校验的分组

注意: 没指定的校验项默认属于Default分组,分组可以继承

@Data
public class Category {
    @NotNull(groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty
    private String categoryName;//分类名称
    @NotEmpty
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;//创建时间
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;//更新时间
    //如果说某个校验项没有指定分组,默认属于Default分组
    //分组之间可以继承, A extends B  那么A中拥有B中所有的校验项
    public interface Add extends Default {
    }
    public interface Update extends Default{
    }
}
    @PutMapping
    public  Result update(@RequestBody @Validated(Category.Update.class) Category category){
        categoryService.upadate(category);
        return Result.success();
    }

4、级联校验

即一个对象的私有成员是一个引用类型 需要给那个引用类型加上@Valid注解

UserInfo.java

package com.bean;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

public class UserInfo {
    private Long id;
    private String name;
    private Integer age;
    @Valid
    private Grade grade;
}

Grade.java

public class Grade {
    @Min(60)
    @Max(100)
    private  Integer Chinese;
    @Min(60)
    @Max(100)
    private Integer English;
    @Min(60)
    @Max(100)
    private Integer Math;
}

5、校验简单参数

要在类上加@Validated

@RequestMapping("/notice")
@RestController
// 必须加上该注解
@Validated
public class UserController {
  // 路径变量
  @GetMapping("{id}")
  public Reponse<NoticeDTO> detail(@PathVariable("id") @Min(1L) Long noticeId) {
    // 参数noticeId校验通过,执行后续业务逻辑
    return Reponse.ok();
  }
  // 请求参数
  @GetMapping("getByTitle")
  public Result getByTitle(@RequestParam("title") @Length(min = 1, max = 20) String  title) {
    // 参数title校验通过,执行后续业务逻辑
    return Result.ok();
  }
}

6、自定义注解

​ 在使用Spring Boot的请求参数校验时,有时候标准的校验注解无法满足特定的业务需求,这时可以通过自定义验证注解来定制校验规则。

创建自定义验证注解的一般步骤如下:

1、创建注解:创建一个新的注解,用于标记需要进行自定义验证的参数。

2、创建自定义校验器: 创建一个实现ConstraintValidator接口的自定义校验器类,用于实现具体的校验逻辑。校验器需要重写isValid()方法。

3、应用自定义注解: 在请求参数对象的字段上应用自定义注解。

4、控制器中使用: 在控制器中使用@Valid注解对参数进行验证。

定义注解,并用@Constraint绑定校验器

@Target({ElementType.FIELD, ElementType.PARAMETER})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
// 指定校验器
@Constraint(validatedBy = UniqueValidator.class)  
public @interface Unique {  

  // 用于自定义验证信息
  String message() default "字段存在重复";  

  // 指定集合中的待校验字段
  String[] field();  

  // 指定分组
  Class<?>[] groups() default {};  
}

制作校验器

// 实现ConstraintValidator<T, R>接口,T为注解的类型,R为注解的字段类型
public class UniqueValidator implements ConstraintValidator<Unique, Collection<?>> {  
  
  private Unique unique;  

  @Override  
  public void initialize(Unique constraintAnnotation) {  
      this.unique = constraintAnnotation;  
  }  

  @Override  
  public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {
    if(){
       return false  //校验后未通过 
    }  else{
        return true  //校验后通过
    }
  }  
}

6.1 自定义注解验证

用户名唯一校验示例

本案例通过自定义验证注解实现注册功能中用户名唯一的校验。

首先,新建valid包,用于存放自定义注解的相关内容。

然后,在valid包下声明自定义注解UniqueUsername,代码如下:

import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 用于验证用户名是否唯一的注解
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueUsername {
    String message() default "用户名已存在";
    // 下面两个属性必须添加
    Class<?>[] groups() default {}; // 用于分组校验
    Class<? extends Payload>[] payload() default {}; // 用于给出验证失败的详细信息
}

接下来,在valid包下声明自定义校验器UniqueUsernameValidator,该类需要实现ConstraintValidator接口并实现isValid方法。需要特别注意,该类前需要添加@Component注解才能生效,@Component注解的作用,将在后续的课程中展开讲解。具体代码如下:

import cn.highedu.boot01.util.DBUtil;
import io.micrometer.common.util.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@Component
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
    @Override
    public boolean isValid(String username, ConstraintValidatorContext constraintValidatorContext) {
        // 用户名为空时不进行校验
        if (StringUtils.isBlank(username)) {
            return true;
        }
        //获取数据库连接
        try (Connection conn = DBUtil.getConnection()){
            //准备插入数据的SQL语句
            String sql = "select * from user where username = ?";
            //创建执行SQL语句的对象
            PreparedStatement ps = conn.prepareStatement(sql);
            //替换SQL语句中的?
            ps.setString(1, username);
            //执行SQL语句
            ResultSet rs = ps.executeQuery();
            if (rs.next()) { // 如果查询到了数据,说明用户名已存在
                return false;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 如果没有查询到数据,说明用户名不存在
        return true;
    }
}

自定义校验器开发完成后,需要在自定义注解前添加@Constraint注解,绑定自定义检验器:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueUsernameValidator.class)
public @interface UniqueUsername {
    
}

接下来,在User类的username属性前添加该自定义注解:

public class User {
    private Integer id;
    @NotBlank(message = "用户名不能为空")
    @Size(min = 4, max = 20, message = "用户名长度的范围为4~20")
    @UniqueUsername
    private String username;

控制器中, 通过BindingResult获取报错信息

@RequestMapping("/users/register")
@ResponseBody
public String register(@Valid @RequestBody User user,
                       BindingResult result) {
       if (result.hasErrors()) {
              //返回验证错误
              return result.getFieldError().getDefaultMessage();
       }
}

7、全局异常处理器

用于处理一些接口业务异常,比如用户输入参数校验失败等。

曾经只能用try-catch处理,但现在可以用全局异常处理器处理

可以通过@ControllerAdvice 注解配置一个全局异常处理类,来统一处理 controller 层中的异常(最后从contoller层抛出去的也可以),于此同时 controller 中可以不用再写 try/catch,这使得代码既整洁又便于维护。

@RestControllerAdvice + @ExceptionHandler

@RestControllerAdvice 组合   
	@ResponseBody
	@ControllerAdvice 组合   定义该类为全局异常处理类
		@Component
@ExceptionHandler    定义该方法为异常处理方法
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BindingException.class)   //选择要处理的异常
    public Result handleException(BindingException e){
        e.printStackTrace();
        log.error("出现了异常! {}", e);
        BindingResult bindingResult = e.getBindingResult();
        List<String> errors = new ArrayList<>();
        bindingResult.getFieldErrors().stream().forEach(item -> {
          errors.add(item.getDefaultMessage());
        });
        String errorMessage = StringUtils.join(errors, '|');
        return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, errorMessage);
    }
}

统一异常处理这个功能可以使用两个注解:@RestControllerAdvice 和 @ControllerAdvice。

这两个注解都可以用来定义全局异常处理器,但是它们的使用场景略有不同。@ControllerAdvice 注解通常用于处理传统的 MVC 应用程序中的异常,例如使用 Thymeleaf、JSP 或者 FreeMarker 等视图技术构建的 Web 应用程序。而 @RestControllerAdvice 注解则通常用于处理 RESTful 服务中的异常,并返回 JSON 格式的数据。

此外,@ControllerAdvice 注解中定义的异常处理方法可以返回 ModelAndView 对象,以实现跳转到错误页面的功能,而 @RestControllerAdvice 注解中定义的异常处理方法则通常返回响应体,返回 JSON 格式的数据

@Valid 和 @Validated

这两个注解是校验的入口,作用相似但用法上存在差异。

// 用于类/接口/枚举,方法以及参数
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})  
@Retention(RetentionPolicy.RUNTIME)
@Documented  
public @interface Validated {  
  // 校验时启动的分组  
  Class<?>[] value() default {};  
}
// 用于方法,字段,构造函数,参数,以及泛型类型  
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })  
@Retention(RUNTIME)  
@Documented
public @interface Valid {  
  // 未提供其他属性  
}
  1. 作用范围不同@Validated 无法作用在于字段, @Valid 无法作用于类;
  2. 注解中的属性不同@Validated 中提供了指定校验分组的属性,而 @Valid 没有这个功能,因为 @Valid 不能进行分组校验。