@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 可以用于任何类型,包括:

  • String
  • IntegerLongDouble 等包装类型
  • ListSetMap 等集合类型
  • 自定义对象类型

校验结果示例

输入值 校验结果 说明
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 类型的字段,如 IntegerLongDate、枚举、自定义对象等。对于 String 类型,通常应该使用 @NotBlank 替代。


2.2 @NotEmpty —— "不能为 null 且不能为空"

定义

@NotEmpty@NotNull 的基础上,增加了"不能为空"的校验。对于不同类型,"空"的含义不同:

  • String:长度不能为 0(即不能是 ""
  • Collection / Mapsize() 不能为 0
  • Arraylength 不能为 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(包括 StringStringBuilder 等)
  • CollectionListSet 等)
  • Map
  • Array(数组)

⚠️ 注意@NotEmpty 不能用于 IntegerLong 等基本包装类型,否则会抛出 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 最适合用于集合类型ListSetMap、数组)的校验,确保集合不为 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 用于 IntegerList 等非 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 这三个注解,可以:

  1. 提升代码健壮性:在入口处拦截非法参数,避免脏数据流入业务层和数据库
  2. 减少防御性代码:不再需要在 Service 层手动编写大量的 if (xxx == null) 判断
  3. 统一错误响应:配合全局异常处理器,返回规范、友好的错误信息
  4. 提高代码可读性:注解本身就是一种文档,一眼就能看出字段的约束条件

记住这个口诀:String 用 @NotBlank,集合用 @NotEmpty,其他用 @NotNull,基本就不会出错了!


如果这篇文章对你有帮助,欢迎点赞收藏 🌟

posted @ 2025-07-30 14:03  cwp0  阅读(463)  评论(0)    收藏  举报