@NotNull 注解也能搞错?同事这波操作让我直接裂开💔

1,117 阅读13分钟

🏆本文收录于「滚雪球学SpringBoot」专栏,专业攻坚指数级提升持续更新中,up!up!up!!

📜 前言

  哈喽,家人们,有没有被“空指针异常”搞得怀疑人生的经历?今天我就亲身体验了一把,这波真的是直接裂开!

  事情是这样的:有位用户在使用 ERP 系统时,为了偷懒不填必填字段(后来排查才知道,真是服了),直接敲了一堆空格绕过校验,成功提交了数据。结果,这问题可就来了——领导点开详情页面,发现页面一片空白。于是,领导火速在 DD 群里艾特我,要求赶紧处理。说实话,看到那一刻我确实有点紧张:页面空白?不对劲啊,这种低级问题怎么可能出现在我负责的系统里?

  一顿线上 Debug 排查后,终于锁定了罪魁祸首:接口报了空指针异常。仔细一看,不得了,竟然是某个必填字段校验居然通过了!再深挖一下代码才发现,问题出在 @NotNull 注解上。同事用这个校验非空,却没处理字符串全是空格的情况。翻查提交记录,嗯,这功能是我安排同事做的,还是个新人任务……

  当场裂开 💔!虽说是个低级错误,但作为带新人的我来说,出这种问题着实有点打脸。不过,回头一想,这次事故也算给我提了个醒:带新人不仅要安排任务,还得盯紧代码质量,尤其是细节校验这种地方。成长的代价有点惨痛,特别是脸(pia的很响)

  面对NullPointerException 异常,它就像幽灵一般,总是在你最不设防的时候突然冒出来。面对我同事,我只能耐心安慰她,并跟她剖析原理,毕竟当初面她时决策是否录取我也有占了很大比重。

  所以说,今天我就特地出一期,给同学们特别是我同事,好好上一课,👀会的同学可以粗略过一遍,不会的同学特别是我那同事!认真看好好学,别走神,因为我只教这一遍。准备好了吗?我要开始咯。

📚 目录

  1. 🧐 什么是 @NotNull?它的作用是什么?
  2. 🎩 @NotNull 的基本用法
  3. ✍️ 实战演示:如何在 Spring Boot 项目中使用 @NotNull
  4. 🔄 @NotNull 与其他校验注解的区别(@NotEmpty、@NotBlank)
  5. 🛠️ 注解搭配:组合实现更强大的校验
  6. 🤓 底层原理:@NotNull 是如何工作的?
  7. 💡 最佳实践与常见坑
  8. 📈 总结与心得

🧐 什么是 @NotNull?它的作用是什么?

  @NotNull注解,它是 Java Bean Validation 提供的一个注解,@NotNull用于验证对象是否不为 null。通常用来标注一个字段、方法参数、方法返回值等,表示该值不能为 null。它是由 Java Bean Validation API 提供的,属于 javax.validation.constraints 包,与@NotBlank一个出处。该注解的作用是对字段或参数进行约束,确保其值在运行时不为 null,否则会触发验证失败并抛出相应的错误信息。它适合用于任何类型的字段或方法参数,只要你希望这个值不能是 null,就可以使用 @NotNull 来确保它有值。通俗一点说,这个注解就是在告诉程序:如果你敢为空,那咱们就没完!而上述这个车祸现场,确不适合用它!适合用@NotBlank

主要属性:

  • message:自定义验证失败时的错误消息。可以使用占位符,例如 {javax.validation.constraints.NotNull.message},这会引用默认的消息模板,或者你可以自己设置如 "Name cannot be null"
@NotNull(message = "Name cannot be null")
private String name;
  • groups:用于指定验证分组。通过这个属性,可以定义不同的验证场景。例如,可以在某些情况下跳过某些验证,只验证特定的组。
@NotNull(groups = Create.class)
private String name;
  • payload:用于传递额外的信息,通常用于与框架交互传递特定的元数据。在一般使用中不太常见,更多地是框架扩展时使用。

  @NotNull 注解属于 javax.validation.constraints 包,可从源码中进行查看,一般使用都需要导入import javax.validation.constraints.*;

  而且,它的应用场景也相对校广,比如数据库的主键、用户名、订单ID等,这些值必须有值,不能留空。@NotNull 可以让你不必在代码中写一堆“!= null”的if else 判断,大大减少这种逻辑校验,且提升代码简洁性与低耦合。

🎩 @NotNull 的基本用法

  @NotNull 的使用非常直观,只需将它放在希望不为 null 的字段或参数前即可。还可以通过设置 message 属性,自定义当验证失败时的提示信息。

示例代码

  比如,我们有一个 User 类,要求用户名 name 是必填项:

import javax.validation.constraints.NotNull;

public class User {

    @NotNull(message = "用户名不能为空")
    private String name;

    private Integer age;

