一、引言:为什么需要关注 @AllArgsConstructor 的非空校验?
Lombok 作为 Java 开发的 "效率神器",通过注解自动生成模板代码,大幅减少了 POJO 类中 getter/setter、构造方法等冗余代码。但在使用@AllArgsConstructor(全参构造方法注解)时,新手常遇到两个核心问题:
- 自动生成的全参构造默认无任何参数校验,传入
null会导致后续业务逻辑出现空指针异常; - 不清楚如何在 Lombok 自动生成的构造方法中优雅实现非空校验,或自定义复杂校验逻辑。
本文将从基础用法到进阶实战,全面讲解@AllArgsConstructor的非空校验实现方式,结合@NonNull、手动构造方法等方案,给出不同场景下的最佳实践,同时补充 Lombok 构造方法相关注解的核心知识点,帮助开发者避坑。
二、前置知识:Lombok 构造方法核心注解梳理
在讲解非空校验前,先明确 Lombok 中构造方法相关注解的核心定位,避免概念混淆:
| 注解 | 核心能力 | 生成规则 | 适用场景 |
|---|---|---|---|
@NoArgsConstructor |
生成无参构造方法 | 无参数,仅初始化对象(final 字段需有默认值) | 实体类(适配 MyBatis/Jackson 反序列化) |
@RequiredArgsConstructor |
生成 "必需参数" 构造方法 | 仅包含final/@NonNull字段,自动为@NonNull字段做 null 校验 |
Spring 服务类(构造器注入) |
@AllArgsConstructor |
生成全参构造方法 | 包含类中所有字段,参数顺序 = 字段声明顺序,默认无任何校验 | 需要批量赋值的场景 |
@Data |
组合注解(隐含@RequiredArgsConstructor) |
生成 getter/setter/toString 等,构造方法规则同@RequiredArgsConstructor |
实体类基础注解 |
关键结论 :@AllArgsConstructor是唯一能生成 "包含所有字段" 构造方法的注解,但默认无校验能力,需结合其他手段实现参数合法性检查。
三、基础方案:@AllArgsConstructor + @NonNull 实现轻量非空校验
3.1 核心原理
@NonNull是 Lombok 提供的参数校验注解,标记在字段上时,会对两类方法生效:
- 构造方法:
@AllArgsConstructor生成的全参构造中,自动为@NonNull字段添加null检查; - Setter 方法:
@Data生成的 setter 方法中,同样会校验@NonNull字段,避免通过 setter 传入null。
3.2 完整代码示例
3.2.1 实体类定义(含注解组合)
java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
/**
* 用户实体类(基础非空校验示例)
* 注解组合:@Data + @NoArgsConstructor(必加) + @AllArgsConstructor + @NonNull
*/
@Data // 生成getter/setter/toString/equals/hashCode
@NoArgsConstructor // 保证框架反序列化可用,实体类必加
@AllArgsConstructor // 生成全参构造方法
public class User {
// 普通字段:无校验
private Long id;
// @NonNull标记:全参构造/setter会做null校验
@NonNull
private String username;
// @NonNull标记:全参构造/setter会做null校验
@NonNull
private String phone;
// 普通字段:无校验
private Integer age;
}
3.2.2 Lombok 自动生成的等价代码
java
public class User {
private Long id;
private String username;
private String phone;
private Integer age;
// 无参构造(@NoArgsConstructor生成)
public User() {}
// 全参构造(@AllArgsConstructor生成,含@NonNull校验)
public User(Long id, String username, String phone, Integer age) {
// 自动为@NonNull字段添加null校验
if (username == null) {
throw new NullPointerException("username is marked non-null but is null");
}
if (phone == null) {
throw new NullPointerException("phone is marked non-null but is null");
}
this.id = id;
this.username = username;
this.phone = phone;
this.age = age;
}
// getter/setter(@Data生成,setter含@NonNull校验)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) {
if (username == null) {
throw new NullPointerException("username is marked non-null but is null");
}
this.username = username;
}
public String getPhone() { return phone; }
public void setPhone(String phone) {
if (phone == null) {
throw new NullPointerException("phone is marked non-null but is null");
}
this.phone = phone;
}
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
// toString/equals/hashCode(@Data生成)
@Override
public String toString() { /* 省略实现 */ }
@Override
public boolean equals(Object o) { /* 省略实现 */ }
@Override
public int hashCode() { /* 省略实现 */ }
}
3.3 测试验证(核心场景)
java
public class UserTest {
public static void main(String[] args) {
// 场景1:@NonNull字段传null → 抛NullPointerException
try {
User user1 = new User(1L, null, "13800138000", 20);
} catch (NullPointerException e) {
System.out.println(e.getMessage()); // 输出:username is marked non-null but is null
}
// 场景2:多个@NonNull字段传null → 优先校验先声明的字段
try {
User user2 = new User(2L, null, null, 25);
} catch (NullPointerException e) {
System.out.println(e.getMessage()); // 输出:username is marked non-null but is null
}
// 场景3:Setter方法传入null → 同样触发校验
try {
User user3 = new User(3L, "zhangsan", "13800138000", 30);
user3.setPhone(null);
} catch (NullPointerException e) {
System.out.println(e.getMessage()); // 输出:phone is marked non-null but is null
}
// 场景4:所有@NonNull字段非空 → 正常创建对象
User user4 = new User(4L, "lisi", "13900139000", 35);
System.out.println(user4); // 输出:User(id=4, username=lisi, phone=13900139000, age=35)
}
}
3.4 该方案的优缺点
| 优点 | 缺点 |
|---|---|
| 零代码侵入:仅需加注解,无需手动写构造 | 仅支持 null 校验,无法处理空白字符串、格式校验等 |
| 自动生效:构造 + setter 双场景校验 | 异常信息固定,无法自定义提示文案 |
| 代码简洁:保留 Lombok 的核心优势 | 校验逻辑不可扩展,仅能满足基础需求 |
四、进阶方案:手动编写全参构造实现自定义校验
当需要更复杂的校验逻辑(如空白字符串、格式校验、范围校验),或自定义异常提示时,需放弃@AllArgsConstructor,手动编写全参构造方法。
4.1 实现思路
- 保留
@Data和@NoArgsConstructor(保证基础能力和框架兼容性); - 移除
@AllArgsConstructor(避免 Lombok 生成默认全参构造,与手动构造冲突); - 手动编写全参构造方法,在方法内实现自定义校验逻辑;
- 校验逻辑需覆盖:非空、格式、范围、业务规则等。
4.2 完整代码示例
java
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户实体类(自定义复杂校验示例)
* 注解组合:@Data + @NoArgsConstructor(放弃@AllArgsConstructor)
*/
@Data
@NoArgsConstructor
public class User {
private Long id;
private String username;
private String phone;
private Integer age;
/**
* 手动编写全参构造方法,实现自定义校验
* @param id 用户ID
* @param username 用户名(非空、非空白)
* @param phone 手机号(非空、符合11位手机号格式)
* @param age 年龄(非空时需在0-150之间)
*/
public User(Long id, String username, String phone, Integer age) {
// 1. 用户名校验:非空 + 非空白
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("【用户名校验失败】用户名不能为空且不能为空白字符串");
}
if (username.length() < 2 || username.length() > 20) {
throw new IllegalArgumentException("【用户名校验失败】用户名长度需在2-20个字符之间");
}
// 2. 手机号校验:非空 + 格式匹配
if (phone == null) {
throw new IllegalArgumentException("【手机号校验失败】手机号不能为空");
}
// 11位手机号正则:以1开头,第二位为3-9,后9位为数字
String phoneRegex = "^1[3-9]\\d{9}$";
if (!phone.matches(phoneRegex)) {
throw new IllegalArgumentException("【手机号校验失败】手机号格式不正确,需为11位有效手机号");
}
// 3. 年龄校验:非空时范围限制
if (age != null) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("【年龄校验失败】年龄需在0-150之间");
}
}
// 4. 赋值(所有校验通过后执行)
this.id = id;
this.username = username;
this.phone = phone;
this.age = age;
}
}
4.3 测试验证(复杂场景)
java
public class UserCustomTest {
public static void main(String[] args) {
// 场景1:用户名空白 → 抛自定义IllegalArgumentException
try {
User user1 = new User(1L, " ", "13800138000", 20);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出:【用户名校验失败】用户名不能为空且不能为空白字符串
}
// 场景2:用户名长度超限 → 抛自定义异常
try {
User user2 = new User(2L, "a", "13800138000", 25);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出:【用户名校验失败】用户名长度需在2-20个字符之间
}
// 场景3:手机号格式错误 → 抛自定义异常
try {
User user3 = new User(3L, "zhangsan", "123456", 30);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出:【手机号校验失败】手机号格式不正确,需为11位有效手机号
}
// 场景4:年龄超出范围 → 抛自定义异常
try {
User user4 = new User(4L, "lisi", "13900139000", 200);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出:【年龄校验失败】年龄需在0-150之间
}
// 场景5:所有校验通过 → 正常创建对象
User user5 = new User(5L, "wangwu", "13700137000", 35);
System.out.println(user5); // 输出:User(id=5, username=wangwu, phone=13700137000, age=35)
}
}
4.4 该方案的优缺点
| 优点 | 缺点 |
|---|---|
| 校验能力无上限:支持任意复杂逻辑 | 需手动编写构造方法,失去 Lombok "零代码" 优势 |
| 异常信息自定义:提示更友好,便于问题定位 | 字段新增 / 删除时,需同步修改构造方法参数 |
| 支持多维度校验:非空、格式、范围、业务规则 | 代码量增加,需维护校验逻辑 |
五、混合方案:@Builder + 自定义校验(推荐生产环境)
对于生产环境,推荐使用@Builder(构建器模式)替代@AllArgsConstructor,结合@NonNull和自定义校验方法,兼顾 "代码简洁" 和 "校验灵活"。
5.1 核心优势
- 构建器模式:创建对象时无需关注参数顺序,可读性更高;
- 校验逻辑解耦:将校验抽离为独立方法,避免构造方法臃肿;
- 兼容 Lombok:保留
@Data的基础能力,同时实现自定义校验。
5.2 完整代码示例
java
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* 用户实体类(Builder + 自定义校验,生产环境推荐)
*/
@Data
@NoArgsConstructor // 保证框架反序列化
@AllArgsConstructor // 配合@Builder使用
@Builder // 生成构建器
public class User {
private Long id;
private String username;
private String phone;
private Integer age;
/**
* 构建对象前的自定义校验(Builder模式专用)
*/
public static void validateUser(User user) {
// 用户名校验
if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空且不能为空白");
}
if (user.getUsername().length() < 2 || user.getUsername().length() > 20) {
throw new IllegalArgumentException("用户名长度需在2-20个字符之间");
}
// 手机号校验
if (user.getPhone() == null) {
throw new IllegalArgumentException("手机号不能为空");
}
String phoneRegex = "^1[3-9]\\d{9}$";
if (!user.getPhone().matches(phoneRegex)) {
throw new IllegalArgumentException("手机号格式不正确");
}
// 年龄校验
if (user.getAge() != null && (user.getAge() < 0 || user.getAge() > 150)) {
throw new IllegalArgumentException("年龄需在0-150之间");
}
}
}
// 使用示例
class UserBuilderTest {
public static void main(String[] args) {
// 构建对象并执行校验
User user = User.builder()
.id(1L)
.username("zhangsan")
.phone("13800138000")
.age(25)
.build();
// 执行自定义校验
User.validateUser(user);
System.out.println(user); // 正常输出
// 错误场景:手机号格式错误
try {
User errorUser = User.builder()
.id(2L)
.username("lisi")
.phone("123456")
.age(30)
.build();
User.validateUser(errorUser);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出:手机号格式不正确
}
}
}
六、避坑指南:Lombok 构造方法注解的核心注意事项
6.1 实体类必须保留 @NoArgsConstructor
MyBatis、Jackson、Spring 等框架在反序列化 / 实例化对象时,默认依赖无参构造方法。如果仅使用@Data + @AllArgsConstructor,会导致框架报错:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of 'com.example.User' (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
解决方案 :实体类固定组合@Data + @NoArgsConstructor,按需添加@AllArgsConstructor。
6.2 @NonNull 的校验范围
@NonNull仅校验 "是否为 null",不校验 "空字符串""0 值" 等:
// 以下代码不会触发@NonNull校验(username是空字符串,不是null)
User user = new User(1L, "", "13800138000", 20);
解决方案:空字符串 / 格式校验需手动实现。
6.3 手动构造方法与 Lombok 注解的兼容
Lombok 不会覆盖手动编写的构造方法,优先级:手动构造 > 注解生成构造。因此:
- 手动编写全参构造后,
@AllArgsConstructor会失效; - 如需保留 Lombok 生成的构造,需避免手动编写同参数列表的构造方法。
6.4 final 字段的特殊处理
如果字段被final修饰,@NoArgsConstructor生成无参构造时,final字段必须有默认值,否则编译报错:
java
// 错误示例:final字段无默认值 + @NoArgsConstructor
@Data
@NoArgsConstructor
public class User {
private final String username; // 编译报错:final字段未初始化
}
// 正确示例:final字段加默认值
@Data
@NoArgsConstructor
public class User {
private final String username = "default"; // 有默认值,编译通过
}
七、总结:不同场景的方案选型
| 场景 | 推荐方案 | 核心优势 |
|---|---|---|
| 仅需基础 null 校验 | @AllArgsConstructor + @NonNull |
代码简洁,零侵入 |
| 需要复杂校验(格式 / 范围) | 手动编写全参构造 + 自定义校验 | 校验能力全面,异常信息友好 |
| 生产环境(可读性 + 扩展性) | @Builder + 独立校验方法 |
构建对象更灵活,校验逻辑解耦 |
| 实体类(框架兼容) | @Data + @NoArgsConstructor 必加 |
适配 MyBatis/Jackson 等框架 |
核心结论
- Lombok 的
@AllArgsConstructor本身无校验能力,需结合@NonNull实现基础 null 校验; - 复杂校验场景需放弃 "全自动生成",通过手动构造或构建器模式实现;
- 实体类开发的黄金组合:
@Data + @NoArgsConstructor,按需添加@AllArgsConstructor/@Builder; - 校验逻辑需遵循 "早校验、早失败" 原则,在对象创建阶段拦截非法参数,避免后续业务逻辑出错。
八、扩展:与 Spring Validation 的结合
如果是 Spring 项目,推荐将字段校验与jakarta.validation(JSR-380)结合,替代手动校验:
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Range;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
@NotBlank(message = "用户名不能为空")
@Pattern(regexp = "^.{2,20}$", message = "用户名长度需在2-20个字符之间")
private String username;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Range(min = 0, max = 150, message = "年龄需在0-150之间")
private Integer age;
}
// Spring中使用
// @Valid注解触发校验
// public ResponseEntity<?> createUser(@Valid @RequestBody User user) { ... }
该方案可实现 "注解式校验",无需手动编写校验逻辑,是 Spring 项目的最佳实践。