当前位置: 首页 > news >正文

Java@Data 与 @NotNull 注解冲突问题

第一章:核心概念解析

1. @Data(Lombok 提供)

  • 自动生成以下方法:
    • getter
    • setter
    • toString()
    • equals()
    • hashCode()
  • 简化实体类编写,提高开发效率。

示例:

import lombok.Data;@Data
public class User {private String username;private Integer age;
}

等价于:

public class User {private String username;private Integer age;public String getUsername() { return username; }public void setUsername(String username) { this.username = username; }public Integer getAge() { return age; }public void setAge(Integer age) { this.age = age; }@Overridepublic String toString() { ... }@Overridepublic boolean equals(Object o) { ... }@Overridepublic int hashCode() { ... }
}

 2. @NotNull(Java Bean Validation 提供)

  • 表示字段或参数不能为 null
  • 常用于接口参数校验,通常配合 @Valid 使用。
  • 只在运行时生效(如 Spring MVC 校验)。

示例:

@PostMapping("/users")
public void createUser(@Valid @RequestBody UserDTO userDTO) {// 如果 userDTO.username == null,会抛出 MethodArgumentNotValidException
}

区别总结

特性@Data@NotNull
来源LombokJava Bean Validation (javax.validation.constraints)
生效阶段编译期运行时
是否阻止 null✅(但仅在校验上下文中)
是否适用于 setter 方法
是否适用于构造函数

第二章:常见冲突场景详解(共 15 个)


场景 1:使用无参构造器创建对象导致字段为 null

问题代码:

@Data
public class User {@NotNull(message = "用户名不能为空")private String username;
}User user = new User(); // username == null

解决方案:

方案一:添加有参构造器

@Data
public class User {@NotNull(message = "用户名不能为空")private String username;public User(String username) {this.username = Objects.requireNonNull(username, "用户名不能为空");}
}

方案二:使用 Lombok 的 @NonNull

import lombok.NonNull;@Data
public class User {@NonNullprivate String username;
}

@NonNull 是编译期插入空值检查,会在生成的 setter 和构造函数中自动加入非空判断。


场景 2:调用 setter 方法传入 null 值

问题代码:

user.setUsername(null); // 不会触发 @NotNull 校验

解决方案:

 手动重写 setter 方法

public class User {@NotNull(message = "用户名不能为空")private String username;public void setUsername(String username) {this.username = Objects.requireNonNull(username, "用户名不能为空");}
}

或者使用 @Setter(AccessLevel.NONE) + 自定义 setter

import lombok.Data;
import lombok.Setter;@Data
public class User {@Setter(AccessLevel.NONE)@NotNull(message = "用户名不能为空")private String username;public void setUsername(String username) {this.username = Objects.requireNonNull(username, "用户名不能为空");}
}

 场景 3:Spring Boot 接口未启用校验导致无效约束

问题代码:

@PostMapping("/users")
public void createUser(@RequestBody UserDTO userDTO) {System.out.println(userDTO.getUsername());
}

即使 username == null,也不会报错。

解决方案:

启用 @Valid

@PostMapping("/users")
public void createUser(@Valid @RequestBody UserDTO userDTO) {...
}

 添加全局异常处理器

@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)@ResponseBodypublic String handleValidationErrors(MethodArgumentNotValidException ex) {return ex.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(", "));}
}

场景 4:字段类型为基本类型(如 int),无法为 null,但仍被标记为 @NotNull

问题代码:

@NotNull
private int age;

分析:

  • int 类型不能为 null,所以 @NotNull 没有意义。
  • 如果数据库字段允许为 NULL,应使用包装类型 Integer

正确做法:

@NotNull(message = "年龄不能为空")
private Integer age;

场景 5:JSON 反序列化时忽略字段非空校验

问题代码:

{"username": null
}

反序列化为:

User user = objectMapper.readValue(json, User.class);

不会触发 @NotNull 校验。

 解决方案:

  • 在 Controller 中使用 @Valid 触发校验;
  • 或者在 DTO 中统一使用 @JsonInclude(Include.NON_NULL) 过滤 null 字段。

 场景 6:构建复杂对象时 Builder 允许字段为 null

问题代码:

User.builder().age(25).build(); // username == null

解决方案:

重写 build() 方法进行校验:

@Builder
public class User {private String username;private Integer age;public static class UserBuilder {public User build() {if (username == null) {throw new IllegalArgumentException("用户名不能为空");}return new User(this);}}
}

 场景 7:Optional 字段误加 @NotNull 导致混淆

问题代码:

@NotNull
private Optional<String> nickname;

分析:

  • Optional 本身就表示“可能为空”,加上 @NotNull 易造成误解。

正确做法:

private Optional<@NotNull String> nickname; // 表示 Optional 内容必须非空

场景 8:MyBatis Plus 查询结果返回 null 字段未处理

问题代码:

User user = userService.getById(1L); // username == null

解决方案:

  • 查询后手动判断字段是否为空;
  • 或者使用封装器统一处理。

 场景 9:前后端交互中字段缺失导致接口失败

问题 JSON:

{"email": "john@example.com"
}

缺少 username 字段,反序列化为 null,接口执行失败。

解决方案:

  • 后端使用 @Valid + @NotNull 强制字段存在;
  • 前端做好表单必填项控制;
  • 提供清晰的错误提示信息。

场景 10:使用 MapStruct 映射实体时忽略字段校验

问题代码:

@Mapper
public interface UserMapper {User toEntity(UserDTO dto);
}

解决方案:

  • 在映射后手动校验;
  • 或者使用 @Valid 包裹整个流程。

场景 11:字段允许为 “空字符串” 但不允许为 null

问题代码:

@NotNull(message = "昵称不能为空")
private String nickname;

前端传了 "",通过校验,但逻辑上仍需处理。

 正确做法:

使用 @NotBlank 替代:

@NotBlank(message = "昵称不能为空且不能全为空格")
private String nickname;

场景 12:嵌套对象校验失效

问题代码:

public class UserDTO {@NotNullprivate Address address;
}public class Address {@NotNullprivate String street;
}

如果只对 UserDTO 使用 @ValidAddress.street 的校验不会触发。

正确做法:

确保使用 @Valid 注解嵌套对象:

public class UserDTO {@Valid@NotNullprivate Address address;
}

场景 13:集合字段校验失效

问题代码:

@NotNull
private List<User> users;

传入空数组 [],不触发异常。

 正确做法:

使用 @NotEmpty

@NotEmpty(message = "用户列表不能为空")
private List<User> users;

场景 14:使用 @Validated 实现分组校验

问题背景:

希望根据不同的业务场景启用不同的校验规则。

解决方案:

定义校验分组:

public interface CreateGroup {}
public interface UpdateGroup {}

使用分组:

public class UserDTO {@NotNull(groups = CreateGroup.class)private String username;@NotNull(groups = UpdateGroup.class)private Long id;
}

Controller 中使用:

@PostMapping("/users")
public void createUser(@Validated(CreateGroup.class) @RequestBody UserDTO userDTO) {...
}

场景 15:自定义校验注解

问题背景:

希望实现更复杂的校验逻辑,例如:

  • 用户名不能以数字开头
  • 邮箱必须符合企业邮箱格式

 解决方案:

1. 创建自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UsernameValidator.class)
public @interface ValidUsername {String message() default "用户名不符合规范";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}
2. 实现校验器
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {return value != null && !Character.isDigit(value.charAt(0));}
}
3. 使用注解
@ValidUsername
private String username;

第三章:最佳实践总结

场景推荐做法
必须非空字段使用 @NonNull(Lombok)或手动构造器/Setter
接口参数校验使用 @Valid + @NotNull
构建对象使用 @Builder 并重写 build() 方法
可为空字段使用 Optional<T> 类型
Spring Boot 项目引入 spring-boot-starter-validation
数据库映射手动判断字段是否为 null
前后端交互后端强制校验,前端配合表单验证
日志输出使用 @ToString(exclude = {...}) 避免敏感字段打印
复杂校验使用自定义注解或 AOP 实现

第四章:拓展知识点

1. @NotNull vs @NotBlank vs @NotEmpty

注解类型是否允许空字符串是否允许空白字符是否允许 null
@NotNull通用
@NotBlankString
@NotEmpty集合、数组、Map、String

2. @Valid vs @Validated

特性@Valid@Validated
支持分组校验
支持类级别校验
支持 AOP 校验
注解位置方法参数上类和方法上均可

3. Spring Validation 校验流程图:

    Controller 层 → @Valid → Validator → ConstraintViolationException → 全局异常处理

http://www.xdnf.cn/news/980785.html

相关文章:

  • 火山引擎大模型系列都有什么内容
  • 从认识AI开始-----生成对抗网络(GAN):通过博弈机制,引导生成
  • 博客:基本框架设计(下)
  • 牛市与熊市:市场周期的双面镜
  • 力扣上C语言编程题:最大子数组和(涉及数组)
  • TikTok数据采集软件避坑指南:代理/IP轮换/账号封禁问题一站解决
  • CAMEL中涉及获取 GOOGLE_API_KEY 和 SEARCH_ENGINE_ID 这两个值
  • 【时时三省】(C语言基础)寄存器变量( register变量)和全局变量的存储类别
  • 曼昆《经济学原理》第九版 第十七章寡头垄断
  • (简单介绍)反事实场景counterfactual scenarios
  • 树莓派超全系列教程文档--(63)rpicam-apps可用选项介绍之常用选项
  • 绝缘胶垫怎么选择,耐压、防滑、厚度、质量参数如何选择?
  • [学习] C语言结构体与联合体的对比分析
  • 网络层 IP协议(第一部分)
  • Web前端基础之HTML
  • 通过Docker和内网穿透技术在Linux上搭建远程Logseq笔记系统
  • 对比学习(Contrastive Learning)方法详解
  • Docker Swarm overlay 和 docker_gwbridge
  • 我们来学mysql -- keepalive主从高可用
  • 线 性 数 据 结 构 双 雄:栈 与 队 列 的 原 理、实 现 与 应 用
  • K8S多维度问题排查
  • argocd部署cli工具并添加k8s集群
  • Spring cloud-k8s容器化部署
  • 6.10【Q】网络安全期末复习
  • 动态多目标进化算法:VARE(Vector Autoregressive Evolution)求解DF1-DF14,提供完整MATLAB代码
  • 线程(下)【Linux操作系统】
  • 鸿蒙Next仓颉语言开发实战教程:订单列表
  • 削皮刨结构化网格划分
  • Ubuntu 24.04 systemd-journald日志系统 journalctl 查看日志
  • 与AI联手,ModbusTCP 转Ethercat控制系统升级解决刚需新思路