四时宝库

程序员的知识宝库

还在为临时查找SpringBoot参数校验烦恼吗?收下这一篇就够了

1、引言

我们在对外提供接口时,不管是从实际的业务需求,还是系统的安全性出发,通常都需要对接口的入参做一些合法性验证。

这些验证通常包括非空、字符串长度、数组大小,格式等等,在以前,我们都是通过手动实现类似的代码,于是,代码中充斥着很多if、else、StringUtils.isEmpty、CollectionUtils.isEmpty...类似的代码。

通过手动的方式来实现这些校验逻辑,它确实存在一定的优点,比如:灵活,无论什么样的校验逻辑,都可以直接翻译成代码实现;但同时,它也存在很多缺点:

  • 编码工作量大,手动实现方式太繁琐;
  • 容易出错;新写的代码越多,越容易出bug,越需要花时间去验证其正确性;

其实,Java生态提供了一套标准JSR-380,它已成为“对象验证”事实上的标准。基于这套标准,我们只需要使用@NotNull、@NotEmpty、@Size、@Range、@Pattern类似的注解,标注需要校验的参数,就可以完成对参数的校验。

Hibernate Validator实现了上述的标准;同时,SpringBoot也无缝集成了Hibernate Validator、自定义验证器、自动验证的功能。让我们在进行参数验证的时候,更加方便,高效。

下文我们将从以下三个方面来介绍参数校验:

  • 如何使用Hibernate Validator来校验参数;
  • SpringBoot集成Hibernate Validator;
  • 参数校验的高阶方式,如:如何自定义验证器、验证分组等等;


2、Hibernate Validator

2.1 如何使用Hibernate Validator来校验参数

(1)添加Maven依赖

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.5.Final</version>
</dependency>

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.0</version>
</dependency>

(2)定义实体类,并对需要校验的属性添加相应的注解

public class User {

    @NotBlank(message = "用户名称不能为空")
    private String name;

    @Email(message = "用户邮箱格式不合法")
    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

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

(3)校验实体的合法性

public class Main {

    public static void main(String[] args) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        User user = new User();
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        if(violations.size() > 0){
            violations.forEach(System.out::println);
        }
    }
}

2.2 JSR-380支持的注解

JSR-380支持的注解汇总如下

2.2.1 空 or 非空

注解

适用对象

null是否验证通过

说明

@NotNull

所有对象

No

不是null

@NotEmpty

CharSequence, Collection, Map, Array

No

不是null、不是""、size>0

@NotBlank

CharSequence

No

不是null、trim后长度大于0

@Null

所有对象

Yes

是null

2.2.2 布尔值

注解

适用对象

null是否验证通过

说明

@AssertTrue

boolean

Yes

必须是true

@AssertFalse

boolean

Yes

必须是false

2.2.3 长度

注解

适用对象

null是否验证通过

说明

@Size(min=0, max=Integer.MAX_VALUE)

CharSequence, Collection, Map, Array

Yes

字符串长度、集合size

2.2.4 数值大小

注解

适用对象

null是否验证通过

说明

@Positive

BigDecimal, BigInteger, byte, short, int, long, float, double

Yes

数字>0

@PositiveOrZero

BigDecimal, BigInteger, byte, short, int, long, float, double

Yes

数字>=0

@Negative

BigDecimal, BigInteger, byte, short, int, long, float, double

Yes

数字<0

@NegativeOrZero

BigDecimal, BigInteger, byte, short, int, long, float, double

Yes

数字<=0

@Min(value=0L)


BigDecimal, BigInteger, byte, short, int, long

Yes

数字>=min.value

@Max(value=0L)

BigDecimal, BigInteger, byte, short, int, long

Yes

数字<=max.value

@Range(min=0L, max=Long.MAX_VALUE)

BigDecimal, BigInteger, byte, short, int, long

Yes

range.min<=数字<=range.max

@DecimalMin(value="")

BigDecimal, BigInteger, CharSequence, byte, short, int, long

Yes

数字>=decimalMin.value

@DecimalMax(value="")

BigDecimal, BigInteger, CharSequence, byte, short, int, long

Yes

