TempGo
发布于 2025-12-18 / 4 阅读
0
0

Bean Validation 中 @NotNull、@NotEmpty 和 @NotBlank 的区别

你是不是也遇到过这种场景:后端已经加上了各种校验注解,但接口返回的 400 提示要么太模糊,要么和预期不一样——有的字段明明是空字符串却通过了校验,有的字段是只有空格也被算“有值”?

很多 Java 项目都会用 Bean Validation(比如 Hibernate Validator)做参数校验,但一旦同时出现 @NotNull@NotEmpty@NotBlank,不少人就开始犯糊涂:它们到底有什么细微差别?该在什么场景下用哪一个?

这篇文章就是围绕这三个常见约束展开,配合简单的示例代码和单元测试,从直观行为到底层实现,帮你彻底搞清楚它们的区别和使用场景,写出既严谨又不过度“矫枉过正”的校验逻辑。

1. 概览

Bean Validation 是 Java 平台上的一套标准验证规范,我们可以通过在领域对象上添加注解约束的方式,轻松完成校验逻辑。

在实际使用中,像 Hibernate Validator 这样的实现一般都比较“开箱即用”,但其中一些看似相近的约束,在实现和语义上有细微而重要的差别,值得单独拎出来说一说。

在这篇文章中,我们会重点对比 Bean Validation 中三个常见约束:@NotNull@NotEmpty@NotBlank

2. Maven 依赖

为了快速搭建一个可运行的环境,来测试 @NotNull@NotEmpty@NotBlank 的行为,我们先添加必要的 Maven 依赖。

这里使用 Hibernate Validator(Bean Validation 的参考实现)来校验我们的领域对象。在 Spring Boot 项目中,只需要引入校验 starter 即可:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

这个依赖会通过传递依赖的方式引入 Hibernate Validator。如果不是 Spring Boot 项目,也可以直接单独引入 Hibernate Validator:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>

在接下来的单元测试中,我们会使用 JUnit 和 AssertJ。实际项目中可以根据需要选择合适版本的 hibernate-validator、EL 实现、junitassertj-core

3. @NotNull 约束

首先,我们实现一个简单的领域类 UserNotNull,并使用 @NotNull 约束它的 name 字段:

public class UserNotNull {

    @NotNull(message = "Name may not be null")
    private String name;

    // 标准构造器 / getter / toString 等
}

接下来写几个单元测试,看看 @NotNull 实际是怎么工作的:

@BeforeClass
public static void setupValidatorInstance() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
}

@Test
public void whenNotNullName_thenNoConstraintViolations() {
    UserNotNull user = new UserNotNull("John");
    Set<ConstraintViolation<UserNotNull>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(0);
}

@Test
public void whenNullName_thenOneConstraintViolation() {
    UserNotNull user = new UserNotNull(null);
    Set<ConstraintViolation<UserNotNull>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(1);
}

@Test
public void whenEmptyName_thenNoConstraintViolations() {
    UserNotNull user = new UserNotNull("");
    Set<ConstraintViolation<UserNotNull>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(0);
}

从结果可以看到:

  • 加了 @NotNull 的字段不允许为 null
  • 但如果是空字符串(""),是可以通过校验的。

为了更直观地理解这一点,我们看一下 @NotNull 背后所用的验证逻辑 NotNullValidatorisValid() 实现,代码非常简单:

public boolean isValid(Object object) {
    return object != null;
}

也就是说,@NotNull 约束的字段(无论是 CharSequenceCollectionMap 还是数组)只要不是 null 就算合法;至于是否为空,是不管的。

4. @NotEmpty 约束

接下来我们看 @NotEmpty。先实现一个示例类 UserNotEmpty

public class UserNotEmpty {

    @NotEmpty(message = "Name may not be empty")
    private String name;

    // 标准构造器 / getter / toString 等
}

然后同样写单元测试,用不同的值赋给 name 字段:

@Test
public void whenNotEmptyName_thenNoConstraintViolations() {
    UserNotEmpty user = new UserNotEmpty("John");
    Set<ConstraintViolation<UserNotEmpty>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(0);
}

@Test
public void whenEmptyName_thenOneConstraintViolation() {
    UserNotEmpty user = new UserNotEmpty("");
    Set<ConstraintViolation<UserNotEmpty>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(1);
}

