@NotBlank、@NotEmpty、@NotNull
一文彻底搞懂 Java 中 @NotNull、@NotEmpty、@NotBlank 的区别与用法
在 Java 后端开发中,参数校验是保障系统健壮性的第一道防线。
@NotNull、@NotEmpty、@NotBlank是 Bean Validation(JSR 303/380)中最常用的三个校验注解,但很多开发者容易混淆它们的使用场景。本文将从定义、源码、适用类型、实际案例、底层原理等多个维度进行深入剖析,帮你一次性彻底搞懂。
一、先说结论:三者核心区别速览
| 注解 | 所属规范 | 校验规则 | 适用类型 |
|---|---|---|---|
@NotNull |
JSR 303 (Bean Validation) | 值不能为 null,但可以是空字符串 "" |
任意类型(Object、String、Integer、Collection 等) |
@NotEmpty |
JSR 303 (Bean Validation) | 值不能为 null 且不能为空(长度/大小 > 0) | String、Collection、Map、Array |
@NotBlank |
JSR 303 (Bean Validation) | 值不能为 null 且去除首尾空格后长度 > 0 | 仅 String 类型 |
一句话总结:@NotNull 管 null,@NotEmpty 管 null 和空,@NotBlank 管 null、空和纯空白字符串。三者的严格程度为:
@NotBlank > @NotEmpty > @NotNull
二、逐个深入解析
2.1 @NotNull —— "不能为 null"
定义
@NotNull 是最基础的非空校验注解,仅校验值是否为 null,不关心值的内容。
源码定义(Hibernate Validator 实现)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = {})
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
校验逻辑
// 等价的校验逻辑
public boolean isValid(Object value) {
return value != null;
}
适用类型
@NotNull 可以用于任何类型,包括:
StringInteger、Long、Double等包装类型List、Set、Map等集合类型- 自定义对象类型
校验结果示例
| 输入值 | 校验结果 | 说明 |
|---|---|---|
null |
❌ 不通过 | null 值 |
"" |
✅ 通过 | 空字符串不是 null |
" " |
✅ 通过 | 空白字符串不是 null |
"hello" |
✅ 通过 | 正常字符串 |
new ArrayList<>() |
✅ 通过 | 空集合不是 null |
0 |
✅ 通过 | 数字 0 不是 null |
典型使用场景
public class UserDTO {
@NotNull(message = "用户ID不能为空")
private Long userId;
@NotNull(message = "创建时间不能为空")
private LocalDateTime createTime;
@NotNull(message = "用户角色不能为空")
private RoleEnum role;
}
最佳实践:
@NotNull最适合用于非 String 类型的字段,如Integer、Long、Date、枚举、自定义对象等。对于 String 类型,通常应该使用@NotBlank替代。
2.2 @NotEmpty —— "不能为 null 且不能为空"
定义
@NotEmpty 在 @NotNull 的基础上,增加了"不能为空"的校验。对于不同类型,"空"的含义不同:
- String:长度不能为 0(即不能是
"") - Collection / Map:
size()不能为 0 - Array:
length不能为 0
源码定义
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = {})
public @interface NotEmpty {
String message() default "{javax.validation.constraints.NotEmpty.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
校验逻辑
// 等价的校验逻辑(以 String 为例)
public boolean isValid(CharSequence charSequence) {
if (charSequence == null) {
return false;
}
return charSequence.length() > 0;
}
// 等价的校验逻辑(以 Collection 为例)
public boolean isValid(Collection<?> collection) {
if (collection == null) {
return false;
}
return !collection.isEmpty();
}
适用类型
@NotEmpty 可以用于以下类型:
CharSequence(包括String、StringBuilder等)Collection(List、Set等)MapArray(数组)
⚠️ 注意:
@NotEmpty不能用于Integer、Long等基本包装类型,否则会抛出UnexpectedTypeException。
校验结果示例
String 类型:
| 输入值 | 校验结果 | 说明 |
|---|---|---|
null |
❌ 不通过 | null 值 |
"" |
❌ 不通过 | 空字符串,长度为 0 |
" " |
✅ 通过 | 空白字符串,长度为 3(大于 0) |
"hello" |
✅ 通过 | 正常字符串 |
Collection 类型:
| 输入值 | 校验结果 | 说明 |
|---|---|---|
null |
❌ 不通过 | null 值 |
[](空列表) |
❌ 不通过 | 集合 size 为 0 |
["a"] |
✅ 通过 | 集合 size 为 1 |
典型使用场景
public class CreateOrderRequest {
@NotEmpty(message = "订单商品列表不能为空")
private List<OrderItemDTO> orderItems;
@NotEmpty(message = "收货地址不能为空")
private String shippingAddress;
@NotEmpty(message = "标签不能为空")
private Set<String> tags;
@NotEmpty(message = "扩展属性不能为空")
private Map<String, Object> extAttributes;
}
最佳实践:
@NotEmpty最适合用于集合类型(List、Set、Map、数组)的校验,确保集合不为 null 且至少包含一个元素。对于 String 类型,如果你希望" "这样的纯空格也不通过校验,应该使用@NotBlank。
2.3 @NotBlank —— "不能为 null、不能为空、不能为纯空白"
定义
@NotBlank 是最严格的字符串校验注解。它在 @NotEmpty 的基础上,增加了去除首尾空格后长度仍需大于 0 的校验。
源码定义
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = {})
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
校验逻辑
// 等价的校验逻辑
public boolean isValid(CharSequence charSequence) {
if (charSequence == null) {
return false;
}
return charSequence.toString().trim().length() > 0;
}
适用类型
@NotBlank 仅适用于 CharSequence 类型(主要是 String)。
⚠️ 注意:将
@NotBlank用于Integer、List等非CharSequence类型会抛出UnexpectedTypeException。
校验结果示例
| 输入值 | 校验结果 | 说明 |
|---|---|---|
null |
❌ 不通过 | null 值 |
"" |
❌ 不通过 | 空字符串 |
" " |
❌ 不通过 | 纯空白字符串,trim 后长度为 0 |
"\t\n" |
❌ 不通过 | 制表符和换行符也属于空白字符 |
" hello " |
✅ 通过 | trim 后为 "hello",长度大于 0 |
"hello" |
✅ 通过 | 正常字符串 |
典型使用场景
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@NotBlank(message = "验证码不能为空")
private String captcha;
}
最佳实践:
@NotBlank是字符串校验的首选注解。在绝大多数业务场景中,一个只包含空格的字符串是没有意义的,因此对于 String 类型的字段,优先使用@NotBlank。
三、三者对比:一张图看懂
null "" " " "hello" 0 [] ["a"]
---- -- ----- ------- - -- -----
@NotNull ❌ ✅ ✅ ✅ ✅ ✅ ✅
@NotEmpty ❌ ❌ ✅ ✅ ⛔ ❌ ✅
@NotBlank ❌ ❌ ❌ ✅ ⛔ ⛔ ⛔
✅ = 校验通过 ❌ = 校验不通过 ⛔ = 不支持该类型(会抛异常)
四、在 Spring Boot 中的实战用法
4.1 引入依赖
在 Spring Boot 2.3+ 版本中,spring-boot-starter-web 不再自动引入 validation 依赖,需要手动添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
如果使用的是 Spring Boot 2.3 之前的版本,
spring-boot-starter-web已经包含了hibernate-validator,无需额外引入。
4.2 在 Controller 中使用
方式一:校验请求体(@RequestBody)
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping("/register")
public Result<Void> register(@Valid @RequestBody UserRegisterRequest request) {
// 如果校验不通过,会抛出 MethodArgumentNotValidException
userService.register(request);
return Result.success();
}
}
public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 32, message = "密码长度必须在6-32之间")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄最小为1")
@Max(value = 150, message = "年龄最大为150")
private Integer age;
@NotEmpty(message = "至少选择一个兴趣标签")
private List<String> interests;
// getter/setter 省略
}
方式二:校验请求参数(@RequestParam / @PathVariable)
@RestController
@RequestMapping("/api/user")
@Validated // 注意:校验方法参数时,需要在类上加 @Validated
public class UserController {
@GetMapping("/{userId}")
public Result<UserVO> getUser(
@PathVariable @NotNull(message = "用户ID不能为空") Long userId) {
return Result.success(userService.getById(userId));
}
@GetMapping("/search")
public Result<List<UserVO>> search(
@RequestParam @NotBlank(message = "关键词不能为空") String keyword) {
return Result.success(userService.search(keyword));
}
}
4.3 全局异常处理
为了返回友好的错误信息,通常需要配合全局异常处理器:
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 @RequestBody 参数校验失败
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException exception) {
String errorMessage = exception.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(400, errorMessage);
}
/**
* 处理 @RequestParam / @PathVariable 参数校验失败
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolation(ConstraintViolationException exception) {
String errorMessage = exception.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
return Result.fail(400, errorMessage);
}
}
4.4 嵌套对象校验
如果 DTO 中包含嵌套对象,需要在嵌套字段上加 @Valid 注解才能触发级联校验:
public class CreateOrderRequest {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@NotNull(message = "收货地址不能为空")
@Valid // 加上 @Valid 才会校验 Address 内部的字段
private Address address;
@NotEmpty(message = "订单项不能为空")
@Valid // 加上 @Valid 才会校验集合中每个元素内部的字段
private List<OrderItem> items;
}
public class Address {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "详细地址不能为空")
private String detail;
}
4.5 分组校验
不同接口可能对同一个 DTO 有不同的校验规则,可以使用分组校验:
// 定义分组接口
public interface CreateGroup {}
public interface UpdateGroup {}
public class UserDTO {
@NotNull(message = "更新时ID不能为空", groups = UpdateGroup.class)
private Long id;
@NotBlank(message = "用户名不能为空", groups = {CreateGroup.class, UpdateGroup.class})
private String username;
@NotBlank(message = "密码不能为空", groups = CreateGroup.class)
private String password;
}
@RestController
public class UserController {
@PostMapping("/create")
public Result<Void> create(
@Validated(CreateGroup.class) @RequestBody UserDTO userDTO) {
// 创建时:校验 username 和 password,不校验 id
return Result.success();
}
@PutMapping("/update")
public Result<Void> update(
@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) {
// 更新时:校验 id 和 username,不校验 password
return Result.success();
}
}
五、常见踩坑与注意事项
5.1 ❌ 踩坑一:@NotBlank 用在非 String 类型上
// ❌ 错误用法:会抛出 UnexpectedTypeException
@NotBlank
private Integer age;
// ✅ 正确用法
@NotNull
private Integer age;
5.2 ❌ 踩坑二:@NotEmpty 用在基本包装类型上
// ❌ 错误用法:会抛出 UnexpectedTypeException
@NotEmpty
private Long userId;
// ✅ 正确用法
@NotNull
private Long userId;
5.3 ❌ 踩坑三:忘记加 @Valid 或 @Validated
// ❌ 错误用法:不会触发校验
@PostMapping("/register")
public Result<Void> register(@RequestBody UserRegisterRequest request) {
// ...
}
// ✅ 正确用法:加上 @Valid
@PostMapping("/register")
public Result<Void> register(@Valid @RequestBody UserRegisterRequest request) {
// ...
}
5.4 ❌ 踩坑四:@Valid 和 @Validated 混淆
| 特性 | @Valid (JSR 303) |
@Validated (Spring) |
|---|---|---|
| 来源 | javax.validation |
org.springframework.validation.annotation |
| 分组校验 | ❌ 不支持 | ✅ 支持 |
| 用在方法参数上 | ✅ 支持 | ✅ 支持 |
| 用在类上 | ❌ 不支持 | ✅ 支持(校验 @RequestParam 等) |
| 嵌套校验 | ✅ 支持 | ❌ 不支持(嵌套字段仍需用 @Valid) |
5.5 ❌ 踩坑五:String 字段用 @NotNull 导致空字符串入库
// ❌ 不推荐:用户提交 "" 或 " " 也能通过校验
@NotNull
private String username;
// ✅ 推荐:使用 @NotBlank 彻底杜绝无意义的字符串
@NotBlank
private String username;
六、包路径变迁:javax vs jakarta
随着 Java EE 向 Jakarta EE 的迁移,这三个注解的包路径也发生了变化:
| 版本 | 包路径 |
|---|---|
| Bean Validation 2.0(Hibernate Validator 6.x) | javax.validation.constraints |
| Jakarta Bean Validation 3.0(Hibernate Validator 7.x+) | jakarta.validation.constraints |
- Spring Boot 2.x → 使用
javax.validation.constraints - Spring Boot 3.x → 使用
jakarta.validation.constraints
如果你从 Spring Boot 2.x 升级到 3.x,需要将所有 javax.validation 的 import 替换为 jakarta.validation。
七、自定义校验注解(扩展)
如果内置的三个注解不能满足需求,可以自定义校验注解。例如,校验手机号格式:
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// null 值不校验,交给 @NotBlank 处理
if (value == null) {
return true;
}
return MOBILE_PATTERN.matcher(value).matches();
}
}
使用方式:
public class LoginRequest {
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
}
八、最佳实践总结
| 字段类型 | 推荐注解 | 理由 |
|---|---|---|
String(用户输入类) |
@NotBlank |
杜绝 null、空串、纯空格 |
String(系统生成类,如 UUID) |
@NotEmpty |
系统生成不会有纯空格的情况 |
Integer / Long / Double |
@NotNull |
包装类型只需判断非 null |
LocalDateTime / Date |
@NotNull |
日期类型只需判断非 null |
Enum |
@NotNull |
枚举类型只需判断非 null |
List / Set |
@NotEmpty |
确保集合非 null 且至少有一个元素 |
Map |
@NotEmpty |
确保 Map 非 null 且至少有一个键值对 |
| 自定义对象 | @NotNull + @Valid |
非 null 且级联校验内部字段 |
九、写在最后
参数校验看似简单,但在实际项目中却是最容易被忽视的环节。合理使用 @NotNull、@NotEmpty、@NotBlank 这三个注解,可以:
- 提升代码健壮性:在入口处拦截非法参数,避免脏数据流入业务层和数据库
- 减少防御性代码:不再需要在 Service 层手动编写大量的
if (xxx == null)判断 - 统一错误响应:配合全局异常处理器,返回规范、友好的错误信息
- 提高代码可读性:注解本身就是一种文档,一眼就能看出字段的约束条件
记住这个口诀:String 用 @NotBlank,集合用 @NotEmpty,其他用 @NotNull,基本就不会出错了!
如果这篇文章对你有帮助,欢迎点赞收藏 🌟

浙公网安备 33010602011771号