java代码校验——全网最详细

导读
在 JavaEE 项目中, RestFull 层接收参数首先要对一些字段的格式进行校验,以防止所有查询都落到数据库,这也是一种合理的限流手段。以前基本上都是用 if…else…,这样的代码太啰嗦,除了使用策略模式进行优化,今天介绍一下校验注解@Valid,@Validated和@PathVariable,不仅可以减轻代码量,还加强了代码的易读性。

1. @Valid 和 @Validated 区别与联系

Hibernate Validator 是 Bean Validation 的参考实现 。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。
在SpringBoot中可以使用@Validated,注解Hibernate Validator加强版,也可以使用@Valid原来Bean Validation java版本

先讲一下这两个注解:@Valid与@Validated都是用来校验接收参数的,如果不使用注解校验参数,那么就需要在业务代码中逐一校验,这样会增加很多的工作量,并且代码不优美。

刚开始接触的时候多半会被弄混,实际上二者差距还是挺大的。根据自己的项目经验,@Validated和@Valid各有特点,可以联合使用。

在这里插入图片描述
校验流程解析
使用 Validation API 进行参数效验步骤整个过程如下图所示,用户访问接口,然后进行参数效验 ,如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理
在这里插入图片描述

(1)提供者

Validated

即org.springframework.validation.annotation.Validated
只用 Spring Validator 校验机制使用,是 Spring 做得一个自定义注解,增强了分组功能;

Valid

即javax.validation.Valid。 使用 Hibernate validation 的时候使用,是 JSR-303规范标准注解支持。如果你是 springboot 项目,那么可以不用单独引入依赖了,因为它就存在于最核心的 web开发包(spring-boot-starter-web)里面;

(2)标注位置

@Validated:可以用在类型、方法和方法参数上,不能用于成员属性(field)上。如果注解在成员属性上,则会报不适用于field的错误;
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)、YPE_US上;

注:TYPE_USE:在 Java 8 之前的版本中,只能允许在声明式前使用 Annotation。而在 Java 8 版本中,Annotation 可以被用在任何使用 Type 的地方,例如:初始化对象时 (new),对象类型转化时,使用 implements 表达式时,或者使用 throws 表达式时。

//初始化对象时
String myString = new @Valid String();

//对象类型转化时
myString = (@Valid String) str;

//使用 implements 表达式时
class MyList<T> implements List<@Valid T> {...}

//使用 throws 表达式时
public void validateValues() throws @Valid ValidationFailedException{...}

(3)分组支持

@Validated:提供分组功能,可以在参数验证时,根据不同的分组采用不同的验证机制;
@Valid:没有分组的功能,不能进行分组校验;

(4)嵌套支持

@Validated:不能进行嵌套对象校验;
@Valid:可以进行嵌套校验,但是需要在嵌套的字段上面加上注解;

2. 常用的校验方法

Maven依赖的引入:
springboot 2.3.0 以后不会自动引入jar包,所以要添加以下maven,springboot2.3以前则不需要引入maven包

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

搭配@Valid或者@Validated使用
@Valid、@Validated 注解可以实现数据的验证,你可以定义实体,在实体的属性上添加校验规则

@Valid 包位置

import javax.validation.Valid;

@Validated 包位置

import org.springframework.validation.annotation.Validated;

Debug进入jar包,可以看到全量的相关注解:
在这里插入图片描述
简述一些常用注解:
空检查

@Null: 所注解的元素必须为 null,接受任何类型。

@NotNull:
(1)限制必须不能为null,但可以为 empty, 可以接受任何类型。
(2)empty:空集合、空数组、空字符等等
(2)一般用于判空 Integer 类型等基本数据类型,而且被其标注的字段可以使用@size、@Max、@Min 对数值进行大小的控制

@NotEmpty:
(1)验证注解的元素值不为null且不为空
(2)不为空:即长度必须大于0,字符串长度不为0、集合大小不为0
(3)一般用于集合类或者数组上,也有人用于String(不推荐)。比如校验 CharSequence、数组、Collection 和 Map。

@NotBlank:
(1)验证注解的元素值不为空且不能为空字符。即:必须有实际字符
(2)不能为空字符:调用 trim() 方法后,长度必须大于0。
(3)不同于@NotEmpty,@NotBlank注解只应用于String 类型上,且在比较时会去除字符串的空格
(4)用于校验CharSequence(含String、StringBuilder和StringBuffer)。只支持字符类型。

