问题背景
最近在写接口的时候,遇到了一个看似简单但实际很坑的参数校验问题。代码是这样的:
arduino
/**
* 关键词
*/
@NotBlank
private String keywords;
看起来很完美对吧?使用的是 javax.validation.constraints.NotBlank 注解来校验字符串不能为空。
结果一测试,直接给我来了个异常:
rust
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotBlank' validating type 'java.lang.String'. Check configuration for 'keywords'
异常分析
看到这个异常信息,第一反应是:什么鬼?@NotBlank 不就是用来校验 String 的吗?怎么还找不到验证器?
去网上搜了一圈,发现众说纷纭:
- 有人说 String 不能配置 @NotBlank(这明显是错的)
- 有人说缺少
validation-api依赖 - 有人说缺少
hibernate-validator依赖 - 还有人说版本不匹配
根本原因
检查项目依赖后发现了问题所在:
xml
<!-- 项目中的实际依赖 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.x</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1</version>
</dependency>
看出问题了吗?看到了心里打个扣
@NotNull能用:因为它是 Bean Validation 1.1 就有的注解,hibernate-validator 5.x 支持@NotBlank不能用:因为它是 Bean Validation 2.0 新增的注解,需要 hibernate-validator 6.x 才支持
虽然引入了 validation-api 2.0(有 @NotBlank 注解定义),但运行时用的是 hibernate-validator 5.4.1(没有 @NotBlank 的验证器实现),所以编译能通过,运行时却找不到验证器!
为什么会抛出异常?
我们直接来看看异常抛出的源码(hibernate-validator 5.x):
scss
private <T, V> ConstraintValidator<A, V> getConstraintValidatorNoUnwrapping(
ValidationContext<T> validationContext,
ValueContext<?, V> valueContext) {
// 确保没有设置 unwrapper
valueContext.setValidatedValueHandler(null);
Type validatedValueType = valueContext.getDeclaredTypeOfValidatedElement();
// 关键:从 ConstraintValidatorManager 中获取已初始化的验证器
ConstraintValidator<A, V> validator = validationContext
.getConstraintValidatorManager()
.getInitializedValidator(
validatedValueType, // String.class
descriptor, // @NotBlank 注解描述符
validationContext.getConstraintValidatorFactory()
);
// 如果找不到对应的验证器实现,抛出异常
if (validator == null) {
throwExceptionForNullValidator(validatedValueType,
valueContext.getPropertyPath().asString());
}
return validator;
}
流程解析:
- 获取被校验字段的类型:
String.class - 从
ConstraintValidatorManager中查找能处理@NotBlank + String组合的验证器 - 在 hibernate-validator 5.x 中找不到
NotBlankValidator,返回 null - 抛出
UnexpectedTypeException异常
为什么会返回 null?
继续追踪 getInitializedValidator 方法的实现:
ini
public <V, A extends Annotation> ConstraintValidator<A, V> getInitializedValidator(
Type validatedValueType,
ConstraintDescriptorImpl<A> descriptor,
ConstraintValidatorFactory constraintFactory) {
Contracts.assertNotNull(validatedValueType);
Contracts.assertNotNull(descriptor);
Contracts.assertNotNull(constraintFactory);
// 1. 首先尝试从缓存中获取
CacheKey key = new CacheKey(descriptor.getAnnotation(), validatedValueType, constraintFactory);
ConstraintValidator<A, V> constraintValidator =
(ConstraintValidator<A, V>) constraintValidatorCache.get(key);
// 2. 缓存中没有,创建并初始化验证器
if (constraintValidator == null) {
constraintValidator = createAndInitializeValidator(
constraintFactory, validatedValueType, descriptor);
constraintValidator = cacheValidator(key, constraintValidator);
} else {
LOG.tracef("Constraint validator %s found in cache.", constraintValidator);
}
// 3. 关键:如果是 DUMMY_CONSTRAINT_VALIDATOR,返回 null!
return DUMMY_CONSTRAINT_VALIDATOR == constraintValidator ? null : constraintValidator;
}
注意最后一行 :如果 constraintValidator 是 DUMMY_CONSTRAINT_VALIDATOR,就返回 null!
那么什么时候会创建 DUMMY_CONSTRAINT_VALIDATOR 呢?继续看 createAndInitializeValidator 方法:
ini
private <V, A extends Annotation> ConstraintValidator<A, V> createAndInitializeValidator(
ConstraintValidatorFactory constraintFactory,
Type validatedValueType,
ConstraintDescriptorImpl<A> descriptor) {
// 1. 核心:查找匹配的验证器类
Class<? extends ConstraintValidator<A, V>> validatorClass =
(Class<? extends ConstraintValidator<A, V>>)
findMatchingValidatorClass(descriptor, validatedValueType);
ConstraintValidator<A, V> constraintValidator;
// 2. 如果找不到匹配的验证器类,返回 DUMMY!
if (validatorClass == null) {
constraintValidator = (ConstraintValidator<A, V>) DUMMY_CONSTRAINT_VALIDATOR;
} else {
// 3. 找到了验证器类,创建实例并初始化
constraintValidator = constraintFactory.getInstance(validatorClass);
if (constraintValidator == null) {
throw LOG.getConstraintFactoryMustNotReturnNullException(validatorClass);
}
initializeValidator(descriptor, constraintValidator);
}
return constraintValidator;
}
关键在于 findMatchingValidatorClass 方法!
这个方法会在 hibernate-validator 内部注册的验证器列表中查找:
-
输入:
@NotBlank注解 +String类型 -
查找:能处理这个组合的
ConstraintValidator实现类 -
结果:
- Hibernate Validator 6.x :找到
org.hibernate.validator.internal.constraintvalidators.bv.NotBlankValidator - Hibernate Validator 5.x:找不到(因为根本没有实现这个类),返回 null
- Hibernate Validator 6.x :找到
完整调用链:
scss
验证触发
↓
getConstraintValidatorNoUnwrapping()
↓
getInitializedValidator() // 从缓存或创建验证器
↓
createAndInitializeValidator()
↓
findMatchingValidatorClass() // 查找验证器类
↓
在 Hibernate Validator 5.x 的验证器注册表中查找
↓
找不到 NotBlankValidator(因为 5.x 没有实现)
↓
返回 null
↓
创建 DUMMY_CONSTRAINT_VALIDATOR
↓
缓存并返回 DUMMY
↓
getInitializedValidator() 检测到是 DUMMY,返回 null
↓
getConstraintValidatorNoUnwrapping() 检测到 validator == null
↓
抛出 UnexpectedTypeException 异常
验证器是如何加载的?
既然 findMatchingValidatorClass 方法找不到验证器,那么验证器列表是从哪里来的呢?我们可以通过 Java Field Watchpoints 来监控验证器列表的构建过程。
在 IDE 中对 matchingConstraintValidatorClasses 字段设置 Field Watchpoint,观察它何时被赋值:
你会发现断点停在了 ConstraintHelper 类的 findValidatorClasses 方法:
swift
/**
* 查找能够处理指定注解类型的所有验证器类
*/
public <A extends Annotation> List<Class<? extends ConstraintValidator<A, ?>>>
findValidatorClasses(Class<A> annotationType, ValidationTarget validationTarget) {
// 1. 获取该注解类型对应的所有验证器类(关键方法!)
List<Class<? extends ConstraintValidator<A, ?>>> validatorClasses =
getAllValidatorClasses(annotationType);
// 2. 过滤出匹配的验证器类
List<Class<? extends ConstraintValidator<A, ?>>> matchingValidatorClasses =
newArrayList();
for (Class<? extends ConstraintValidator<A, ?>> validatorClass : validatorClasses) {
// 检查验证器是否支持指定的验证目标(方法参数、返回值等)
if (supportsValidationTarget(validatorClass, validationTarget)) {
matchingValidatorClasses.add(validatorClass);
}
}
return matchingValidatorClasses;
}
关键在于 getAllValidatorClasses 方法!
这个方法从内部的验证器注册表中查找:
swift
private <A extends Annotation> List<Class<? extends ConstraintValidator<A, ?>>>
getAllValidatorClasses(Class<A> annotationType) {
// 从 builtinConstraints 映射表中查找
List<Class<? extends ConstraintValidator<?, ?>>> validatorClasses =
builtinConstraints.get(annotationType);
if (validatorClasses == null) {
// 如果不是内置约束,尝试从注解的 @Constraint 元注解中获取
validatorClasses = getValidatorClassesFromConstraintAnnotation(annotationType);
}
return (List) validatorClasses;
}
验证器注册表:builtinConstraints 的初始化
builtinConstraints 是一个 Map<Class<? extends Annotation>, List<Class<? extends ConstraintValidator<?, ?>>>> 映射表,在 ConstraintHelper 构造函数中初始化。
Hibernate Validator 5.x 的注册表:
ruby
public ConstraintHelper() {
Map<Class<? extends Annotation>, List<Class<? extends ConstraintValidator<?, ?>>>>
tmpConstraints = newHashMap();
// 注册 @NotNull 验证器
putConstraint(tmpConstraints, NotNull.class,
NotNullValidator.class);
// 注册 @Size 验证器(多个实现)
putConstraints(tmpConstraints, Size.class,
Arrays.asList(
SizeValidatorForCharSequence.class,
SizeValidatorForCollection.class,
SizeValidatorForArray.class,
SizeValidatorForMap.class
)
);
// 注册 @Min, @Max, @Pattern 等...
putConstraint(tmpConstraints, Min.class, MinValidatorForNumber.class);
putConstraint(tmpConstraints, Max.class, MaxValidatorForNumber.class);
putConstraint(tmpConstraints, Pattern.class, PatternValidator.class);
//注意:这里没有 NotBlank 的注册!
this.builtinConstraints = Collections.unmodifiableMap(tmpConstraints);
}
完整的验证器查找流程
现在我们可以把整个流程串起来了:
dart
1. 应用启动
↓
2. ConstraintHelper 初始化
↓
3. 构建 builtinConstraints 映射表
- Hibernate Validator 5.x: 没有 NotBlank.class → NotBlankValidator.class 映射
- Hibernate Validator 6.x: 有 NotBlank.class → NotBlankValidator.class 映射
↓
4. 触发字段校验(@NotBlank String keywords)
↓
5. findMatchingValidatorClass() 被调用
↓
6. 调用 findValidatorClasses(NotBlank.class, ...)
↓
7. 调用 getAllValidatorClasses(NotBlank.class)
↓
8. 从 builtinConstraints.get(NotBlank.class) 查找
- Hibernate Validator 5.x: 返回 null(映射表中没有)
- Hibernate Validator 6.x: 返回 [NotBlankValidator.class]
↓
9. Hibernate Validator 5.x 路径:
validatorClasses == null
→ matchingValidatorClasses 为空
→ findMatchingValidatorClass 返回 null
→ 创建 DUMMY_CONSTRAINT_VALIDATOR
→ getInitializedValidator 返回 null
→ 抛出 UnexpectedTypeException
通过调试验证:
你可以在以下位置打断点验证整个流程:
ConstraintHelper构造函数 - 查看builtinConstraints的内容getAllValidatorClasses- 查看查找结果findValidatorClasses- 查看matchingValidatorClasses的构建createAndInitializeValidator- 查看validatorClass是否为 null

