bigywater.tech
Java · 373

Bean Validation:把约束声明在模型边界

Bean Validation 一行注解就能表达约束,但 null、空串、级联、分组与消息模板各有细节。这篇用 Hibernate Validator 逐项实测,也包括自定义消息模板里的注入风险与验证的成本。

null 合法
@Size、@Pattern、@Email、@Min、@Past、@Positive 对 null 都视为合法,要拒绝 null 需要 @NotNull
{key} 仍被插值
自定义约束的消息模板中,${...} 不求值,但 {jakarta.validation.constraints.NotNull.message} 这样的消息参数仍被替换
3.1 ms vs 约 2 µs
每次验证都新建 ValidatorFactory 与复用 Validator 的稳态单次验证耗时

约束对 null / 空串的判定

实测 · 本机执行输出
  [basic]
  全部为默认值(null 与 ok = true)的 User:
        id → “must not be null”(值 null)
        items → “must not be null”(值 null)
        name → “must not be blank”(值 null)
        tag → “must not be empty”(值 null)
  Plain 所有字段为 null:
        g → “must not be null”(值 null)
  Plain 给空字符串 / 越界值:
        a → “size must be between 3 and 2147483647”(值 “”)
        b → “must match "[a-z]+"”(值 “”)
        d → “must be greater than or equal to 5”(值 “3”)
        e → “must be a past date”(值 “2026-10-05”)
        f → “must be greater than 0”(值 “0”)
  合法值:(没有违规)
  @NotBlank 对 "  "、@NotEmpty 对 ""、@Size / @Min / @Email / @Pattern / @Past:
        age → “must be greater than or equal to 18”(值 “17”)
        born → “must be a past date”(值 “2027-10-04”)
        code → “size must be between 3 and 5”(值 “ab”)
        email → “must be a well-formed email address”(值 “not-an-email”)
        items → “size must be between 2 and 2147483647”(值 “[x]”)
        name → “must not be blank”(值 “  ”)
        slug → “must match "[a-z]+"”(值 “ABC”)
        tag → “must not be empty”(值 “”)
  ConstraintViolation 的各个字段(以 age 为例):propertyPath = age;message = must be greater than or equal to 18;messageTemplate = {jakarta.validation.constraints.Min.message};invalidValue = 17;rootBeanClass = User;leafBean 同一对象? true;约束注解 Min;attributes [value=18]
  违规集合的类型 HashSet(Set,不保证顺序;需要排序再展示)

级联、容器与循环引用

实测 · 本机执行输出
  [nested]
  @Valid 级联 + 容器元素约束(List<@NotBlank String>、Map<@NotBlank String, @Min(1) Integer>):
        address.city → “must not be blank”(值 null)
        address.zip → “size must be between 0 and 3”(值 “12345”)
        aliases[1].<list element> → “must not be blank”(值 “ ”)
        aliases[2].<list element> → “must not be blank”(值 “”)
        scores<K>[].<map key> → “must not be blank”(值 “”)
        scores[].<map value> → “must be greater than or equal to 1”(值 “0”)
        scores[m].<map value> → “must be greater than or equal to 1”(值 “0”)
  address 为 null 且标了 @Valid(没有 @NotNull):(没有违规)
  循环引用的对象图(c1 → c2 → c1,都有 @NotBlank name = null):
        name → “must not be blank”(值 null)
        next.name → “must not be blank”(值 null)
  Address.owner(User)带 @Valid,再往下:
        city → “must not be blank”(值 “”)
        owner.id → “must not be null”(值 null)
        owner.items → “must not be null”(值 null)
        owner.name → “must not be blank”(值 null)
        owner.tag → “must not be empty”(值 null)
  validateProperty(u, "name") 只验证一个属性:
        name → “must not be blank”(值 null)
  validateValue(User.class, "age", 10):
        age → “must be greater than or equal to 18”(值 “10”)
  validateProperty 属性名不存在:IllegalArgumentException: HV000227: The validated type Main$User does not specify the property: nope
  validate(null):IllegalArgumentException: HV000116: The object to be validated must not be null.
  直接 validate 一个 List(不是 bean):(没有违规)(列表本身没有约束,元素不会被验证)

分组与组序列