Boolean检查

@AssertTrue:被注释的元素必须为true
@AssertFalse:被注释的元素必须为false

长度检查

@Size(max, min): 被注释的元素的大小必须在指定的范围内。一般用于限制数组、集合长度范围必须在min到max之间。也可以用于限制字符串在指定的范围内(不推荐)
@Length(min=, max=) :被注释的字符串的大小必须在指定的范围内,即验证注解的元素值长度在min和max区间内,只能用于字符串

日期检查:Date/Calendar

@Past:被注释的元素必须是一个过去的日期
@PastOrPresent: 验证元素值必须是当前时间或一个过去的时间,认为 null 是有效的
@Future:被注释的元素必须是一个将来的日期
@FutureOrPresent: 验证元素值必须是当前时间或一个将来的时间,认为 null 是有效的

数值检查:建议使用在Stirng,Integer类型

@Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Digits:验证是否为数字
@Digits(integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Positive:验证元素必须为正数,认为 null 是有效的。BigDecimal、BigInteger,byte、short、int、long、float、double 及其包装类型
@PositiveOrZero: 同Positive,验证元素必须为正数或 0,认为 null 是有效的
@Negative: 验证元素必须为负数,认为 null 是有效的
@NegativeOrZero: 验证元素必须为负数或 0,认为 null 是有效的
@Range(min=long1,max=long2):被注释的元素必须在合适的范围内,即检查数字是否在范围min到max之间
该注解可以用来验证数字与字符串类型,数值或者字符串的值必须在 min 和 max 指定的范围内

字符验证

@URL(protocol=协议,host=主机,port=端口号,regexp=正则表达式,flags=标识):用于校验一个字符串是否是合法URL
@Pattern(regexp=“reg”):被注释的元素必须符合指定的正则表达式。

注意:如果 @Pattern 所注解的元素是null,则@Pattern 注解会返回 true,即也会通过校验,所以应该把 @Pattern 注解和 @NotNull 注解结合使用。

其他验证

@Vaild:递归验证,用于任何非原子类型,比如用于对象、数组和集合,会对对象的元素、数组的元素进行一一校验
@Email:用于验证一个字符串是否是一个合法的右键地址,空字符串或null算验证通过

message属性

(1)message支持表达式和EL表达式 ,比如message = “姓名长度限制为{min}到{max} ${1+2}”)
(2)几乎所有的校验注解都有该注解
(3)如果想把错误描述统一写到properties的话,在classpath下面新建
ValidationMessages_zh_CN.properties文件(注意value需要转换为unicode编码),然后用{}格式的占位符

3. @Validated分组校验

场景:多个 Restfull 接口共用一个标准 Bean,每个接口的参数相同,但是需要校验的参数(必输项)却不完全相同,这样的场景可以使用 @Validated,因为它提供了分组校验的功能。

在这里插入图片描述
注意:

(1)没有指定显示分组的被校验字段和校验注解,默认都是 Default 组(即 Default.class)
(2)若自定义的分组接口未继承 Default 分组,且 @Validated(或 @Valid)注解未传参 Default.class,则只会校验请求对象中进行了显示分组的字段,不会校验默认分组(没有进行显示分组)的字段
(3)自定义的分组接口不继承 Default 分组 + @Validated(或 @Valid)注解传参 {自定义分组接口.class, Default.class}
(4)= 自定义的分组接口继承 Default 分组 + @Validated(或 @Valid)注解只传参自定义分组接口

案例1

如果同一个参数,需要在不同场景下应用不同的校验规则,就需要用到分组校验了。比如:新注册用户还没起名字,我们允许name字段为空,但是在更新时候不允许将名字更新为空字符。

分组校验有三个步骤:

第一步:定义一个分组类(或接口)

public interface Update extends Default{
}

第二步:在校验注解上添加groups属性指定分组

public class UserVO {
    @NotBlank(message = "name 不能为空",groups = Update.class)
    private String name;
    // 省略其他代码...
}

第三步:Controller方法的@Validated注解添加分组类

@PostMapping("update")
public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
    return new ResultInfo().success(userVO);
}

自定义的Update分组接口继承了Default接口。校验注解(如: @NotBlank)和@validated默认其他注解都属于Default.class分组,这一点在javax.validation.groups.Default注释中有说明

