当前位置: 首页 > 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 → 全局异常处理

相关文章:

  • StackOverflowError
  • spring:使用注解@获取第三方bean实例
  • 表格里的图片链接怎么变成图片【附工具+源码演示】
  • 如何彻底删除Neo4j中的所有数据:完整指南
  • Java八股文——Spring「Spring 篇」
  • 2024蓝桥杯C/C++ B组国赛
  • EtherCAT转CANopen网关实现与伺服系统连通的配置实例探究
  • Spring Cache+Redis缓存方案 vs 传统redis缓存直接使用RedisTemplate 方案对比
  • Oracle集群OCR磁盘组掉盘问题处理
  • git pull 和 git fecth 的区别,远程仓库创建了新分支,可以用git fetch更新,可以看到远程创建的新分支
  • K8S中应用无法获取用户真实ip问题排查
  • 基于微信小程序的天气预报app
  • Vue 数据代理机制实现
  • BYC8-1200PQ超快二极管!光伏逆变/快充首选,35ns极速恢复,成本直降20%!
  • 3-16单元格区域尺寸调整(发货单记录保存-方法2)学习笔记
  • 3-15单元格偏移设置(发货单记录保存-方法1)学习笔记
  • 云原生核心技术 (12/12): 终章:使用 GitLab CI 将应用自动部署到 K8s (保姆级教程)
  • 力扣-121.买卖股票的最佳时机
  • Linux常用命令详解
  • 【PmHub面试篇】集成 Sentinel+OpenFeign实现网关流量控制与服务降级相关面试题解答
  • 成都网站的优化/重庆seo网站建设
  • 国内互动网站建设/泉州seo代理计费
  • 福州营销型网站建设/青岛seo招聘
  • 金湖网站建设/友情链接怎么弄
  • 苏州高端网站建设/真实的优化排名
  • 河南移动商城网站建设/自动连点器