实测 · 本机执行输出
  [groups]
  默认组验证 Article(title / body 属于自定义组,author 在 Default 组):
        author → “must not be null”(值 null)
  Draft 组:
        title → “must not be blank”(值 null)
  Publish 组:
        body → “must not be blank”(值 null)
  Default + Draft + Publish:
        author → “must not be null”(值 null)
        body → “must not be blank”(值 null)
        title → “must not be blank”(值 null)
  @GroupSequence 在类上(Article2 自身先于 Pricey):a 为 null 时:
      
        a → “must not be blank”(值 null)
      (昂贵的检查执行了)
      a 合法之后:
        exp → “expensive check ran”(值 “false”)
  不存在的组(传 Object.class):ValidationException: HV000045: A group has to be an interface. java.lang.Object is not.

方法与构造器验证

实测 · 本机执行输出
  [method]
  方法参数 greet("", 0):
        greet.arg0 → “must not be blank”(值 “”)
        greet.arg1 → “must be greater than or equal to 1”(值 “0”)
  返回值 find("miss") 返回 null:
        find.<return value> → “must not be null”(值 null)
  返回值 list() 返回 [x](@Size(min = 2)):
        list.<return value> → “size must be between 2 and 2147483647”(值 “[x]”)
  构造器参数 new Service(null):
        Service.arg0 → “must not be null”(值 null)
  直接调用 s.greet("", 0)(没有拦截器,不会自动验证):(方法验证需要 AOP / CDI / Spring 拦截,单独的 Validator 不会拦截普通调用)
  参数个数不对:IllegalArgumentException: HV000181: Wrong number of parameters. Method or constructor class java.lang.String#greet(String, int) expects 2 parameters, but got 1.

自定义约束、消息模板与语言

实测 · 本机执行输出
  [custom]
  类级别约束 @Same(两次密码不一致):
        pw2 → “两次输入不一致”(值 “Main$Signup@647e447”)
  一致:(没有违规)
  消息模板里的表达式:自定义验证器用 buildConstraintViolationWithTemplate("用户输入:" + 用户文本),文本是 ${1+1}:
         → “用户输入:${1+1}”(值 “Main$Msg@359f7cdf”)
  同样,文本是 #{1+1}:
         → “用户输入:#{1+1}”(值 “Main$Msg@1fa268de”)
  文本是 ${validatedValue}(试探能否访问验证上下文):
         → “用户输入:${validatedValue}”(值 “Main$Msg@4f6ee6e4”)
  文本是 {jakarta.validation.constraints.NotNull.message}(消息键):
         → “用户输入:must not be null”(值 “Main$Msg@4466af20”)
  文本是 ${''.getClass().forName('java.lang.Runtime')}(尝试反射):
         → “用户输入:${''.getClass().forName('java.lang.Runtime')}”(值 “Main$Msg@a514af7”)
  注解里的 message 可以引用 {min} 与 ${validatedValue}:
        s → “至少 5 个字符,当前值 ab”(值 “ab”)
      en 下 @NotNull 的消息:must not be null
      de 下 @NotNull 的消息:darf nicht null sein
      fr 下 @NotNull 的消息:ne doit pas être nul
      zh 下 @NotNull 的消息:不得为 null
      ja 下 @NotNull 的消息:null は許可されていません

成本与线程安全

实测 · 本机执行输出
  [cost]
  JVM 里第一次 buildDefaultValidatorFactory 78 ms;同一 JVM 里第二次构建 1 ms;getValidator 0.65 ms;对 User 第一次 validate(含解析约束元数据)42 ms
  稳态(同样的字段,无 address 级联):合法的 User(0 个违规)1.81 µs / 次;3 个违规的 User(name 空白、age 5、email 不合法,要生成消息)2.16 µs / 次
  带 @Valid address 级联的合法 User:2.36 µs / 次
  反面做法:每次验证都新建 ValidatorFactory:平均 3.1 ms / 次(对比稳态的几 µs)
  failFast(true):全部违规的 User 返回 1 条违规(默认 4 条)
  8 个线程共用同一个 Validator 各验证 2 万次(合法 / 非法交替):结果不对 0 次

分步演示

Bean Validation
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64,JDK 21.0.12;Hibernate Validator 8.0.1.Final、jakarta.validation-api 3.0.2、Expressly 5.0.0(表达式)
    • 没有验证:Spring / Quarkus / Jakarta EE 容器里的方法验证拦截、@Valid 与 JPA 的集成(持久化前验证)、自定义 ExpressionLanguageFeatureLevel 配置打开 EL 后的行为、约束映射 XML(validation.xml)、Hibernate Validator 的 -parameters 参数名支持、Java 记录(record)与 Kotlin 的约束注解位置
    京ICP备2026067502号-1京公网安备11010502063487号