/**
 * Default Jakarta Bean Validation group.
 * <p>
 * Unless a list of groups is explicitly defined:
 * <ul>
 *     <li>constraints belong to the {@code Default} group</li>
 *     <li>validation applies to the {@code Default} group</li>
 * </ul>
 * Most structural constraints should belong to the default group.
 *
 * @author Emmanuel Bernard
 */
public interface Default {
}

在编写Update分组接口时,如果继承了Default,下面两个写法就是等效的:
@Validated({Update.class}),@Validated({Update.class,Default.class})
如果Update不继承Default,@Validated({Update.class})就只会校验属于Update.class分组的参数字段

案例2

第一步:新建请求对象

@Data
public class TeacherDTO {
 
    @NotBlank(message = "id必传")
    private String id;
 
    @NotBlank(message = "不能没有名称")
    private String name;
 
    @NotNull(message = "age必传")
    private Integer age;
 
    @NotBlank(message = "不能没有idCard")
    private String idCard;
 
    @NotBlank(message = "老师不能没有手机号", groups = OnlyTeacher.class)
    private String phone;
 
    @NotEmpty(message = "学生不能没有书")
    @Size(min = 2, message = "学生必须有两本书", groups = OnlyStudent.class)
    private List<String> bookNames;
 
    @NotEmpty
    @Size(min = 1, message = "老师不能没有学生", groups = TeacherWithDefault.class)
    private List<String> studentList;
}

第二步:新建分组

// Teacher分组
public interface TeacherValid { }
 
// Student分组
public interface StudentValid { }
 
// 继承Default的分组
public interface OtherValid extends Default{ }

第三步:接口测试

/**
 * Created by tjm on 2022/11/11.
 */
@RestController
@RequestMapping("/test")
public class TestValidController {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestValidController.class);
 
    /**
     * 测试 - 分组校验 - 默认default
     */
    @PostMapping("/only/default")
    public Object testDefaultValid(@Validated TeacherDTO param, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
        }
        return ResultGenerator.genSuccessResult();
    }
    
    /**
     * 测试 - 分组校验 - 只有teacher
     */
    @PostMapping("/only/teacher")
    public Object testOnlyTeacherValid(@Validated(OnlyTeacher.class) TeacherDTO param, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
        }
        return ResultGenerator.genSuccessResult();
    }
 
    /**
     * 测试 - 分组校验 - 只有student
     */
    @PostMapping("/only/student")
    public Object testOnlyStudentValid(@Validated(OnlyStudent.class) TeacherDTO param, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
        }
        return ResultGenerator.genSuccessResult();
    }
 
    /**
     * 测试 - 分组校验 - teacher + default
     */
    @PostMapping("/with/teacher")
    public Object testWithTeacherValid(@Validated({OnlyTeacher.class, Default.class}) TeacherDTO param, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
        }
        return ResultGenerator.genSuccessResult();
    }
 
 
    /**
     * 测试 - 分组校验 - 继承default
     */
    @PostMapping("/with/default")
    public Object testWithDefaultValid(@Validated(TeacherWithDefault.class) TeacherDTO param, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
        }
        return ResultGenerator.genSuccessResult();
    }
}

结果
在这里插入图片描述

案例3

分组使用
有时在同一个 PO 类上,同一个属性在不同的请求中验证规则不一样,比如 ID, 在新增时可以为空,但是在更新是不能为空。这种校验方式可以使用 @Validated 根据分组指定校验规则。

1、定义分组
// 分组必须为接口,它只是一个标记类,不需要有任何方法

// 更新分组, 
public interface Update {
}

// 新增分组
public interface Add {
}

2、PO指定分组

@Data
public class BannersEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 编号
     *   指定校验分组为 Update
     */
    @TableId
    @NotNull(message = "id不能为空", groups = {Update.class}) //
    private Long id;
    /**
     * 图片名称
     * 指定校验分组为 Update 和 add
     */
    @NotBlank(message = "图片名称不能为空", groups = {Add.class, Update.class})
    private String name;
  
}

3、请求指定分组

  /**
     * 保存
     *  校验器  @Validated 指定  Add 分组
     */
    @RequestMapping("/save")
    public R save(@RequestBody 
                  @Validated(value = {Add.class}) BannersEntity banners) {           
        bannersService.save(banners);
        return R.ok();
    }

    /**
     * 修改
     *  校验器  @Validated 指定  Update 分组
     */
    @RequestMapping("/update")
    public R update(@RequestBody @Validated(value = {Update.class}) BannersEntity banners) {
        bannersService.updateById(banners);
        return R.ok();
    }