从调试截图可以清楚地看到:
-
有
javax.validation.constraints.NotNull- 这是标准 Bean Validation 注解
- Hibernate Validator 5.x 支持
-
有
org.hibernate.validator.constraints.NotBlank- 注意包名!这是
org.hibernate.validator.constraints - 不是标准的
javax.validation.constraints - 这是 Hibernate Validator 自己扩展的注解,不是 Bean Validation 2.0 标准
- 注意包名!这是
-
没有
javax.validation.constraints.NotBlank- 标准的 Bean Validation 2.0
@NotBlank不在映射表中 - 所以使用
javax.validation.constraints.NotBlank会找不到验证器
- 标准的 Bean Validation 2.0
两个不同的 @NotBlank
typescript
// ❌ 在 Hibernate Validator 5.x 中不支持(Bean Validation 2.0 标准)
import javax.validation.constraints.NotBlank;
@NotBlank // 抛出异常:找不到验证器
private String keywords;
typescript
// 在 Hibernate Validator 5.x 中支持(Hibernate 自定义扩展)
import org.hibernate.validator.constraints.NotBlank;
@NotBlank // 可以正常工作
private String keywords;
为什么会有两个 @NotBlank?
- Hibernate Validator 5.x 时代 :
@NotBlank是 Hibernate 的自定义扩展,位于org.hibernate.validator.constraints包 - Bean Validation 2.0 时代 :
@NotBlank被纳入标准规范,位于javax.validation.constraints包 - Hibernate Validator 6.x :同时支持两个包的
@NotBlank,建议使用标准包
解决方案
方案一:检查并添加依赖(推荐)
确保 pom.xml 中包含正确版本的依赖:
xml
<!-- Bean Validation API -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- Hibernate Validator 实现 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
注意版本对应关系:
validation-api 2.0.x对应hibernate-validator 6.xvalidation-api 1.1.x对应hibernate-validator 5.x(不支持 @NotBlank)
方案二:使用 Spring Boot Starter
如果是 Spring Boot 项目,推荐使用官方 starter:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
这个 starter 会自动引入兼容版本的 validation-api 和 hibernate-validator。
方案三:降级使用 @NotEmpty 或 @NotNull + @Size
如果暂时无法升级依赖,可以使用旧版本支持的注解组合:
less
// 方式1:使用 @NotEmpty(Bean Validation 2.0)
@NotEmpty
private String keywords;
// 方式2:使用组合注解(Bean Validation 1.1)
@NotNull
@Size(min = 1)
private String keywords;
总结
这次踩坑让我深刻理解了:
- 注解定义和注解实现是分离的:validation-api 只提供规范,hibernate-validator 提供实现
- 版本匹配很关键:Bean Validation 1.1 不支持 @NotBlank
- 依赖管理要规范:使用 Spring Boot Starter 可以避免很多版本冲突问题
- 错误信息要仔细读 :
No validator could be found其实已经明确指出是缺少验证器实现
普通公司开发确实需要注意这些细节,毕竟没有专门的架构组来统一管理依赖版本,踩过坑才能记得更牢。
相关资源: