【如何优雅的参数校验】:hibernate-validator 校验框架集成SpringMvc

432 阅读5分钟

hibernate-validator校验框架

一、传统的参数校验

public class TraditionalTest {


    @Test
    public void test01(){
        UserInfo userInfo = new UserInfo();
        validateUserInfo(userInfo);
    }

    private static void validateUserInfo(UserInfo userInfo){
        // 用户名校验
        String name = userInfo.getName();
        if (name == null || "".equals(name) || "".equals(name.trim())) {
            //不符合校验规则
            throw new RuntimeException("name 不符合校验规则");
        }

        // age校验
        Integer age = userInfo.getAge();
        boolean ageValidate = age > 1 && age < 200;
        if (!ageValidate) {
            throw new RuntimeException("age不符合校验规则,应在(1-200)");
        }

        //......
    }

}

二、非web环境下使用hibernate-validator

2.1 搭建环境

        <!--引入hibernate-validator-->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>7.0.1.Final</version>
        </dependency>
        <!--el 规范和Tomcat的实现,用于解析messages里面的表达式-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-el</artifactId>
            <version>10.0.22</version>
        </dependency>

2.2 validator初体验

  • 编写 ValidationUtil 工具类

    package com.zs.validation;
    
    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 = Validation.buildDefaultValidatorFactory().getValidator();
        }
    
        public static List<String>  valid(Object obj) {
            //如果被校验对象 没有校验通过,则set里面就有校验信息
            Set<ConstraintViolation<Object>> set = validator.validate(obj);
            List<String> list = set.stream().map(v -> "属性:" + v.getPropertyPath() + ",属性的值" +
                    v.getInvalidValue() + ",校验不通过的提示信息:" + v.getMessage())
                    .collect(Collectors.toList());
            return list;
        }
    }
    
  • 给需要校验的类添加注解

    import jakarta.validation.constraints.NotNull;
    
    @Data
    public class UserInfo {
        /**
         * 不能是null, "", "   "
         */
        @NotNull
        private String name;
    
  • 测试验证

    public class Demo {
    
        public static void main(String[] args) {
           
            UserInfo userInfo =new UserInfo() ;
    
            List<String> list = ValidationUtil.valid(userInfo);
            System.out.println(list);
    
        }
    }
    
    

2.3 validator加载原理

2.3.1 javaEE规范

  • javaEE规范是很多不相关的java package组成的javaee规范
  • 常见的javaEE规范
    • javax.sql ---- mysql ,sqlserver,oracle …
    • javax.jms ---- activemq
    • javax.servlet ---- tomcat,jetty …
    • javax.persistence ---- hibernate
    • javax.transaction----分布式事务
    • java.validation ----- hibernate-validator

​ jdk自带了一些常用的javaee规范,对于没有自带的如果想要使用就需要自己引用了,比如beanvalidation

2.3.2 SPI机制

  • ​ spi全称为 (Service Provider Interface),是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的 机制,一种解耦非常优秀的思想。
  • spi的工作原理: 就是ClassPath路径下的META-INF/services文件夹中, 以接口的全限定名来命名文件名,文件里面写该接口的实现。然后再资源加载的方式,读取文件的内容(接口实现的全限定名), 然后再去加载类。
  • spi可以很灵活的让接口和实现分离, 让api提供者只提供接口, 第三方来实现。

2.3.3 源码解析

49d277848567cec970282e61bac4d759.jpeg

3cc92dc6a0d135def5c8c377926ffaa0.jpeg

d70d4516ca819b76c9d711545743cfef.jpeg

2.4 常用的校验约束注解

@Null			//被注释的元素必须为null
@NotNull	    //被注释的元素必须不为null
@NotEmpty	    //被注释的集合(size > 0)/字符串(!=null && !"")
@NotBlank	    //!=null && !"" && !"   "
@AssertTrue		//被注释的元素必须为true
@AssertFalse	//被注释的元素必须为false
@Min(value)	 	//被注释的元素必须是一个数字,>=
@Max(value)		//被注释的元素必须是一个数字,<=
@DecimalMin(value)		>=
@DecimalMax(value)		<=

@Size(max,min)		//被注释的元素的大小必须在指定的范围内
@Digits(integer,fraction)	//被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past							//被注释的元素必须是一个过去的日期
@PastOrPresent		   //时间
@NegativeOrZero		  //<=0
@Future					  //被注释的元素必须是一个将来的日期
@Pattern(value)		  //被注释的元素必须符合指定的正则表达式
@Email					   //	被注释的元素必须是电子邮箱地址

2.5 约束和校验类的绑定原理

@NotNull

NotNullValidator 类校验 NotNull注解

同理 @Xxx注解的校验器类为XxxValidator

org.hibernate.validator.internal.metadata.core.ConstraintHelper

if(enabledBuiltinConstraints.contains(BuiltinConstraint.JAKARTA_VALIDATION_CONSTRAINTS_NOT_BLANK)) {
    putBuiltinConstraint(tmpConstraints, NotBlank.class, NotBlankValidator.class);
}

注意:一个注解约束可能对应多个约束Validator,如@NotEmpty

55e11ca18a12f7a565e0b6753401b492.jpeg

2.6 自定义校验规则

  • 自定义注解

    @Constraint(validatedBy = {UserStateValidator.class })
    @Target({ FIELD})
    @Retention(RUNTIME)
    public @interface UserState {
    
        String message() default "";
    
        Class<?>[] groups() default { };
    
        Class<? extends Payload>[] payload() default { };
    }
    
    
  • 自定义注解校验器

    /**
     * @description 用户状态验证器
     */
    
    public class UserStateValidator implements ConstraintValidator<UserState,Integer> {
    
    
        @Override
        public boolean isValid(Integer value, ConstraintValidatorContext context) {
    
            //如果用户状态为空 或者为1  校验失败
            if ( null==value || 1!=value) {
                return false;
            }
            return true;
    
        }
    }
    
    

2.7 分组校验

public class UserInfo {

    public interface Add{}
    public interface Update{}

    // 只用于新增校验
    @NotNull(groups = {Add.class})
    private String name;

    private Integer age;
}

public class ValidationUtil2 {

   
    private static Validator validator;

    static {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    public static List<String> valid(Object obj, Class<?>... groups) {
        //如果被校验对象 没有校验通过,则set里面就有校验信息
        Set<ConstraintViolation<Object>> set = validator.validate(obj,groups);
        List<String> list = set.stream().map(v ->
                "属性:" + v.getPropertyPath() +
                        ",属性的值" + v.getInvalidValue() +
                        ",校验不通过的提示信息:" + v.getMessage() +
                        ",消息模板(为被替换的提示信息):" + v.getMessageTemplate()
        )
                .collect(Collectors.toList());
        return list;
    }
}

2.8 @Valid级联校验

public class Grade {

    //班级号
    @NotBlank
    private String no;
}
public class UserInfo {

    public interface Add{}
    public interface Update{}

    // 只用于新增校验
    @NotNull(groups = {Add.class})
    private String name;

    private Integer age;
    
    @NotNull
    @Valid //被引用对象加@Valid注解才可以完成级联校验
    private Grade grade;
    
}

三、web环境中使用下使用hibernate-validator

3.1 搭建springboot环境

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.7.7</version>
        </dependency>
        <!--web组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.7</version>
</dependency>

3.2 使用@Valid自动校验

  • 编程式校验

      /**
         * 编程式校验
         *
         * @param userInfo 用户信息
         * @return {@link String}
         */
        @GetMapping("/addUser")
        public String addUser(UserInfo userInfo){
            List<String> result = ValidationUtil.valid(userInfo);
            System.out.println(result);
            if (result.size() > 0) {
                return "failed";
            } else {
                return "success";
      
           }
        }
    
  • 注解式校验

     @GetMapping("/addUser2")
        public String addUser2(@Valid UserInfo userInfo, BindingResult bindingResult){
            if (bindingResult.hasErrors()) { //判断是不是满足约束
                List<ObjectError> allErrors = bindingResult.getAllErrors();
                for (ObjectError error : allErrors) {
                    System.out.println(error.getObjectName() + "::" + error.getDefaultMessage());
                }
    
                //获取没通过校验的字段详情
                List<FieldError> fieldErrors = bindingResult.getFieldErrors();
                for (FieldError fieldError : fieldErrors) {
                    System.out.println(fieldError.getField() + ":" + fieldError.getDefaultMessage()
                     + ",当前没通过校验规则的值是:" + fieldError.getRejectedValue());
                }
            }
            return "ok";
        }
    

3.3 使用@Validated 自动校验

 /**
     * 编程式校验
     *
     * @param userInfo 用户信息
     * @return {@link String}
     */
    @GetMapping("/addUser3")
    public String addUser3(@Validated(value={UserInfo.Add.class}) UserInfo userInfo, BindingResult bindingResult){
        List<String> result=new ArrayList<>();
        if (bindingResult.hasErrors()) { //判断是不是满足约束
            List<ObjectError> allErrors = bindingResult.getAllErrors();

            //获取没通过校验的字段详情
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
              String s=   fieldError.getField() + ":" + fieldError.getDefaultMessage()
                        + ",当前没通过校验规则的值是:" + fieldError.getRejectedValue();
                System.out.println(s);
                result.add(s);
            }
        }
        return result.toString();
    }

@Validated 和 @Valid的区别

  • @Validated可以指定分组

  • @validated支持方法参数的自动校验

    @RestController
    @Validated //表示整个类都启用校验,如果碰到入参含有bean validation 注解的话,就会自动校验
    public class UserInfoHandler {
    
        @GetMapping("/getByName")
        public String getByName(@NotNull String name){
            return name + "ok";
        }
    

3.4 统一异常处理

  • ​ 控制层代码

     /**
         * 最终版本
         * @param userInfo
         * @return
         */
        @GetMapping("/addUser4")
        public AjaxResult addUser4(@Validated(value={UserInfo.Add.class}) UserInfo userInfo){
           //TODO 后续业务处理
            return AjaxResult.success();
        }
    
  • 全局异常处理器

    /**
     * 全局异常处理器
     */
    @RestControllerAdvice
    public class GlobalExceptionHandler
    {
        
        /**
         * 自定义验证异常
         */
        @ExceptionHandler(BindException.class)
        public AjaxResult handleBindException(BindException e)
        {
    
            String message = e.getAllErrors().get(0).getDefaultMessage();
            return AjaxResult.error(message);
        }
    
    
    }