4. @Valid嵌套校验

@Valid: 嵌套校验 = 入参@Valid + 属性上@Valid

用在方法入参上无法单独提供嵌套验证功能。
能配合嵌套验证注解@Valid进行嵌套验证。
能够用在成员属性(字段)上,提示验证框架进行嵌套验证。

@Validated: 嵌套校验 = 入参@Validated + 属性上@Valid

用在方法入参上无法单独提供嵌套验证功能。
能配合嵌套验证注解@Valid进行嵌套验证。
不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。

注意:

(1)支持外部类嵌套校验 (2)同时也支持内部类嵌套校验

新建请求对象

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;
}

接口测试

  /**
     * 测试 - 分组校验 - 继承default
     */
    @PostMapping("/item")
    public Object testItemValid(@Validated Item param, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
        }
        return ResultGenerator.genSuccessResult();
    }

测试结果

1. 不仅校验 Item 参数,还会校验子类 Prop 参数;
2. 注意:嵌套验证必须在子参数上用 @Valid。

只是在方法参数前面添加 @Valid和 @Validated注解,不会对嵌套的实体类进行校验.要想实现对嵌套的实体类进行校验,需要在嵌套的实体类属性上添加 @Valid注解

5. 自定义校验

校验器的格式

public class XXXValidator implements ConstraintValidator<CheckXXX, Object> {

    private String name;

    private int age;

    @Override
    public void initialize(CheckTimeInterval constraintAnnotation) {
        this.name = constraintAnnotation.name();
        this.age = constraintAnnotation.age();
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        BeanWrapper beanWrapper = new BeanWrapperImpl(o);
        String name = (String)beanWrapper.getPropertyValue(name);
        int age = (Integer)beanWrapper.getPropertyValue(age);
        return age > 10 && name.equals("abc");
    }
}
  • ConstraintValidator<>:第二个参数表示注解修饰的对象类型。(类注解就是Object)
  • initialize():类初始化方法。
  • isValid():返回true表示校验通过,返回false表示校验失败,返回message的错误提示信息。

当组件提供的校验器, 不满足需求时,可以自定义校验器,步骤如下:
1、定义校验注解

// 定义 MyValid  校验主机
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) //  指定可以使用的目标
@Retention(RetentionPolicy.RUNTIME) // 
@Constraint(validatedBy = {MyValidConstraint.class}) //指定校验器
public @interface MyValid {
   
    // 其中 
    String message() default "校验失败"; // 校验不通过时的提示信息
    Class<?>[] groups() default {};    // 分组
    Class<? extends Payload>[] payload() default {}; // 载体
}

说明:自定义校验器必须要有 message,groups,payload 这三项,此外可以进行扩展

2、定义校验器

// 实现接口 ConstraintValidator
public class MyValidConstraint implements ConstraintValidator<MyValid, Object> {

    private MyValid myValid;

    @Override
    public void initialize(MyValid myValid) {
        this.myValid = myValid;
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {  
        // TODO 此处些具体校验逻辑   
        return true; // 返回 true 表示验证通过,false 表示验证失败
    }
}

案例1

第一步:自定义注解

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE,
        ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckTimeIntervalValidation.class)
@Documented
@Repeatable(CheckTimeInterval.List.class)
public @interface CheckTimeInterval {
    /**
     * 开始日期
     * @return field
     */
    String beginTime() default "startTime";

    /**
     * 查询参数时间类型
     * @return field
     */
    String timeType() default "LocalDateTime";

    /**
     * 结束日期
     * @return field
     */
    String endTime() default "endTime";

    /**
     * 日期间隔
     * @return field
     */
    int dayRange() default 30;

    String message() default "{org.hibernate.validator.referneceguide.chapter06.CheckCase.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
            ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {

        CheckTimeInterval[] value();
    }
}

第二步:自定义解析注解工具类

public class CheckTimeIntervalValidation implements ConstraintValidator<CheckTimeInterval, Object> {

    private String beginTime;

    private String endTime;

    private int dayRange;

    private String timeType;