数字<=decimalMax.value

2.2.5 日期、时间

注解

适用对象

null是否验证通过

说明

@Past


java.util.Date

java.util.Calendar

java.time.Instant

java.time.LocalDate

java.time.LocalDateTime

java.time.LocalTime

java.time.MonthDay

java.time.OffsetDateTime

java.time.OffsetTime

java.time.Year

java.time.YearMonth

java.time.ZonedDateTime

java.time.chrono.HijrahDate

java.time.chrono.JapaneseDate

java.time.chrono.MinguoDate

java.time.chrono.ThaiBuddhistDate

Yes

时间在当前时间之前

@PastOrPresent

同上

Yes

时间在当前时间之前 或者 等于此时

@Future

同上

Yes

时间在当前时间之后

@FutureOrPresent

同上

Yes

时间在当前时间之后 或者 等于此时

2.2.6 格式

注解

适用对象

null是否验证通过

说明

@Pattern(regexp="", flags={})

CharSequence

Yes

匹配正则表达式

@Email @Email(regexp=".*", flags={})

CharSequence

Yes

匹配邮箱格式

@Digts(integer=0, fraction=0)

BigDecimal, BigInteger, CharSequence, byte, short, int, long

Yes

必须是数字类型,且满足整数位数<=digits.integer, 浮点位数<=digits.fraction


3、SpringBoot集成Hibernate Validator

3.1 准备工作

(1)首先,我们需要创建一个SpringBoot项目,这个不是本文的重点,大家自行创建即可。

(2)接下来,添加相应的Maven依赖:

<!-- springboot集成validation的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

(3)定义全局异常处理器:

使用@ControllerAdvice、@ExceptionHandler来对验证框架抛出的异常进行统一处理,并将错误信息拼接后统一返回,具体处理代码如下:

@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {

    /**
     * 符号常量
     */
    private final String SPLITOR = ", ";
    private final String SEPARATOR = ": ";

    /**
     * RequestBody、Form对象参数 验证不通过时抛出的异常
     * 需要在对应的参数上加 @Validated 注解
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Response handleValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
        // 从异常对象中拿到ObjectError对象
       String errorMsg = this.convertFiledErrors(e.getBindingResult().getFieldErrors());
        log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), errorMsg);
        // 然后提取错误提示信息进行返回
        return Response.fail(400, errorMsg);
    }

    /**
     * 简单参数 验证不通过时抛出的异常
     * 需要在 Controller 上加 @Validated 注解
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    public Response handleConstraintViolationException(HttpServletRequest request, ConstraintViolationException e) {
        // 从异常对象中拿到ObjectError对象
        String errorMsg = this.convertConstraintViolations(e);
        log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), errorMsg);
        return Response.fail(400, errorMsg);
    }

    @ExceptionHandler(value = BindException.class)
    public Response handleBindException(HttpServletRequest request, BindException e) {
        String errorMsg = convertFiledErrors(e.getFieldErrors());
        log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), errorMsg);
        return Response.fail(400, errorMsg);
    }

    /**
     * 入参反序列化为具体入参时产生的异常
     *
     * @param request
     * @param e
     * @return
     */
    @ExceptionHandler(value = HttpMessageNotReadableException.class)
    public Response handleBindException(HttpServletRequest request, HttpMessageNotReadableException e) {
        log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), "入参解析失败");
        return Response.fail(400, "入参解析失败");
    }

    /**
     * 全局默认异常处理
     *
     * @param request
     * @param t
     * @return
     */
    @ExceptionHandler(value = Throwable.class)
    public Response handleException(HttpServletRequest request, Throwable t) {
        log.error("{} - Exception!", request.getServletPath(), t);
        return Response.fail(500, "未知错误");
    }

    /**
     * 转换FieldError列表为错误提示信息
     *
     * @param fieldErrors
     * @return
     */
    private String convertFiledErrors(List<FieldError> fieldErrors) {
        return Optional.ofNullable(fieldErrors)
                .map(fieldErrorsInner -> fieldErrorsInner.stream()
                        .flatMap(fieldError -> Stream.of(fieldError.getField(), SEPARATOR, fieldError.getDefaultMessage(), SPLITOR))
                        .collect(Collectors.joining()))
                .map(msg -> msg.substring(0, msg.length() - SPLITOR.length()))
                .orElse(null);
    }

    /**
     * 转换ConstraintViolationException异常为错误提示信息
     *
     * @param e
     * @return
     */
    private String convertConstraintViolations(ConstraintViolationException e) {
        return Optional.ofNullable(e.getConstraintViolations())
                .map(violations -> violations.stream()
                        .flatMap(constraintViolation -> {
                            String path = constraintViolation.getPropertyPath().toString();
                            String errMsg = constraintViolation.getMessage();
                            return Stream.of(path, SEPARATOR, errMsg, SPLITOR);
                        }).collect(Collectors.joining())
                ).map(msg -> msg.substring(0, msg.length() - SPLITOR.length()))
                .orElse("");

    }
}