@Test
public void whenNullName_thenOneConstraintViolation() {
    UserNotEmpty user = new UserNotEmpty(null);
    Set<ConstraintViolation<UserNotEmpty>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(1);
}

@NotEmpty 在实现上会复用 @NotNull 的非空判断,同时还会检查被校验对象的“长度/大小”是否大于 0(针对不同类型有不同的度量方式)。

简单来说,@NotEmpty 约束的字段(如 CharSequenceCollectionMap 或数组)必须不是 null,并且长度或大小必须大于 0。 换句话说,既不能是 null,也不能是“空集合/空字符串/空数组”。

如果需要更严格的约束,可以把 @NotEmpty@Size 结合使用,在“非空”的基础上再限定最小、最大长度:

@NotEmpty(message = "Name may not be empty")
@Size(min = 2, max = 32, message = "Name must be between 2 and 32 characters long")
private String name;

5. @NotBlank 约束

再看最后一个 @NotBlank,它通常用于只对 字符串 做“非空白”校验。

先定义一个示例类 UserNotBlank

public class UserNotBlank {

    @NotBlank(message = "Name may not be blank")
    private String name;

    // 标准构造器 / getter / toString 等
}

同样写几个单元测试来理解它的行为:

@Test
public void whenNotBlankName_thenNoConstraintViolations() {
    UserNotBlank user = new UserNotBlank("John");
    Set<ConstraintViolation<UserNotBlank>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(0);
}

@Test
public void whenBlankName_thenOneConstraintViolation() {
    UserNotBlank user = new UserNotBlank(" ");
    Set<ConstraintViolation<UserNotBlank>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(1);
}

@Test
public void whenEmptyName_thenOneConstraintViolation() {
    UserNotBlank user = new UserNotBlank("");
    Set<ConstraintViolation<UserNotBlank>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(1);
}

@Test
public void whenNullName_thenOneConstraintViolation() {
    UserNotBlank user = new UserNotBlank(null);
    Set<ConstraintViolation<UserNotBlank>> violations = validator.validate(user);

    assertThat(violations.size()).isEqualTo(1);
}

@NotBlank 使用的验证器是 NotBlankValidator,它会先判断是否为 null,然后对字符串做 trim() 再判断长度是否大于 0:

public boolean isValid(
        CharSequence charSequence,
        ConstraintValidatorContext constraintValidatorContext) {
    if (charSequence == null) {
        return false;
    }
    return charSequence.toString().trim().length() > 0;
}

可以看到:

  • 传入 null 会返回 false
  • 只包含空格、制表符等“空白字符”的字符串,trim() 后长度为 0,也会被判定为无效。

因此,@NotBlank 约束的 String 字段必须不是 null,并且在去除前后空白字符之后的长度必须大于 0。 换句话说,既不能为 null,也不能是空字符串,更不能只是若干空格。

6. 并排对比

到目前为止,我们分别看了 @NotNull@NotEmpty@NotBlank 在类字段上的行为。下面做一个并排总结,方便快速对比和记忆:

  • @NotNull:被约束的 CharSequenceCollectionMap 或数组 只要不是 null 就视为有效,可以是空的
  • @NotEmpty:被约束的 CharSequenceCollectionMap 或数组 必须不是 null,并且长度/大小必须大于 0
  • @NotBlank:被约束的 String 必须不是 null,并且在 trim() 之后长度必须大于 0

一个常见的组合是:

  • 对集合、数组之类,用 @NotEmpty(必要时再配合 @Size)。
  • 对只要求“非 null、但可以空字符串”的字段,用 @NotNull
  • 对必须有“非空白内容”的字符串(如用户名、密码、标题),优先使用 @NotBlank

7. 总结

本文基于简单示例和单元测试,对比了 Bean Validation 中 @NotNull@NotEmpty@NotBlank 三个约束的行为差异:

  • @NotNull 只管“不是 null”。
  • @NotEmpty 在此基础上,再要求“长度/大小大于 0”。
  • @NotBlank 则进一步要求“去掉空白字符后长度大于 0”,只适用于字符串。

在实际项目中,选对约束可以让你的接口校验既严谨又语义清晰,避免“看起来加了注解,实际上校验并没有按预期执行”的尴尬局面。下一次写字段校验时,不妨根据这里的对比表,重新审视一下自己选用的注解是否真的匹配业务需求。


评论