    @Override
    public void initialize(CheckTimeInterval constraintAnnotation) {
        this.beginTime = constraintAnnotation.beginTime();
        this.endTime = constraintAnnotation.endTime();
        this.dayRange = constraintAnnotation.dayRange();
        this.timeType=constraintAnnotation.timeType();
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        BeanWrapper beanWrapper = new BeanWrapperImpl(o);
        long result=0;
        if ("LocalDateTime".equals(timeType)){
            LocalDateTime begin = (LocalDateTime) (beanWrapper.getPropertyValue(beginTime));
            LocalDateTime end = (LocalDateTime) (beanWrapper.getPropertyValue(endTime));
            if (null == begin || null == end) {
                return false;
            }
            // result = end.compareTo(begin);
            result=begin.until(end, ChronoUnit.DAYS);
        }else if ("LocalDate".equals(timeType)){
            LocalDate begin = (LocalDate) (beanWrapper.getPropertyValue(beginTime));
            LocalDate end = (LocalDate) (beanWrapper.getPropertyValue(endTime));
            if (null == begin || null == end) {
                return false;
            }
            result=begin.until(end, ChronoUnit.DAYS);
        }

        return result>=0&&result <= dayRange;
    }
}

第三步:测试

@CheckTimeInterval(beginTime = "start",endTime = "end",timeType="LocalDateTime"
,dayRange = 14,message = "开始时间必须小于结束时间,只能查询14天的日期范围数据")
@CheckTimeInterval(beginTime = "start",endTime = "end",timeType="LocalDateTime"
,dayRange = 28,message = "开始时间必须小于结束时间,只能查询28天的日期范围数据")
public class RunningRecordQuery {
	...
}

参考:https://blog.csdn.net/Lemon_MY/article/details/128645810

案例2

validation 为我们提供了这么多特性,几乎可以满足日常开发中绝大多数参数校验场景了。但是一个好的框架一定是方便扩展的。有了扩展能力,就能应对更多复杂的业务场景,毕竟在开发过程中,唯一不变的就是变化本身。 Validation允许用户自定义校验

实现步骤很简单分两步:
第一步:创建一个自定义校验注解

package cn.soboys.core.validator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;


/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:49 
 * 日期验证 约束注解类
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsDateTimeValidator.class}) // 标明由哪个类执行校验逻辑
public @interface IsDateTime {

    // 校验出错时默认返回的消息
    String message() default "日期格式错误";
    //分组校验
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    //下面是我自己定义属性
    boolean required() default true;
    String dateFormat() default "yyyy-MM-dd";
}

注意:message用于显示错误信息这个字段是必须的,groups和payload也是必须的
@Constraint(validatedBy = {
HandsomeBoyValidator.class})用来指定处理这个注解逻辑的类

第二步:编写校验者类

package cn.soboys.core.validator;

import cn.hutool.core.util.StrUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:51
 * 日期验证器
 */
public class IsDateTimeValidator implements ConstraintValidator<IsDateTime, String> {

    private boolean required = false;
    private String dateFormat = "yyyy-MM-dd";

    /**
     * 用于初始化注解上的值到这个validator
     * @param constraintAnnotation
     */
    @Override
    public void initialize(IsDateTime constraintAnnotation) {
        required = constraintAnnotation.required();
        dateFormat = constraintAnnotation.dateFormat();
    }

    /**
     * 具体的校验逻辑
     * @param value
     * @param context
     * @return
     */
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (required) {
            return ValidatorUtil.isDateTime(value, dateFormat);
        } else {
            if (StrUtil.isBlank(value)) {
                return true;
            } else {
                return ValidatorUtil.isDateTime(value, dateFormat);
            }
        }
    }
}

注意这里验证逻辑我抽出来单独写了一个工具类,ValidatorUtil

package cn.soboys.core.validator;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:51
 * 验证表达式
 */
public class ValidatorUtil {
    private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
    private static final Pattern money_pattern = Pattern.compile("^[0-9]+\\.?[0-9]{0,2}$");

    /**
     * 验证手机号
     *
     * @param src
     * @return
     */
    public static boolean isMobile(String src) {
        if (StrUtil.isBlank(src)) {
            return false;
        }
        Matcher m = mobile_pattern.matcher(src);
        return m.matches();
    }