3.2 验证简单参数

@RestController
@Validated
public class ValidationController {

    /**
     * 对于不是封装类型的参数,需要在当前Controller上增加@Validated注解
     *
     * @param code
     * @return
     */
    @GetMapping("/valid-simple-param")
    public Response valid(@RequestParam(value = "code", required = false)
                          @NotNull(message = "code不能为null")
                          @NotBlank(message = "code不能为blank")
                          @Size(min = 1, max = 6, message = "code的长度必须是1-6个字符") String code) {
        return Response.success("World: " + code);
    }

}

(1)使用JSR-380提供的注解标注对应的简单参数;(后面介绍的自定义验证注解也可以用在这里)

  • @NotNull(message = "code不能为null"):表示code不能为null【@NotBlank其实已经包含了这一约束】;
  • @NotBlank(message = "code不能为blank"):表示code不能为null、""、" ";
  • @Size(min = 1, max = 6, message = "code的长度必须是1-6个字符"):表示code的长度必须是1-6个字符;

(2)当前Controller需要使用@Validated注解标注;

(3)校验不通过时,会抛出ConstraintViolationException;

3.3 验证RequestBody、Form对象参数

接口定义:

@RestController
public class ValidationController {

    @PostMapping("/valid-entity-param")
    public Response valid(@RequestBody @Validated ValidRequest request) {
        return Response.success();
    }
    
}
  • 入参需要使用@Validated注解标注;
  • 校验不通过时,会抛出MethodArgumentNotValidException异常;

入参实体定义:

这里列举了一些校验的案例,也添加了相应的注释,可以结合注释理解。

对于一些需要特殊说明的案例,下文会具体展开讲解。

@Data
public class ValidRequest {

    @NotNull(message = "数字param1不能为null")
    private Long param1;

    /**
     * @NotNull      字符串 不能为 null
     * @NotEmpty     字符串 不能为 空串,如:""
     * @NotBlank     字符串去除前后的空格 不能为 空串,如:""、"  "
     */
    @NotNull(message = "字符串param2不能为null")
    @NotEmpty(message = "字符串param2不能为empty")
    @NotBlank(message = "字符串param2不能为blank")
    private String param2;

    @NotNull(message = "字符串param3不能为空")
    @Size(min = 6, max = 11, message = "字符串param3长度必须是6-11个字符")
    private String param3;

    /**
     * @Pattern 只能用来校验String类型
     */
    @NotNull(message = "type不能为null")
    @Pattern(regexp = "[1|2|3]", message = "未知的类型")
    private String type;

