@NotBlank 不生效报错 No validator could be found:Hibernate Validator 版本匹配指北

问题背景

最近在写接口的时候,遇到了一个看似简单但实际很坑的参数校验问题。代码是这样的:

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;
}

流程解析:

  1. 获取被校验字段的类型:String.class
  2. ConstraintValidatorManager 中查找能处理 @NotBlank + String 组合的验证器
  3. 在 hibernate-validator 5.x 中找不到 NotBlankValidator,返回 null
  4. 抛出 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;
}

注意最后一行 :如果 constraintValidatorDUMMY_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

完整调用链:

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

通过调试验证:

你可以在以下位置打断点验证整个流程:

  1. ConstraintHelper 构造函数 - 查看 builtinConstraints 的内容
  2. getAllValidatorClasses - 查看查找结果
  3. findValidatorClasses - 查看 matchingValidatorClasses 的构建
  4. createAndInitializeValidator - 查看 validatorClass 是否为 null

从调试截图可以清楚地看到:

  1. javax.validation.constraints.NotNull

    • 这是标准 Bean Validation 注解
    • Hibernate Validator 5.x 支持
  2. org.hibernate.validator.constraints.NotBlank

    • 注意包名!这是 org.hibernate.validator.constraints
    • 不是标准的 javax.validation.constraints
    • 这是 Hibernate Validator 自己扩展的注解,不是 Bean Validation 2.0 标准
  3. 没有 javax.validation.constraints.NotBlank

    • 标准的 Bean Validation 2.0 @NotBlank 不在映射表中
    • 所以使用 javax.validation.constraints.NotBlank 会找不到验证器

两个不同的 @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.x
  • validation-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-apihibernate-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;

总结

这次踩坑让我深刻理解了:

  1. 注解定义和注解实现是分离的:validation-api 只提供规范,hibernate-validator 提供实现
  2. 版本匹配很关键:Bean Validation 1.1 不支持 @NotBlank
  3. 依赖管理要规范:使用 Spring Boot Starter 可以避免很多版本冲突问题
  4. 错误信息要仔细读No validator could be found 其实已经明确指出是缺少验证器实现

普通公司开发确实需要注意这些细节,毕竟没有专门的架构组来统一管理依赖版本,踩过坑才能记得更牢。


相关资源:

相关推荐
嘉伟桑1 小时前
Python调用电价API返回JSON示例:分时电价、现货电价和字段解析
后端
武子康1 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
我叫黑大帅1 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
吃饱了得干活1 小时前
亿级订单表分库分表设计,从0到1全流程
java·数据库·面试
AskHarries2 小时前
文件上传系统
后端
阿里嘎多学长2 小时前
2026-07-22 GitHub 热点项目精选
开发语言·程序员·github·代码托管
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端