万字详解 Lombok 构造方法注解:@AllArgsConstructor 非空校验实现与最佳实践

一、引言:为什么需要关注 @AllArgsConstructor 的非空校验?

Lombok 作为 Java 开发的 "效率神器",通过注解自动生成模板代码,大幅减少了 POJO 类中 getter/setter、构造方法等冗余代码。但在使用@AllArgsConstructor(全参构造方法注解)时,新手常遇到两个核心问题:

  1. 自动生成的全参构造默认无任何参数校验,传入null会导致后续业务逻辑出现空指针异常;
  2. 不清楚如何在 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 提供的参数校验注解,标记在字段上时,会对两类方法生效:

  1. 构造方法:@AllArgsConstructor生成的全参构造中,自动为@NonNull字段添加null检查;
  2. 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 实现思路

  1. 保留@Data@NoArgsConstructor(保证基础能力和框架兼容性);
  2. 移除@AllArgsConstructor(避免 Lombok 生成默认全参构造,与手动构造冲突);
  3. 手动编写全参构造方法,在方法内实现自定义校验逻辑;
  4. 校验逻辑需覆盖:非空、格式、范围、业务规则等。

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 核心优势

  1. 构建器模式:创建对象时无需关注参数顺序,可读性更高;
  2. 校验逻辑解耦:将校验抽离为独立方法,避免构造方法臃肿;
  3. 兼容 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 不会覆盖手动编写的构造方法,优先级:手动构造 > 注解生成构造。因此:

  1. 手动编写全参构造后,@AllArgsConstructor会失效;
  2. 如需保留 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 等框架

核心结论

  1. Lombok 的@AllArgsConstructor本身无校验能力,需结合@NonNull实现基础 null 校验;
  2. 复杂校验场景需放弃 "全自动生成",通过手动构造或构建器模式实现;
  3. 实体类开发的黄金组合:@Data + @NoArgsConstructor,按需添加@AllArgsConstructor/@Builder
  4. 校验逻辑需遵循 "早校验、早失败" 原则,在对象创建阶段拦截非法参数,避免后续业务逻辑出错。

八、扩展:与 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 项目的最佳实践。

相关推荐
Mintopia1 小时前
现代 CSS 使用技巧(进阶篇):从布局到性能的实战方法
前端·css
_日拱一卒1 小时前
LeetCode(力扣):二叉树的前序遍历
java·数据结构·算法·leetcode
换个网名有点难1 小时前
Openclaw中NODE踩坑,NPM、PNPM和CNPM有什么区别
前端·npm·node.js
catchadmin2 小时前
Chrome DevTools MCP 让 AI 无缝接管浏览器调试会话
前端·chrome·chrome devtools
吠品2 小时前
SQL Server 2012日志文件管理:解决过大问题的全面指南
服务器·数据库·oracle
Cobyte2 小时前
30行代码,一个循环:这就是AI Agent的核心秘密—Agent Loop
前端·后端·aigc
熙胤2 小时前
【MySQL】数据库和表的操作
数据库·mysql·oracle
掘根2 小时前
【即时通讯系统】环境搭建7——ODB
数据库·oracle
爱敲代码的小鱼2 小时前
springboot案例:
java·spring boot·后端