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