    /**
     * 验证枚举值是否合法 ,所有枚举需要继承此方法重写
     *
     * @param beanClass 枚举类
     * @param status    对应code
     * @return
     * @throws Exception
     */
    public static boolean isEnum(Class<?> beanClass, String status) throws Exception {
        if (StrUtil.isBlank(status)) {
            return false;
        }

        //转换枚举类
        Class<Enum> clazz = (Class<Enum>) beanClass;
        /**
         * 其实枚举是语法糖
         * 是封装好的多个Enum类的实列
         * 获取所有枚举实例
         */
        Enum[] enumConstants = clazz.getEnumConstants();

        //根据方法名获取方法
        Method getCode = clazz.getMethod("getCode");
        Method getDesc = clazz.getMethod("getDesc");
        for (Enum enums : enumConstants) {
            //得到枚举实例名
            String instance = enums.name();
            //执行枚举方法获得枚举实例对应的值
            String code = getCode.invoke(enums).toString();
            if (code.equals(status)) {
                return true;
            }
            String desc = getDesc.invoke(enums).toString();
            System.out.println(StrFormatter.format("实列{}---code:{}desc{}", instance, code, desc));
        }
        return false;
    }

    /**
     * 验证金额0.00
     *
     * @param money
     * @return
     */
    public static boolean isMoney(BigDecimal money) {
        if (StrUtil.isEmptyIfStr(money)) {
            return false;
        }
        if (!NumberUtil.isNumber(String.valueOf(money.doubleValue()))) {
            return false;
        }
        if (money.doubleValue() == 0) {
            return false;
        }
        Matcher m = money_pattern.matcher(String.valueOf(money.doubleValue()));
        return m.matches();
    }