    /**
     * 手机号,8位 or 11位
     */
    @NotBlank(message = "手机号必填")
    @Pattern(regexp = "^\\d{8,11}#34;, message = "手机号不合法")
    private String phone;

    /**
     * @Email 注解 只会在 email != null 时生效;<br/>
     * 如果email不能为null,则需要加上 @NotNull 注解 <br/>
     *
     * @Email 注解中regexp属性,支持自定义email的正则表达式,regexp 表达式只能比默认的正则更严格;
     * 参考代码:EmailValidator
     *
     */
    @NotNull(message = "邮箱不能为空")
    @Email(regexp = "^(\\w)+(\\.\\w+)*@163.com#34;, message = "邮箱不合法")
    // 该正则表达式无效
    // @Email(regexp = "A|B|C|D", message = "邮箱不合法")
    private String email;

    /**
     * yyyy-MM-dd
     */
    @NotNull
    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}#34;, message = "生日不合法")
    private String birthDateStr;

    @Valid
    private SubModel subModel;

    @Valid
    @NotEmpty
    private List<SubModel> subModels;

    @NotEmpty
    private List<String> codes;
}

特殊说明:

(1)@NotNull、@NotEmpty、@NotBlank

@NotNull(message = "数字param1不能为null")
private Long param1;

/**
 * @NotNull      字符串 不能为 null
 * @NotEmpty     字符串 不能为 空串,如:""
 * @NotBlank     字符串去除前后的空格 不能为 空串,如:""、"  "
 */
@NotNull(message = "字符串param2不能为null")
@NotEmpty(message = "字符串param2不能为empty")
@NotBlank(message = "字符串param2不能为blank")
private String param2;

@NotEmpty
private List<String> codes;
  • 前面2.2.1节已经说明,@NotEmpty、@NotBlank标注的属性,如果字段为null,也是会校验不通过的,因此,不需要再使用@NotNull注解标注。

1)对于一个注解,null是否验证通过,可查看2.2节中的表格

2)后面,对于自定义的注解,null是否验证通过,可以根据实际需求来自行实现。

  • @NotEmpty还可用于标注Collection、Map、Array,表示集合不能为null、不能为空集合。

(2)@Pattern

/**
 * @Pattern 只能用来校验String类型
 */
@NotNull(message = "type不能为null")
@Pattern(regexp = "[1|2|3]", message = "未知的类型")
private String type;

/**
 * 手机号,8位 or 11位
 */
@NotBlank(message = "手机号必填")
@Pattern(regexp = "^\\d{8,11}#34;, message = "手机号不合法")
private String phone;

/**
 * yyyy-MM-dd
 */
@NotNull
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}#34;, message = "生日不合法")
private String birthDateStr;
  • regexp属性用来指定字段需要满足的正则表达式;
  • @Pattern 注解只能用来标注CharSequence类型的对象【参考 PatternValidator 类】;

如果标注了其他类型的参数,则会抛出如下异常:

这里分享一篇关于正则表达式的干货:

常用正则表达式合集,这一篇就够了!!(含完整案例,建议收藏)_冰 河的博客-CSDN博客_常用正则表达式汇总

(3)@Email

/**
 * @Email 注解 只会在 email != null 时生效;<br/>
 * 如果email不能为null,则需要加上 @NotNull 注解 <br/>
 *
 * @Email 注解中regexp属性,支持自定义email的正则表达式,regexp 表达式只能比默认的正则更严格;
 * 参考代码:EmailValidator
 *
 */
@NotNull(message = "邮箱不能为空")
@Email(regexp = "^(\\w)+(\\.\\w+)*@163.com#34;, message = "邮箱不合法")
// 该正则表达式无效
// @Email(regexp = "A|B|C|D", message = "邮箱不合法")
private String email;
  • @Email 注解只会在 email != null 时生效,如果email不能为null,则需要加上 @NotNull 注解;
  • @Email 注解中的regexp属性,支持自定义email的正则表达式,用于定制化的email格式;
  • @Email 注解在进行验证时,会先进行一个基本的email格式验证,验证合格的话,才会使用自定义的正则验证。这一点,可从EmailValidator的源码出看到:

(4)@Valid

@Valid
private SubModel subModel;

@Valid
@NotEmpty
private List<SubModel> subModels;

@Data
public class SubModel {

    @NotBlank(message = "subParam1必填")
    @Size(min = 1, max = 8, message = "subParam1在1-8之间")
    private String subParam1;

    @Positive(message = "subParam2必须为正数")
    private Integer subParam2;

}


4、参数校验的高阶方式

4.1 自定义校验注解

当现有的校验注解无法满足我们的业务需求时,我们可以自定义校验注解。

自定义校验注解有两种方式:

(1)将原有的多个校验注解组合成为一个新的校验注解,这样可以继承多个注解的能力;

(2)完全创建一个新的校验注解,来实现自定义的业务校验功能。

4.1.1 自定义组合注解

定义一个校验注解,用于校验手机号的合法性。

@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Repeatable(PhoneNumber.List.class)
@Pattern(regexp = "^\\d{8,11}#34;, message = "手机号格式不正确")
@ReportAsSingleViolation
public @interface PhoneNumber {

    String message() default "";

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

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

    /**
     * 语法糖,PhoneNumber 注解可以在某个元素上使用多次
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        PhoneNumber[] value();
    }
}

(1)自定义组合校验注解,需要使用@Constraint(validatedBy = {})标注;【注意:validatedBy必须设置为{}】

  • (2)该功能可以基于@Pattern注解的功能做扩展;
  • 使用了@Pattern(regexp = "^\\d{8,11}#34;, message = "手机号格式不正确") 修饰当前注解;

(3)@ReportAsSingleViolation

默认情况下,组合注解中的一个或多个子注解校验失败的情况下,会分别触发子注解各自错误报告,如果想要使用组合注解中定义的错误信息,则添加该注解。添加之后只要组合注解中有至少一个子注解校验失败,则会生成组合注解中定义的错误报告,子注解的错误信息被忽略。

JSR-380中定义的 @Range 注解其实就是一种自定义组合注解,有兴趣可以看下它的源码。

@Range 注解 组合了 @Min 和 @Max 注解的功能

@Documented
@Constraint(validatedBy = { })
@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Min(0)
@Max(Long.MAX_VALUE)
@ReportAsSingleViolation
public @interface Range {
   @OverridesAttribute(constraint = Min.class, name = "value") long min() default 0;

   @OverridesAttribute(constraint = Max.class, name = "value") long max() default Long.MAX_VALUE;

   String message() default "{org.hibernate.validator.constraints.Range.message}";

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

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

   /**
    * Defines several {@code @Range} annotations on the same element.
    */
   @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
   @Retention(RUNTIME)
   @Documented
   public @interface List {
      Range[] value();
   }
}

4.1.2 完全创建新的校验注解

完全创建新的校验注解,一般需要分如下几步:

(1)定义相应的注解,用于标注需要校验的参数;

(2)实现验证器,即具体的校验逻辑;

(3)在定义的注解上,使用元注解@Constraint(validatedBy = {})来关联具体的验证器实现;

下面我们来看一个具体的案例。

需求:文件的类型只能为三种类型:图片(1)、视频(2)、PDF(3)。

@Data
public class File {

    private String fileId;

    // 文件的类型只能为三种类型:图片(1)、视频(2)、PDF(3)
    @DiscreteValue(values = {1, 2, 3}, message = "不支持的类型")
    private int type;

}

定义注解类:

@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {DiscreteValueValidator.class})
public @interface DiscreteValue {

    // values 用于指定 字段 可以选择的值
    int[] values() default {};

    String message() default "未知的值";

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

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

}
  • @Documented、@Target、@Retention 这三个元注解不做过多解释
  • @Constraint(validatedBy = {DiscreteValueValidator.class}):用于指定该注解对应的验证处理器注意:validatedBy是个数组,支持设置多个验证处理器;
  • values 属性: 用于指定 字段 可以选择的值;
  • message 属性:用于指定校验不通过时展示的消息;
  • groups 属性:用于指定验证器分组,下文会讲到;
  • payload 属性:具体作用暂不清楚,等后面再研究,但定义注解时,一般都需要定于该字段;

定义验证处理器:

public class DiscreteValueValidator implements ConstraintValidator<DiscreteValue, Integer> {

    private Set<Integer> valueSet;

    /**
     * 用于初始化验证处理器,这里将@DiscreteValue中指定的values赋值到valueSet中
     *
     * @param constraintAnnotation
     */
    @Override
    public void initialize(DiscreteValue constraintAnnotation) {
        this.valueSet = new HashSet<>();
        for (int value : constraintAnnotation.values()) {
            valueSet.add(value);
        }
    }

    /**
     * 真正的验证逻辑,验证valueSet中是否包含方法传入的value
     *
     * @param value
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return valueSet.contains(value);
    }
    
}

附录中会给出一些自定义Validator的示例。


4.2 验证指定分组

实际开发中,我们肯定给前端同学提供过“创建XXX”和“更新XXX”的接口。

  • 对于创建接口,实体中的id属性是必须为null的;
  • 对于更新接口,实体中的id属性是必须传的,即不能为null的;

定义待验证的实体类:

@Data
public class GroupValidRequest {

    @Null(message = "创建时id必须为null", groups = Create.class)
    @NotNull(message = "更新时id必传", groups = Update.class)
    private Integer id;

    @NotNull(message = "字符串param3不能为空")
    @Size(min = 6, max = 11, message = "字符串param3长度必须是6-11个字符")
    private String name;


    /**
     * 创建时的分组
     */
    public interface Create {

    }

    /**
     * 更新时的分组
     */
    public interface Update {

    }
}

分组必须定义成接口

定义http接口:

@RestController
public class ValidationController {

    /**
     * 创建的请求
     *
     * @param request
     * @return
     */
    @PostMapping("/valid-group-request-create")
    public Response validCreate(@RequestBody @Validated(GroupValidRequest.Create.class) GroupValidRequest request) {
        return Response.success();
    }

    /**
     * 更新的请求
     *
     * @param request
     * @return
     */
    @PostMapping("/valid-group-request-update")
    public Response validUpdate(@RequestBody @Validated(GroupValidRequest.Update.class) GroupValidRequest request) {
        return Response.success();
    }

}


附录

更多自定义校验注解示例

待校验的实体类

@Data
public class CustomValidRequest {

    @DateFormat
    private String birthday;

    @DateTimeFormat
    private String createdAt;

    @PhoneNumber
    private String phone;

    @DiscreteValue(values = {1, 2, 3}, message = "不支持的类型")
    private int type;

    @EnumValue(enumClass = TaskTypeEnum.class, message = "任务类型不合法")
    private String taskType;

    @EnumValue(enumClass = TaskStatusEnum.class, message = "任务状态值不合法")
    private Integer taskStatus;

    @Ipv4()
    private String ip;

}

@DateFormat、@DateTimeFormat

用于校验时间格式

@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = DateFormatValidator.class)
public @interface DateFormat {

    /**
     * 校验不通过时展示的消息
     *
     * @return
     */
    String message() default "日期格式不合法";

    /**
     * 按这个格式校验日期字符串
     *
     * @return
     */
    String format() default "yyyy-MM-dd";

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

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

}

@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@DateFormat
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface DateTimeFormat {

    /**
     * 校验不通过时展示的消息
     * 使用@OverridesAttribute注解 标识 该属性继承DateFormat中的message属性
     *
     * @return
     */
    @OverridesAttribute(constraint = DateFormat.class, name = "message")
    String message() default "日期时间格式不合法";

    /**
     * 按这个格式校验日期时间字符串
     * 使用@OverridesAttribute注解 标识 该属性继承DateFormat中的format属性
     *
     * @return
     */
    @OverridesAttribute(constraint = DateFormat.class, name = "format")
    String format() default "yyyy-MM-dd HH:mm:ss";

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

    Class<? extends Payload>[] payload() default {};
}
public class DateFormatValidator implements ConstraintValidator<DateFormat, String> {

    // 格式
    private String format;

    @Override
    public void initialize(DateFormat constraintAnnotation) {
        this.format = constraintAnnotation.format();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (!StringUtils.hasText(value)) {
            return true;
        }

        try {
            DateTimeFormatter.ofPattern(this.format).parse(value);
            return true;
        } catch (Throwable t) {
            return false;
        }
    }

}

@EnumValue

用于校验枚举值,验证的枚举类型需要实现BaseEnum接口。

public interface BaseEnum<T> {

    T getCode();

    String getMessage();
    
}
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {

    Class<? extends BaseEnum> enumClass();

    String message() default "未知的值";

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

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

}
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

    private Class<? extends BaseEnum> enumClass;

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        Class<? extends BaseEnum> enumClass = constraintAnnotation.enumClass();
        if (!enumClass.isEnum()) {
            throw new IllegalArgumentException("enumClass必须为枚举类型");
        }

        this.enumClass = enumClass;
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (null == value) {
            return true;
        }

        BaseEnum[] enumValues = enumClass.getEnumConstants();
        return Stream.of(enumValues).filter(e -> e.isEquals(value)).findFirst().map(v -> true).orElse(false);
    }

}

