Java Bean Validation 详解(上)

简介: 前言最近维护一个老项目,项目使用最原始的Servlet,项目中充斥着各种类似判空的简单校验,为了减少重复代码,因此需要手动引入 Java 的 Bean Validation。

前言

最近维护一个老项目,项目使用最原始的Servlet,项目中充斥着各种类似判空的简单校验,为了减少重复代码,因此需要手动引入 Java 的 Bean Validation。


Java Bean Validation作为一个规范,更多的是定义一些标准化的接口,日常使用中我们常常引入HIbernate Validator实现。在不关心具体实现的情况下校验参数时经常使用的代码如下:

Validator validator = Validation.byDefaultProvider().configure().buildValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> violations = validator.validate(new Object());

那么不免会产生疑问,上述获取的Validator是单例吗?或者说ValidatorFactory有对Validator进行缓存吗?如果进行了缓存那么在高并发的情况下就可以减少对象的创建,提高应用的性能。


带着这个问题,我们就需要对Java Bean Validation 进行深入了解。网络上的文章大多是一些零碎的知识点及简单的使用,通过对 JSR-303 文档的阅读及Bean Validation源码的查看总结出本篇,以期解答上述问题,并深入了解 Bean Validation。


Bean Validation 背景

校验数据是贯穿整个应用程序的一项常见任务,从表示层到持久层每个层可能会实现相同的校验逻辑,这样就会非常耗时且容易出错。为了避免在每个层中重复这些校验,开发人员通常将校验逻辑直接绑定到实体类中,将实体类与校验代码(实际上是类本身的元数据)混杂在一起。


Bean Validation 作为一种规范,由 Hibernate Validator 的高级开发人员 Emmanuel Bernard 领导,目前 Bean Validation 规范已经出了三版,分别是 JSR-303、JSR-349 及 JSR-380。它的目标是为java程序开发人员提供一个类级别的约束定义和校验能力。


约束

约束是约束注解和约束校验实现列表的组合,用于定义并对值的限制进行校验。


约束注解

约束注解可以应用在类、方法、属性或别的约束注解。约束注解还可以用在别的元素类型,但是约束校验时不必处理,约束注解的javadoc最好对支持的类型进行说明。


约束注解的属性

message、groups、payload是约束注解属性的保留名称,属性名称也不能以valid开头,同时也可以具有其他的属性。


约束注解属性 message : 每个约束注解都必须定义一个类型为String的属性message,message的默认值建议作为resource bundule的键,以便支持国际化,推荐使用类名+注解的名称,如String message() default "{com.zzuhkp.constraint.MyConstraint.message}";


约束注解属性 groups : 每个约束注解都必须定义一个默认值为空数组的属性groups,groups指定注解所属的组,用于控制校验的顺序或部分校验,如果没有指定groups,则约束注解所属的组为default,如Class<?>[] groups() default {};


约束注解属性 payload : 每个约束注解都必须定义一个默认值为空数组的属性payload,payload指定约束注解所携带的附加元信息,payLoad的值可以被校验结果ConstraintViolation从ConstraintDescriptor中获取,如Class<? extends Payload>[] payload() default {};


示例:java内置的约束注解NotNull源码如下

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
  String message() default "{javax.validation.constraints.NotNull.message}";
  Class<?>[] groups() default { };
  Class<? extends Payload>[] payload() default { };
  /**
   * Defines several {@link NotNull} annotations on the same element.
   *
   * @see javax.validation.constraints.NotNull
   */
  @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
  @Retention(RUNTIME)
  @Documented
  @interface List {
    NotNull[] value();
  }
}

重复约束注解

相同的约束注解可以使用不用的group,每个group的错误消息可能不同,为了支持这种需求,Bean Validation 也支持常规注解(没有被@Constraint标注),常规注解只要包含一个名为value,类型为约束注解数组的属性即可。


在 jdk 8 之前,java尚不支持重复注解,因此通常在约束注解中定义一个内部的注解List,List的属性value的类型为约束注解的数组,如上面的@NotNull注解。


示例如下:

public class RequestDTO {
    @Pattern(regexp = "^[0-9]+$", message = "手机号码只能为数字")
    @Pattern(regexp = "^1[34578]\\d{9}$", groups = RequestDTO.class, message = "手机号码格式有误")
    private String phone;
}


组合注解

通过组合约束注解可以创建更高级的约束注解,组合注解即在约束注解上标注其他约束注解。使用方式如下:


组合注解上使用其他约束注解,默认情况返回各约束注解的错误信息

使用注解@ReportAsSingleViolation验证失败后将返回组合注解的message信息。

组合注解上使用注解@OverridesAttribute覆盖被直接标注的其他的注解的属性。

示例如下:

@NotNull    //① 约束注解可以注解在组合注解上,默认情况返回组合中各个约束注解的错误信息
@Size(min = 1, max = 5, message = "大小有误")   //①
@Pattern(regexp = "", message = "格式有误1")    //①
@Pattern(regexp = "", message = "格式有误2")    //①
@ReportAsSingleViolation    //② 添加该注解后,验证失败返回组合注解(@Composition)的message()信息
@Constraint(validatedBy = {})
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Composition {
    String message() default "组合注解错误提示";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    //③ 使用一个属性覆盖约束注解的多个属性
    @OverridesAttribute.List({
        @OverridesAttribute(constraint = Size.class, name = "min"),
        @OverridesAttribute(constraint = Size.class, name = "max")})
    int size() default 5;
    //④ 覆盖约束注解的属性(即标注@Composition的注解@Size的message属性)
    @OverridesAttribute(constraint = Size.class, name = "message")
    String sizeMessage() default "覆盖注解Size的错误提示";
    //⑤ 存在多个相同约束注解时,使用属性constraintIndex标识约束注解的索引位置
    @OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 0)
    String patternMessage1() default "覆盖第一个注解Pattern的错误提示";
    //⑤
    @OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 1)
    String patternMessage2() default "覆盖第二个注解Pattern的错误提示";
}

约束校验实现

约束校验实现用于对给定类型执行给定约束注解的校验,可由约束注解中的@Constraint属性validatedBy指定,约束注解实现需要实现接口ConstraintValidator。也可以通过实现ConstraintValidatorFactory获取ConstraintValidator。

ConstraintValidator源码如下:

/**
 * @param <A> 实现要处理的约束注解类型
 * @param <T> 实现支持的目标类型,必须是非参数化类型或无界的通配符类型
 */
public interface ConstraintValidator<A extends Annotation, T> {
  /**
   * #isValid调用之前进行调用,用于初始化
   * 
   * @param 约束注解的实例
   */
  default void initialize(A constraintAnnotation) {
  }
  /**
   * 校验逻辑的实现,需要考虑线程安全问题
   *
   * @param 要校验的对象值,注意不能修改值的状态
   * @param 约束校验上下文,可用于重新创建ConstraintViolation
   *
   * @return 是否通过校验
   */
  boolean isValid(T value, ConstraintValidatorContext context);
}


ConstraintValidator 校验时使用的 ConstraintValidatorContext 可用于禁用默认 ConstraintViolation ,创建新的 ConstraintViolation,ConstraintViolation 包含违反约束的的元信息,ConstraintValidatorContext 部分源码如下:

public interface ConstraintValidatorContext {
  /**
   * 禁用默认ConstraintViolation对象生成
   */
  void disableDefaultConstraintViolation();
  /**
   * @return 默认的消息模板
   */
  String getDefaultConstraintMessageTemplate();
  /**
   *
   * @since 2.0
   */
  ClockProvider getClockProvider();
  /**
   * 使用给定的信息模板构建ConstraintViolation
   */
  ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);
  /**
   * 返回给定类型的实例
   * @since 1.1
   */
  <T> T unwrap(Class<T> type);
}


约束目标

约束可以用于校验对象、字段、属性。


校验对象:约束可以用在接口或类上,对运行时具体的对象进行校验。

校验字段、属性:约束可以同时用在字段和属性,属性的read方法需要满足 JavaBean的规范(参见Java基础知识之JavaBean)。如果同时应用到字段和其对应的属性,则会重复校验。父类中的getter方法定义的约束会被子类重写的getter方法覆盖。

递归校验:如果要对字段或属性的对象值内部的字段或属性进行递归校验,可以在字段或属性read方法上添加注解@Valid。如果对象的类型为数组、Collection、Map,则会循环校验每一项值,对于Map仅校验value,不校验key。

内建的约束

java bean validation 内建了一些常用的约束,整理日常开发使用的约束如下。
image.png

自定义约束示例

1、定义约束