    // Getter 和 Setter
}

  在这里,@NotNull 保证了 name 必须有值,不得为空。Spring 会在接收到数据时自动进行检查,如果 name 为空,就会抛出验证错误,并返回我们设置的提示信息“用户名不能为空”。但如果你得需求要保证 name 不为空串,那么你选择用它就不合适了。

  不信,这里我给大家做个实验,大家请看:

  很明显结果已定,如果是要用@NotNull 注解,那就判断不了空格值的场景,这点大家需要慎重鉴定,别跟我同事一样犯这种低级错误哦。

✍️ 实战演示:如何在项目中使用 @NotNull

  接下来,我们来看看在 Spring Boot 项目中,@NotNull 是如何与控制器和异常处理器协作,进行全面的校验。

Step 1: 导入对应依赖

  首先,为了使用 @NotNull,你需要在项目中引入一个支持 Jakarta Bean Validation 的实现库,比如 Hibernate Validator。

以下是常见的 Maven 依赖配置:

Maven 配置:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.5.Final</version> <!-- 请根据需要选择版本 -->
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

Gradle 配置:

implementation 'org.hibernate.validator:hibernate-validator:6.2.5.Final'
implementation 'javax.validation:validation-api:2.0.1.Final'

  这个根据大家具体的项目依赖构建方式而定,而选择对应的方式导入依赖。

Step 2: 创建验证实体类

  假设我们有一个用户注册请求,其中 nameemail 是必填的,而年龄 age 是可选的。我们可以使用 @NotNull 标记这些必填字段。

import javax.validation.constraints.NotNull;

public class RegistrationRequest {

    @NotNull(message = "姓名不能为空")
    private String name;

    @NotNull(message = "邮箱不能为空")
    private String email;

    private Integer age;

    // Getter 和 Setter
}

Step 3: 在 Controller 中使用 @Validated 触发验证

  在控制器方法中,我们可以使用 @Valid 注解来触发验证逻辑,如果 nameemail 字段为 null,Spring 会自动返回错误提示。

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/system/testUser")
public class RegistrationController {

    /**
     * 新增user
     */
    @ApiOperation("新增user")
    @PostMapping("/add")
    public R<Void> add(@Validated @RequestBody User user) {
        return toR(userService.insert(user));
    }
}

Step 4: 使用全局异常处理器捕获验证失败

  为了更优雅地处理验证失败的情况,我们可以使用全局异常处理器,将错误信息集中管理。

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String handleValidationExceptions(MethodArgumentNotValidException ex) {
        return ex.getBindingResult().getFieldError().getDefaultMessage();
    }
}

  这样一来,当验证失败时,系统会返回我们自定义的错误信息,更加直观友好。

Step 5: 实际演示

  为了验证如上代码注解是否真正能够拦截这些情景,我们直接上Postman来模拟调用接口测试一下。

模拟场景1:用户名直接传null值

  这里我们请求的时候,将name直接传null值进行请求。Postman请求如下:

  如上成功验证若用户名传入null值则自动会被拦截,且提示对应的message信息,如“姓名不能为空”。

模拟场景2:邮箱号直接传null值

  继续模拟,这里我们在发请求的时候,将email直接传null值。Postman请求如下:

  如上成功验证邮箱号传null值也依然会被拦截,强行被提示“邮箱不能为空”。

  总之,这里已经替大家都测试过了,大家可放心食用。

🔄 @NotNull 与其他校验注解的区别(@NotEmpty、@NotBlank)

  Java Bean Validation 它提供了多个用于非空校验的注解,每个注解的适用范围和检查内容都不同。我们特地来看看 @NotNull@NotEmpty@NotBlank 的区别。

  1. @NotNull:用于验证字段不为 null,允许空字符串或空集合。
  2. @NotEmpty:不仅要求字段不为 null,还不允许空集合或空字符串。
  3. @NotBlank:最严格的检查,不允许 null,且字符串内容不能全是空格。
public class Example {

    @NotNull(message = "此字段不能为空")
    private List<String> notNullList;

    @NotEmpty(message = "此字段不能为空集合")
    private List<String> notEmptyList;

    @NotBlank(message = "此字段不能全为空白字符")
    private String notBlankField;
}

  @NotNull 是最基本的检查,它仅保证字段有值;如果需要更严格的校验,比如检查字符串或集合是否有内容,还需要结合其他注解,所以说,看到这里,可别再乱用这三个注解啦。

🛠️ 注解搭配:组合实现更强大的校验

  在实际开发中,@NotNull 通常与其他校验注解配合使用,比如 @Size@Pattern 等,以满足更复杂的校验需求。

  例如,我们希望用户名不仅不能为空,还必须是至少 5 个字符以上的字符串,这时可以组合 @NotNull@Size

import javax.validation.constraints.Size;

public class UserRequest {

    @NotNull(message = "用户名不能为空")
    @Size(min = 5, message = "用户名至少包含5个字符")
    private String name;

    @NotNull(message = "邮箱不能为空")
    private String email;
}

  通过这种组合方式,我们可以实现更精准的数据控制。

🤓 底层原理:@NotNull 是如何工作的?

  @NotNull 注解依赖于 Java Bean Validation API(JSR 380),通常由 Hibernate Validator 等实现库支持。当我们在字段上标记了 @NotNull,Spring 会在数据传入时自动触发验证逻辑。如果字段的值为 null,验证就会失败,因此请求也就直接会被拦截无法通过。

  这意味着,我们只需在实体类上标记 @NotNull,校验过程便会在数据传入时自动执行,无需额外手动代码。这种机制大大提升了代码的简洁性和数据校验的一致性。

  接着我们重点来剖析下是它的工作原理:

1. 注解作用

  @NotNull 注解的作用是标注某个字段、方法参数或方法返回值不能为 null。如果该值为 null,在验证时会触发验证失败。

2. 验证机制

依赖验证实现(如 Hibernate Validator)

  @NotNull 依赖于 Java Bean Validation API 来执行约束检查。该 API 定义了约束的注解(如 @NotNull),然后通过实现(如 Hibernate Validator)执行实际的验证工作。

  通常,验证过程包括以下步骤:

  1. 创建 Validator 工厂:通常你会创建一个 ValidatorFactory,然后通过它获取一个 Validator 实例。
  2. 执行验证:通过调用 validate()validateProperty() 方法来验证对象或对象的某个属性。
  3. 获取验证结果:验证的结果会返回一组 ConstraintViolation 对象,描述违反约束的具体信息。

3. 验证过程

  当验证器(如 Hibernate Validator)执行 @NotNull 的验证时,它会:

  • 读取注解的属性:例如,messagegroupspayload 等。
  • 检查目标值是否为 null:如果值是 null,则会产生验证错误。
  • 产生验证结果:如果验证失败,ConstraintViolation 会保存错误信息,表示验证失败的字段或参数。

4. 使用场景

a. 验证字段

  @NotNull 通常用于类的字段,确保该字段不能为空。以下是一个例子:

public class User {
    @NotNull(message = "Username cannot be null")
    private String username;
}

  验证时,如果 usernamenull,就会触发验证错误,返回指定的错误消息。

b. 验证方法参数

  你也可以在方法的参数上使用 @NotNull,确保传入的参数不为空。

public void setUsername(@NotNull String username) {
    this.username = username;
}

  如果调用 setUsername() 时传入 null,会触发验证失败。

c. 验证方法返回值

  @NotNull 也可以用于方法返回值,确保返回值不为 null

@NotNull(message = "Returned value cannot be null")
public String getUsername() {
    return username;
}

5. 验证的执行

  通常,@NotNull 验证是在对象的生命周期中的某个时刻触发的。常见的触发方式包括:

  • 在数据持久化前(如 JPA/Hibernate):验证数据在存入数据库之前不为空。
  • 在方法调用时:通过验证方法的参数,确保传入的参数不能为空。
  • 在框架(如 Spring)中:通过使用 @Valid@Validated 注解来触发验证。

6. 错误处理

  如果验证失败,验证器会抛出异常或返回验证错误信息:

  • 验证失败时抛出异常@NotNull 的验证失败会触发 ConstraintViolationException 异常。在一些框架(如 Hibernate Validator)中,验证失败会将错误信息保存在 ConstraintViolation 对象中。
  @NotNull(message = "Field cannot be null")
  private String field;
  
  Set<ConstraintViolation<User>> violations = validator.validate(user);
  if (!violations.isEmpty()) {
      // 处理验证失败的情况
  }
  • 验证失败时返回错误信息:例如,Spring 中使用 @Valid 进行参数验证,验证失败时返回错误消息。

💡 最佳实践与常见坑

  1. 正确选择注解:如果只需要基本的非空校验,@NotNull 是首选;但如果需要检查集合或字符串的内容,使用 @NotEmpty@NotBlank 更合适(严重强调!)。

  2. 明确错误信息:在 @NotNullmessage 属性中提供清晰的错误提示,帮助用户快速定位问题。

  3. 结合全局异常处理:将校验异常集中处理,可以提升代码的可读性和可维护性。

  4. 注意注解顺序:如果多个注解组合使用,确保 @NotNull 在最前面,保证非空检查优先执行。

📈 总结与心得

  @NotNull 注解虽然简单,但在数据完整性校验中可是重中之重。它不仅可以省去大量“! = null” 的表单字段校验之外,减少由于数据不完整导致的错误,还能提高代码的可读性。通过合理地使用 @NotNull,并结合其他注解,可实现更灵活、更细致的数据验证,谁用谁爽系列之一。

  希望这篇文章能够帮助你(特别是同事你!如果你看到了这篇文章的话🤬)更好地理解 @NotNull 的使用场景和注意事项,还在用if else 校验的同学们赶紧在项目中试试吧!🎉

📣 关于我

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿哇。

-End-