@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 其实已经明确指出是缺少验证器实现

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


相关资源:

相关推荐
随风飘的云12 分钟前
mysql在查询的时候走索引比不走索引一定快吗?
后端
h贤15 分钟前
高可靠微服务消息设计:Outbox模式、延迟队列与Watermill集成实践
后端
架构师专栏16 分钟前
Spring Boot 4 概述与重大变化
spring boot·后端
踏浪无痕17 分钟前
6张表、14步业务逻辑,Mall订单事务凭什么比你的3步事务还稳?
spring boot·spring·面试
武子康19 分钟前
大数据-162 Apache Kylin 增量 Cube 与 Segment 实战:按天分区增量构建指南
大数据·后端·apache kylin
SimonKing42 分钟前
IntelliJ IDEA 2025.2.x的小惊喜和小BUG
java·后端·程序员
青梅主码1 小时前
介绍一下我用AI开发的一款新工具:函数图像绘制工具(二)
后端
q***01771 小时前
Spring Boot 热部署
java·spring boot·后端
IT_陈寒2 小时前
JavaScript 闭包通关指南:从作用域链到内存管理的8个核心知识点
前端·人工智能·后端