@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CustomValidator.class})
public @interface IsTrue {
    String message() default "参数不为真";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2、定义约束实现

public class CustomValidator implements ConstraintValidator<IsTrue, Boolean> {
    public CustomValidator(){
    }
    @Override
    public void initialize(IsTrue constraintAnnotation) {
    }
    @Override
    public boolean isValid(Boolean value, ConstraintValidatorContext context) {
        return value != null && value;
    }
}

3、测试用例

public class Bean {
    @IsTrue
    private Boolean real;
    public Bean(Boolean real) {
        this.real = real;
    }
}
public class BeanValidatorTest {
    public static void main(String[] args) {
        Bean bean = new Bean(false);
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Bean>> violationSet = validator.validate(bean);
        violationSet.forEach(System.out::println);
    }
}

4、打印结果如下:

ConstraintViolationImpl{interpolatedMessage='参数不为真', propertyPath=real, rootBeanClass=class com.zzuhkp.validator.Bean, messageTemplate='参数不为真'}

分组和分组序列

分组

分组由java接口定义,通过约束注解的属性groups可以指定约束所属的分组,如果没有指定则默认分组为javax.validation.groups.Default,校验时可以根据分组进行校验,仅校验指定的分组。

示例:

public class BeanValidatorTest {
    static class Bean {
        @NotNull(message = "约束属于默认的Default分组")
        private String name;
        @NotNull(groups = Default.class, message = "约束属于默认的Default分组")
        private Integer sex;
        @NotNull(groups = CustomGroup.class, message = "约束属于自定义分组CustomGroup")
        private Integer age;
    }
    /**
     * 自定义group
     */
    interface CustomGroup {
    }
    public static void main(String[] args) {
        Bean bean = new Bean();
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Bean>> violationSet = validator.validate(bean, CustomGroup.class);
    }
}

打印结果为:

ConstraintViolationImpl{interpolatedMessage='约束属于默认的Default分组', propertyPath=sex, rootBeanClass=class com.zzuhkp.validator.BeanValidatorTest$Bean, messageTemplate='约束属于默认的Default分组'}
ConstraintViolationImpl{interpolatedMessage='约束属于自定义分组CustomGroup', propertyPath=age, rootBeanClass=class com.zzuhkp.validator.BeanValidatorTest$Bean, messageTemplate='约束属于自定义分组CustomGroup'}
ConstraintViolationImpl{interpolatedMessage='约束属于SuperGroup分组', propertyPath=name, rootBeanClass=class com.zzuhkp.validator.BeanValidatorTest$Bean, messageTemplate='约束属于SuperGroup分组'}

分组序列

分组中的约束校验时是无序的。在有些情况下,可能需要先校验一个分组,校验不通过时再校验另一个分组,例如后面的校验依赖前面的校验或者后面的校验比较消耗资源。为了达到按照顺序校验的目的,可以在分组上添加注解@GroupSequence使分组成为一个序列,并通过value属性指定序列中包含的分组。这样前面的分组校验不通过则不会校验后面的分组,值得注意的是序列中的分组可能是一个序列或者继承其他的分组,这种情况也会按照序列定义顺序校验。

示例如下:

public class BeanValidatorTest {
    static class Bean {
        @NotNull(groups = CustomGroupA.class, message = "约束属于CustomGroupA分组")
        private String name;
        @NotNull(groups = CustomGroupB.class, message = "约束属于CustomGroupB分组")
        private Integer sex;
    }
    interface CustomGroupA {
    }
    interface CustomGroupB {
    }
    @GroupSequence({CustomGroupA.class, CustomGroupB.class})
    interface CustomSequence {
    }
    public static void main(String[] args) {
        Bean bean = new Bean();
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Bean>> violationSet = validator.validate(bean, CustomSequence.class);
        violationSet.forEach(System.out::println);
    }
}

打印结果如下:

ConstraintViolationImpl{interpolatedMessage='约束属于CustomGroupA分组', propertyPath=name, rootBeanClass=class com.zzuhkp.validator.BeanValidatorTest$Bean, messageTemplate='约束属于CustomGroupA分组'}


示例中定义了两个分组CustomGroupA和CustomGroupB,以及一个序列CustomSequence,序列包含了分组CustomGroupA和CustomGroupB。校验时前面分组CustomGroupA的校验未通过,CustomGroupB未进行校验。

目录
相关文章
|
6月前
|
搜索推荐 Java 开发者
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException 问题处理
【5月更文挑战第14天】org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException 问题处理
641 1
|
3月前
|
Java Spring 容器
Java SpringBoot 中,动态执行 bean 对象中的方法
Java SpringBoot 中,动态执行 bean 对象中的方法
41 0
|
3月前
|
Java Spring
Java SpringBoot Bean InitializingBean 项目初始化
Java SpringBoot Bean InitializingBean 项目初始化
55 0
|
4月前
|
存储 前端开发 Java
Java中的不同Bean作用域
【7月更文挑战第5天】
59 0
Java中的不同Bean作用域
|
5月前
|
Java 持续交付 Maven
Java报错:Missing ServletWebServerFactory bean,如何解决
Java开发中遇到`Missing ServletWebServerFactory bean`错误?该问题可能由依赖冲突、配置问题或环境不一致引起。解决方法包括:检查依赖版本一致性、修复配置错误、确保环境匹配,以及查看IDE中的JRE配置。预防这类问题,可采用版本管理工具、CI/CD流程、代码审查和社区学习。木头左提醒,记得点赞和分享,下次见!
Java报错:Missing ServletWebServerFactory bean,如何解决
|
5月前
|
Java API 数据处理
Java Bean参数验证:深入探索javax.validation.constraints注解
Java Bean参数验证:深入探索javax.validation.constraints注解
206 0
|
6月前
|
消息中间件 安全 Java
在Spring Bean中,如何通过Java配置类定义Bean?
【4月更文挑战第30天】在Spring Bean中,如何通过Java配置类定义Bean?
99 1
|
6月前
|
Java 测试技术 Spring
|
6月前
|
XML Java 程序员
作为Java程序员还不知道Spring中Bean创建过程和作用?
作为Java程序员还不知道Spring中Bean创建过程和作用?
46 0
|
6月前
|
XML 缓存 Java
Java常见bean 工具类性能对比
Java常见bean 工具类性能对比
99 0
下一篇
无影云桌面