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.pswp.cn/diannao/86671.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

离线部署openstack 2024.1 glance

控制节点镜像服务 离线下载 apt install --download-only glancemkdir /controller/glance mv /var/cache/apt/archives/*.deb /controller/glance/ dpkg -i /controller/glance/*.deb在一个控制节点操作 CREATE DATABASE glance; GRANT ALL PRIVILEGES ON glance.* TO glan…

.NET AOT 详解

简介 AOT&#xff08;Ahead-Of-Time Compilation&#xff09;是一种将代码直接编译为机器码的技术&#xff0c;与传统的 JIT&#xff08;Just-In-Time Compilation&#xff09;编译方式形成对比。在.NET 中&#xff0c;AOT 编译可以在应用发布时将 IL&#xff08;中间语言&…

博客系统自动化测试

基于SSM&#xff08;Spring Spring MVC MyBatis&#xff09;框架构建的个人博客系统&#xff0c;通过分层架构实现高效协作&#xff1a;Spring负责依赖注入与事务管理&#xff0c;Spring MVC处理HTTP请求分发&#xff0c;MyBatis完成数据持久化操作。系统包含以下核心功能模块…

animate.css详解:轻松实现网页动画效果

前言 在网页设计中&#xff0c;动画效果不仅仅是视觉上的装饰&#xff0c;更是提升用户体验的重要元素。animate.css 作为一个轻量级的 CSS 动画库&#xff0c;提供了丰富的预设动画效果&#xff0c;本文将探讨 animate.css 使用方法以及在实际项目中的应用案例&#xff0c;帮助…

【多智能体】基于嵌套进化算法的多代理工作流

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;本专栏《人工智能》旨在记录最新的科研前沿&#xff0c;包括大模型、具身智能、智能体等相关领域&#xff0c;期待与你一同探索、学习、进步&#xff0c;一起卷起来叭&#xff…

电源知多少?LDO VS DCDC((下)

首先补充几个上一节没有提到的知识&#xff0c;我们通常说的DCDC同步整流是指什么&#xff1f; 同步是指采用通态电阻极低的专用功率MOS来取代整流二极管以降低整流损耗&#xff0c;&#xff0c;但是同步整流有以下两点需要注意&#xff1a;1、MOS在导通之后的压降比较低&…

数组方法_push()/pop()/数组方法_shift()/unshift()

push 方法用于在数组的末端添加一个或多个元素&#xff0c;并返回添加新元 素后的数组长度。注意&#xff0c;该方法会改变原数组 var arr [];arr.push("颤三") // 1arr.push(itbaizhan) // 2arr.push(true, {}) // 4arr // [颤三 , itbaizhan, true, {}] pop 方法用…

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…

多头与空头:市场博弈的两面

在金融市场中&#xff0c;多头&#xff08;Bull&#xff09;和空头&#xff08;Bear&#xff09;代表两种截然相反的投资策略&#xff0c;它们的博弈构成了市场价格波动的核心动力。 1. 概念对比&#xff1a;看涨与看跌的本质区别 多头&#xff08;Bull&#xff09;&#xff0…

Excel 发现此工作表中有一处或多处公式引用错误。请检查公式中的单元格引用、区域名称、已定义名称以及到其他工作簿的链接是否均正确无误。弹窗

Excel 提示“发现此工作表中有一处或多处公式引用错误”通常表示公式中存在无效引用。以下是系统化的检查步骤&#xff0c;帮助你定位和修复问题&#xff1a; 1. 检查单元格引用&#xff1a; 无效单元格引用&#xff1a;检查公式中的单元格地址&#xff08;如 A1、B10&…

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …

【指针】(适合考研、专升本)

指针 &与*是两个作用相反的运算符。 二级指针只能保存一级指针变量的地址和指向指针数组&#xff0c;其余情况不考虑。 int *p[2];int a12;int b15;*p&a;*(p1)&b;printf("%d\n%d\n",**p,**(p1));int **rp;printf("%d\n",**r); 普遍变量…

电路图识图基础知识-行程开关自动往返运行控制电路详解(二十三)

行程开关自动往返运行控制电路详解 在机床设备运行中&#xff0c;部分工作台需在特定距离内自动往复循环&#xff0c;行程开关自动往返运行控制电路可实现该功能&#xff0c;通过行程开关自动控制电动机正反转&#xff0c;保障工作台有序运动&#xff0c;以下展开详细解析。 …

SpringBoot学习day1-SpringBoot的简介与搭建

springboot回顾springspringbootspringboot搭建&#xff08;新闻为例&#xff09;springboot中的配置文件spring集成jdbc,mybatis,阿里巴巴数据源**SpringBoot 集成日志功能**(了解)常用日志组件日志级别 springboot统一异常处理 springboot 回顾spring spring是一个轻量级的…

【牛客小白月赛117】E题——种类数小结

1 初步想法 1.1 前置知识&#xff1a;vector数组的去重操作 unique()将不重复的元素放在数组前面&#xff0c;重复元素移到后面&#xff0c;qs获取不重复元素的后一个位置&#xff0c;之后用erase()函数去除重复元素。 qsunique(a.begin()1,a.begin()k1); a.erase(qs,a.end(…

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…

MatAnyone本地部署,视频分割处理,绿幕抠像(WIN/MAC)

大家好&#xff0c;今天要和大家分享的项目是MatAnyone&#xff0c;与上一篇分享的SAM2LONG类似&#xff0c;不过上次的分享没有提到如何在 MAC 上部署&#xff0c;后来有小伙伴私信说希望能出一个 MAC 版本的。那正好看到MatAnyone这个项目顺手就写下来。该项目基于SAM2同样可…

记录下blog的成长过程

2025-06-11 新人榜83 2025-06-09 新人榜87 北京市原力月榜 80

C语言学习20250611

指针 指针类型 int p;》普通的整形变量int *p;》p先与*结合&#xff0c;表示p为指针&#xff0c;该指针指向的内容的数据类型为整型int p[3];》p为一个由整型数据组成的数组int *p[3];》因为[]比*优先级高&#xff0c;p先与方括号结合&#xff0c;所以p为一个数组&#xff0c…

【AI智能体】Dify 从部署到使用操作详解

目录 一、前言 二、Dify 介绍 2.1 Dify 是什么 2.2 Dify 核心特性 2.2.1 多模型支持 2.2.2 可视化编排工作流 2.2.3 低代码/无代码开发 2.3 Dify 适用场景 2.4 Dify 与Coze的对比 2.4.1 定位与目标用户 2.4.2 核心功能对比 2.4.3 开发体验与成本 2.4.4 适用场景对比…