1. 概述

Bean Validation 是一个标准的校验规范,它允许我们通过注解形式声明一组约束来轻松校验领域对象。

虽然使用像 Hibernate Validator 这样的 Bean Validation 实现整体上非常直观,但一些约束在实现细节上存在细微却重要的差异,值得深入探讨。

在本篇文章中,我们将重点分析 @NotNull、*@NotEmpty* 和 @NotBlank 三者之间的区别

2. Maven 依赖配置

为了快速搭建一个可运行的测试环境,验证这三种注解的行为,首先我们需要引入必要的 Maven 依赖。

这里我们使用 Hibernate Validator —— Bean Validation 的参考实现,用于校验我们的领域对象。

以下是 pom.xml 中的相关依赖配置:

<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.0.Final</version>
</dependency>

我们还会在单元测试中使用 JUnitAssertJ,请确保你已在 Maven Central 上获取最新版本的 hibernate-validatorGlassFish 的 EL 实现junitassertj-core

3. @NotNull 约束详解

接下来,我们先定义一个简单的 UserNotNull 领域类,并对其中的 name 字段使用 @NotNull 注解:

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,但允许为空字符串。

我们可以通过查看 NotNullValidatorisValid() 方法来理解它的实现逻辑,该方法非常简单:

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

⚠️ 总结:*使用 @NotNull 约束的字段(如 CharSequenceCollectionMap* 或 Array)不能为 null,但可以为空(如空字符串)**。

4. @NotEmpty 约束详解

接下来我们定义 UserNotEmpty 类,并使用 @NotEmpty 注解:

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 注解不仅检查字段是否为 null,还会检查其长度或大小是否大于 0。它内部使用了 @NotNull 的校验逻辑,并增加了对长度的判断。

✅ 总结:使用 @NotEmpty 约束的字段必须非 null,且其长度/大小必须大于 0

⚠️ 此外,我们还可以将 @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 注解:

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 类,其内部会对字符串进行 trim() 处理,并判断长度是否大于 0:

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

✅ 总结:使用 @NotBlank 约束的字段必须非 null,且去除前后空格后的长度必须大于 0

⚠️ 换句话说,*@NotBlank* 是最严格的字符串约束,连只包含空格的字符串都不允许。

6. 三者对比一览

到目前为止,我们已经分别介绍了这三种注解的使用方式和行为。下面通过一个对比表格来快速总结它们的区别:

注解 是否允许 null 是否允许空字符串 "" 是否允许纯空格字符串 " "
@NotNull ❌ 不允许 ✅ 允许 ✅ 允许
@NotEmpty ❌ 不允许 ❌ 不允许 ✅ 允许
@NotBlank ❌ 不允许 ❌ 不允许 ❌ 不允许

✅ 简单粗暴总结:

  • @NotNull:只要不是 null 就行,哪怕是空字符串。
  • @NotEmpty:不能为空,长度必须大于 0。
  • @NotBlank:不能是空白字符,必须有“实际内容”。

7. 结论

本文我们详细分析了 Bean Validation 中 @NotNull、*@NotEmpty* 和 @NotBlank 三种注解的使用方式和行为差异,并通过示例代码和测试验证了它们的校验逻辑。

如需查看本文中的完整代码示例,可访问 GitHub 项目地址


原始标题:Difference Between @NotNull, @NotEmpty, and @NotBlank Constraints in Bean Validation