@Ipv4

@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {Ipv4Validator.class})
@Repeatable(Ipv4.List.class)
public @interface Ipv4 {

    String message() default "IP地址格式不正确";

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

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

    @Documented
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @interface List {
        Ipv4[] value();
    }

}
public class Ipv4Validator implements ConstraintValidator<Ipv4, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isEmpty(value)) {
            return true;
        }

        String[] part = value.split("\\.");
        if(part.length != 4) {
            return false;
        }

        // 避免 172.001.020.2 的情况
        for(int i = 0 ; i < 4 ; i ++){
            if (!StringUtils.isNumeric(part[i])
                    || Integer.parseInt(part[i], 10) > 255
                    || (StringUtils.startsWith(part[i], "0") && !StringUtils.equals(part[i], "0"))) {
                return false;
            }
        }

        return true;
    }
}


单独使用Hibernate Validator

SpringBoot集成Hibernate Validator的方式,可以帮助我们对HTTP接口的入参进行验证。

然而,有时我们需要校验参数的场景不一定都是HTTP接口,因此,就需要直接使用Hibernate Validator来进行参数验证,这里提供了一个ValidateUtil类,可以帮忙我们手动触发参数的校验。

public class ValidateUtil {

    /**
     * 快速失败的validator
     */
    private static final Validator VALIDATOR_FAIL_FAST = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .buildValidatorFactory()
            .getValidator();

    /**
     * 校验所有属性的validator
     */
    private static final Validator VALIDATOR_ALL = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(false)
            .buildValidatorFactory()
            .getValidator();


    /**
     * 获取校验结果
     *
     * @param object
     * @param groups
     * @param <T>
     * @return
     */
    public static <T> Set<ConstraintViolation<T>> getValidatorFastResult(T object, Class<?>... groups) {
        return VALIDATOR_FAIL_FAST.validate(object, groups);
    }

    public static <T> void validateFailFast(T object, Class<?>... groups) {
        Set<ConstraintViolation<T>> validateResult = getValidatorFastResult(object, groups);
        if (validateResult.size() <= 0) {
            return;
        }

        throw new ValidationException(buildValidateMsg(validateResult));
    }

    public static <T> void validateFailFast(Collection<T> objects, Class<?>... groups) {
        for (T object : objects) {
            validateFailFast(object, groups);
        }
    }

    /**
     * 获取校验结果
     *
     * @param object
     * @param groups
     * @param <T>
     * @return
     */
    public static <T> Set<ConstraintViolation<T>> getValidatorAllResult(T object, Class<?>... groups) {
        return VALIDATOR_ALL.validate(object, groups);
    }

    public static <T> void validateAll(T object, Class<?>... groups) {
        Set<ConstraintViolation<T>> validateResult = getValidatorAllResult(object, groups);
        if (validateResult.size() <= 0) {
            return;
        }

        throw new ValidationException(buildValidateMsg(validateResult));
    }

    public static <T> void validateAll(Collection<T> objects, Class<?>... groups) {
        for (T object : objects) {
            validateAll(object, groups);
        }
    }

    /**
     * 构造校验的错误消息
     *
     * @param validateResult
     * @param <T>
     * @return
     */
    private static <T> String buildValidateMsg(Set<ConstraintViolation<T>> validateResult) {
        StringBuilder validateMsg = new StringBuilder();
        for (ConstraintViolation<T> violation : validateResult) {
            validateMsg.append(violation.getMessage()).append(";\n");
        }

        return validateMsg.toString();
    }

}


参考文档

https://github.com/marqueeluo/spring-boot-validation-demo

Spring基础系列-参数校验:

https://www.cnblogs.com/V1haoge/p/9953744.html

多字段联合逻辑校验:

分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题【享学Spring MVC】_YourBatman的博客-CSDN博客_groupsequence

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接