你是不是也遇到过这种场景:后端已经加上了各种校验注解,但接口返回的 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 实现、junit 和 assertj-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 背后所用的验证逻辑 NotNullValidator 的 isValid() 实现,代码非常简单:
public boolean isValid(Object object) {
return object != null;
}
也就是说,被 @NotNull 约束的字段(无论是 CharSequence、Collection、Map 还是数组)只要不是 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 约束的字段(如 CharSequence、Collection、Map 或数组)必须不是 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:被约束的CharSequence、Collection、Map或数组 只要不是null就视为有效,可以是空的。@NotEmpty:被约束的CharSequence、Collection、Map或数组 必须不是null,并且长度/大小必须大于 0。@NotBlank:被约束的String必须不是null,并且在trim()之后长度必须大于 0。
一个常见的组合是:
- 对集合、数组之类,用
@NotEmpty(必要时再配合@Size)。 - 对只要求“非 null、但可以空字符串”的字段,用
@NotNull。 - 对必须有“非空白内容”的字符串(如用户名、密码、标题),优先使用
@NotBlank。
7. 总结
本文基于简单示例和单元测试,对比了 Bean Validation 中 @NotNull、@NotEmpty 和 @NotBlank 三个约束的行为差异:
@NotNull只管“不是null”。@NotEmpty在此基础上,再要求“长度/大小大于 0”。@NotBlank则进一步要求“去掉空白字符后长度大于 0”,只适用于字符串。
在实际项目中,选对约束可以让你的接口校验既严谨又语义清晰,避免“看起来加了注解,实际上校验并没有按预期执行”的尴尬局面。下一次写字段校验时,不妨根据这里的对比表,重新审视一下自己选用的注解是否真的匹配业务需求。