    /**
     * 验证 日期
     *
     * @param date
     * @param dateFormat
     * @return
     */
    public static boolean isDateTime(String date, String dateFormat) {
        if (StrUtil.isBlank(date)) {
            return false;
        }
        try {
            DateUtil.parse(date, dateFormat);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

我自定义了补充了很多验证器,包括日期验证,枚举验证,手机号验证,金额验证
在这里插入图片描述
自定义校验注解使用起来和内置注解无异,在需要的字段上添加相应注解即可

6. Restfull层@Validated的使用

校验参数的时候,如何判断并返回失败的结果?一般有两种方式:

1. 全局异常捕获

@ControllerAdvice
@RestController
@Slf4j
public class GlobalExceptionHandler {
 
    /**
     * 非法参数验证异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(value = HttpStatus.OK)
    public ApiResult handleMethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        List<String> list = new ArrayList<>();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        for (FieldError fieldError : fieldErrors) {
            list.add(fieldError.getDefaultMessage());
        }
        Collections.sort(list);
        log.error("fieldErrors" + JSON.toJSONString(list));
        return ApiResult.fail(ApiCode.PARAMETER_EXCEPTION, list);
    }
}

2. 用 BindingResult 在实体类校验信息返回结果绑定
即使是全局异常捕获的方式,也能看到:校验信息是被封装在 BindingResult 对象里的,所以,我们也可以在 RestFull 层直接取。

(1) BindingResult用在实体类校验信息返回结果绑定;
(2) BindingResult.hasErrors()判断是否校验通过,bindingResult.getFieldError().getDefaultMessage()
获取在 TestEntity 的属性设置的自定义message,如果没有设置,则返回默认值 “javax.validation.constraints.XXX.message”。

可以看到,我上面的例子用的都是这种方法,我觉得这样更方便、直观,维护性更好。

  1. restful风格用法
    在多个参数校验,或者@RequestParam 形式时候,需要在controller上加注@Validated
 @GetMapping("/get")
    public RspDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId) {
        User user = userService.selectById(userId);
        if (user == null) {
            return new RspDTO<User>().nonAbsent("用户不存在");
        }
        return new RspDTO<User>().success(user);
    }


@RestController
@RequestMapping("user/")
@Validated
public class UserController extends AbstractController {
}

7、案例

7.1 初级约束注解:

  1. @NotNull(message = “用户id不能为空”)
1) 没有设置值时会返回
 (2)设置为userInfo.setUserId("");时不会返回
 (3)userInfo.setUserId(" ");时不会返回

2.@NotEmpty(message = “用户名不能为空”)

1)不会自动去掉字符串前后的空格再判断是否为空
 (2)没有设置值时会返回
 (3) userInfo.setUserName("");时返回
 (4)userInfo.setUserName(" ");时不返回

3.@NotBlank(message = “密码不能为空”)

1)自动去掉字符串前后的空格再判断是否为空
(2)没有设置值时会返回
(3)userInfo.setPassWord("");时返回
(4)userInfo.setPassWord(" ");时返回

4.@Length(min = 6,max = 20,message = “密码不能少于6位,也不能多于20位”)

1)可以直接不设置值
(2)即数量必须在620之间(包含620

5.@Email(message = “邮箱不正确”)

1)可以直接不设置值
(2)必须是正确的邮箱格式
  1. @Min(value = 18, message = “年龄不能小于18岁”)
1)可以直接不设置值
(2)即年龄必须大于或者等于18
  1. @Max(value = 60, message = “年龄不能大于60岁”)
1)可以直接不设置值
 (2)即年龄必须等于小于60

8.@Past(message = “生日只能是过去的时间或者现在的时间”)

1)可以直接不设置值
 (2)传来的时间只能是过去的时间或者现在的时间,不能是未来时间

9.@Size(min = 1,message = “不能少于一个好友”)

1)可以直接不设置值
 (2)集合里面的内容不能少于1

7.2. 中级约束注解:

1.@Valid

 private List<@Valid UserInfo> friends;表示对UserInfo类中里面的每个属性进行验证

2.@NotNull(message = “注册时邮箱不能为空”,groups = RegisterGroup.class)

 注册场景 public interface RegisterGroup{}
登录场景 public interface LoginGroup{}
 注册时邮箱不能为空,登录时可以为空
 set = validator.validate(userInfo,UserInfo.RegisterGroup.class);

3.组排序场景
@GroupSequence({
LoginGroup.class,
RegisterGroup.class,
Default.class
})
public interface Group{}

1)set = validator.validate(userInfo,UserInfo.Group.class);2)先验证 LoginGroup组的,都通过才验证下面的,不通过直接返回验证错误信息,验证顺序按照上面的顺序进行验证

7.3. 高级约束注解:

@Valid对方法输入参数进行约束注解校验

public void setUserInfo(@Valid UserInfo userInfo){ }
set1 = executableValidator.validateParameters(service,method1,paramObject);

@Valid对方法返回值进行约束校验

public @Valid UserInfo getUserInfo(){return new UserInfo();}
set1 = executableValidator.validateReturnValue(userInfoService,method,returnValue);

@Valid对构造函数输入参数进行校验

public UserInfoService(@Valid UserInfo userInfo){}
set1 = executableValidator.validateConstructorParameters(constructor,paramObject);

7.4. 上面初,中,高级注解示例

pom.xml

<!-- Validation 相关依赖 -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.16.Final</version>
        </dependency>
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>javax.el</artifactId>
            <version>2.2.6</version>
        </dependency>

待验证对象实体类UserInfo.java:

package com.mystudy.elastic.job.springboot.validation;

import org.hibernate.validator.constraints.Length;

import javax.validation.GroupSequence;
import javax.validation.Valid;
import javax.validation.constraints.*;
import javax.validation.groups.Default;
import java.util.Date;
import java.util.List;

/**
 * 待验证对象实体类
 * 用户信息类
 */
public class UserInfo {

    //登录场景
    public interface LoginGroup{}

    //注册场景
    public interface RegisterGroup{}

    //组排序场景
    @GroupSequence({
            LoginGroup.class,
            RegisterGroup.class,
            Default.class
    })
    public interface Group{}

    /**
     * 用户id
     */
    @NotNull(message = "用户id不能为空")
    private String userId;

    /**
     * 用户名
     */
    @NotEmpty(message = "用户名不能为空")
    private String userName;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空")
    @Length(min = 6, max = 20, message = "密码不能少于6位,也不能多于20位")
    private String passWord;

    /**
     * 邮箱
     */
//    @NotNull(message = "注册时邮箱不能为空",groups = RegisterGroup.class)
    @Email(message = "邮箱不正确")
    private String email;

    /**
     * 年龄
     */
    @Min(value = 18, message = "年龄不能小于18岁")
    @Max(value = 60, message = "年龄不能大于60岁")
    private Integer age;

    /**
     * 手机号
     */
    @Phone(message = "手机号不是158后面随便的手机号")
    private String phone;

    /**
     * 生日
     */
    @Past(message = "生日只能是过去的时间或者现在的时间")
    private Date birthday;

    /**
     * 好友列表
     */
    @Size(min = 1,message = "不能少于一个好友")
    private List<@Valid UserInfo> friends;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public List<UserInfo> getFriends() {
        return friends;
    }

    public void setFriends(List<UserInfo> friends) {
        this.friends = friends;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

用户信息服务类UserInfoService.java

package com.mystudy.elastic.job.springboot.validation;

import javax.validation.Valid;

/**
 * 用户信息服务类
 */
public class UserInfoService {
    /**
     *  userInfo作为输入参数
     * @param userInfo
     */
    public void setUserInfo(@Valid UserInfo userInfo){

    }

    /**
     * userInfo作为输出参数
     * @return
     */
    public @Valid UserInfo getUserInfo(){
        return new UserInfo();
    }

    /**
     * 默认构造函数
     */
    public UserInfoService(){

    }

    /**
     * 接收userInfo作为参数的构造函数
     * @param userInfo
     */
    public UserInfoService(@Valid UserInfo userInfo){

    }
}

验证测试类ValidationTest.java:

package com.mystudy.elastic.job.springboot.validation;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.executable.ExecutableValidator;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.Set;

/**
 * 验证测试类
 */
public class ValidationTest {

    //验证器对象
    private Validator validator;

    //待验证对象
    private UserInfo userInfo;

    //验证结果集合
    private Set<ConstraintViolation<UserInfo>> set;

    //验证结果集合
    private Set<ConstraintViolation<UserInfoService>> set1;

    /**
     * 初始化操作
     */
    @Before
    public void init() {
        //初始化验证器
        validator = Validation.buildDefaultValidatorFactory().getValidator();
        //初始化待验证对象 用户信息
        userInfo = new UserInfo();
        userInfo.setUserId(" ");
        userInfo.setUserName("  ");
        userInfo.setPassWord("  ");
        userInfo.setPassWord("333333");
        userInfo.setAge(18);
        userInfo.setBirthday(new Date());
        userInfo.setPhone("15987377373");

        UserInfo friend = new UserInfo();
        friend.setUserId("wangxiaoxi");
        friend.setUserName("王小喜");
        friend.setPassWord("wangxiaoxi");
        userInfo.setFriends(new ArrayList() {{
            add(friend);
        }});

    }

    /**
     * 结果打印
     */
    @After
    public void print() {
        set.forEach(item -> {
            //输出验证错误信息
            System.out.println(item.getMessage());
        });
//        set1.forEach(item -> {
//            //输出验证错误信息
//            System.out.println(item.getMessage());
//        });
    }

    @Test
    public void nullValidation() {
        //使用验证器对对象进行验证
        set = validator.validate(userInfo);
    }

    /**
     * 级联验证测试方法
     */
    @Test
    public void graphValidation() {
        set = validator.validate(userInfo);
    }

    /**
     * 验证注册时,邮箱是否为空
     */
    @Test
    public void groupValidation() {
        set = validator.validate(userInfo, UserInfo.RegisterGroup.class);
    }

    /**
     * 组排序
     */
    @Test
    public void groupSequenceValidate() {
        set = validator.validate(userInfo, UserInfo.Group.class);
    }

    /**
     * 对方法输入参数进行约束注解校验
     */
    @Test
    public void paramValidation() throws NoSuchMethodException {
        //获取校验执行器
        ExecutableValidator executableValidator = validator.forExecutables();

        //待验证对象
        UserInfoService service = new UserInfoService();
        //待验证方法
        Method method1 = service.getClass().getMethod("setUserInfo", UserInfo.class);
        //方法的输入参数
        Object[] paramObject = new Object[]{new UserInfo()};

        //对方法的输入参数进行校验
        set1 = executableValidator.validateParameters(service, method1, paramObject);

    }

    /**
     * 对方法返回值进行约束校验
     */
    @Test
    public void returnValueValidation() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //获取校验执行器
        ExecutableValidator executableValidator = validator.forExecutables();

        //构造要验证的方法对象
        UserInfoService userInfoService = new UserInfoService();
        Method method = userInfoService.getClass().getMethod("getUserInfo");

        //调用方法得到返回值
        Object returnValue = method.invoke(userInfoService);

        //校验方法返回值是否符合约束
        set1 = executableValidator.validateReturnValue(userInfoService, method, returnValue);
    }

    /**
     * 对构造函数输入参数进行校验
     */
    @Test
    public void constructorValidation() throws NoSuchMethodException {

        //获取验证执行器
        ExecutableValidator executableValidator = validator.forExecutables();

        //获取构造函数
        Constructor<UserInfoService> constructor = UserInfoService.class.getConstructor(UserInfo.class);
        Object[] paramObject = new Object[]{new UserInfo()};

        //校验构造函数
        set1 = executableValidator.validateConstructorParameters(constructor, paramObject);

    }
}

鸣谢: https://blog.csdn.net/weixin_44259720/article/details/127965610
https://zhuanlan.zhihu.com/p/387776766
原文链接:https://blog.csdn.net/weixin_45703155/article/details/130001434

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值