第一章:核心概念解析
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 |
---|---|---|
来源 | Lombok | Java 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
使用 @Valid
,Address.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 | 通用 | ✅ | ✅ | ❌ |
@NotBlank | String | ❌ | ❌ | ❌ |
@NotEmpty | 集合、数组、Map、String | ❌ | ✅ | ❌ |
2. @Valid
vs @Validated
特性 | @Valid | @Validated |
---|---|---|
支持分组校验 | ❌ | ✅ |
支持类级别校验 | ✅ | ✅ |
支持 AOP 校验 | ❌ | ✅ |
注解位置 | 方法参数上 | 类和方法上均可 |
3. Spring Validation 校验流程图:
Controller 层 → @Valid
→ Validator → ConstraintViolationException → 全局异常处理