一、核心注解基础细节(4 个构造相关注解,逐个拆透)
先明确 4 个核心注解的 "底层定义 + 生成规则 + 细节补充",每个注解都包含 "基础作用 + 生成逻辑 + 隐藏细节 + 代码示例 + 等价手动代码",不跳过任何一个细节。
1. @RequiredArgsConstructor(重点中的重点,Spring 开发必用)
1.1 底层定义(精准无歧义)
Lombok 提供的 "必需参数构造方法生成注解",核心作用是:仅为类中 "必须赋值的字段" 生成构造方法,且自动为 @NonNull 字段添加非空校验,无需手动编写任何代码。
1.2 关键细节 1:什么是 "必须赋值的字段"(仅 2 种,无第三种)
注解只会识别以下两类字段作为构造方法的参数,普通字段(既非 final 也非 @NonNull)绝对不会出现在构造参数中,这是核心细节,必须记死:
- 字段 1:被 final 修饰的字段
- 细节 1:final 字段的特性是 "必须在对象创建时完成赋值"(要么直接给默认值,要么通过构造方法赋值),因此 @RequiredArgsConstructor 会将其视为 "必需字段";
- 细节 2:如果 final 字段已经直接赋值(如
private final int age = 18;),则不会出现在构造方法参数中(因为已经完成赋值,无需通过构造方法传入); - 细节 3:如果 final 字段没有默认值,且未被 @RequiredArgsConstructor 纳入构造参数(比如注解未添加),则编译报错(违背 final 字段的赋值规则)。
- 字段 2:被 @NonNull 修饰的字段
- 细节 1:@NonNull 是 Lombok 的非空标记注解,作用是 "强制该字段不能为 null",因此 @RequiredArgsConstructor 会将其视为 "必需字段",必须通过构造方法传入非 null 值;
- 细节 2:@NonNull 仅作用于 "构造方法参数" 和 "setter 方法",不会影响字段本身的赋值逻辑,仅做 "非空校验";
- 细节 3:如果 @NonNull 字段同时被 final 修饰,依然会被纳入构造参数,但如果 final 有默认值,则不会纳入(优先级:final 默认值 > @NonNull)。
1.3 关键细节 2:自动生成的构造方法的特性
- 特性 1:构造方法的访问修饰符是 "public"(默认),如果需要 private/protected,可通过注解参数设置:
@RequiredArgsConstructor(access = AccessLevel.PRIVATE); - 特性 2:构造方法的参数顺序 = 类中 "必需字段" 的声明顺序(先声明的字段,先作为构造参数),与字段类型、注解顺序无关;
- 特性 3:自动为 @NonNull 字段添加非空校验逻辑,校验失败会抛出 NullPointerException,异常信息固定为 "字段名 is marked non-null but is null",无法自定义(除非手动写构造方法);
- 特性 4:Lombok 不会覆盖手动编写的构造方法,如果手动写了与 @RequiredArgsConstructor 生成的构造方法参数一致的构造方法,注解生成的构造方法会失效(优先级:手动构造 > 注解生成)。
1.4 代码示例(含所有细节场景)
场景 1:包含 final(无默认值)、@NonNull、普通字段
java
import lombok.RequiredArgsConstructor;
import lombok.NonNull;
// 注解生成必需参数构造方法,访问修饰符默认public
@RequiredArgsConstructor
public class UserService {
// 必需字段1:final修饰(无默认值)→ 纳入构造参数
private final UserMapper userMapper;
// 必需字段2:@NonNull修饰(非final)→ 纳入构造参数
@NonNull
private String serviceName;
// 必需字段3:final修饰(有默认值)→ 不纳入构造参数
private final int maxCount = 100;
// 普通字段:既非final也非@NonNull → 不纳入构造参数
private int timeout = 30;
}
1.5 等价手动代码(看清 Lombok 到底生成了什么)
java
public class UserService {
private final UserMapper userMapper;
@NonNull
private String serviceName;
private final int maxCount = 100;
private int timeout = 30;
// @RequiredArgsConstructor自动生成的构造方法
public UserService(UserMapper userMapper, String serviceName) {
// 自动为@NonNull字段添加非空校验
if (serviceName == null) {
throw new NullPointerException("serviceName is marked non-null but is null");
}
// 为final字段赋值(无默认值)
this.userMapper = userMapper;
// 为@NonNull字段赋值
this.serviceName = serviceName;
// maxCount有默认值,无需赋值;timeout是普通字段,无需赋值
}
}
1.6 实战核心场景(Spring 构造器注入,细节拉满)
这是 @RequiredArgsConstructor 最常用的场景,Spring 官方推荐 "构造器注入"(比 @Autowired 字段注入更安全、可测试性更好),注解能彻底简化代码,细节如下:
- 细节 1:Spring 构造器注入的核心要求:依赖字段用 final 修饰,且有对应的构造方法(参数包含所有 final 依赖);
- 细节 2:@RequiredArgsConstructor 会自动生成 "包含所有 final 字段" 的构造方法,因此无需手动写构造方法,也无需加 @Autowired(Spring 4.3+ 会自动识别构造器注入,无需 @Autowired);
- 细节 3:如果依赖字段没有用 final 修饰,即使加了 @RequiredArgsConstructor,也不会被纳入构造参数,无法完成注入;
- 细节 4:如果有多个 final 依赖,构造方法的参数顺序 = 依赖字段的声明顺序,Spring 会按参数类型 + 顺序自动匹配注入,无需担心顺序问题。
代码示例(实战标准写法):
java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service // Spring服务类
@RequiredArgsConstructor // 自动生成包含所有final依赖的构造方法
public class OrderService {
// 细节1:依赖用final修饰 → 纳入构造参数,完成注入
private final OrderMapper orderMapper;
private final UserService userService;
// 细节2:无需手动写构造方法,无需加@Autowired
// 细节3:如果新增final依赖,注解会自动更新构造方法,无需手动修改
// 业务方法示例
public void createOrder(Long userId) {
// 直接使用注入的依赖,无需担心空指针(构造器注入保证依赖非空)
User user = userService.getUserById(userId);
orderMapper.insertOrder(new Order(user.getId()));
}
}
对比传统手动写法(凸显注解优势):
java
@Service
public class OrderService {
private final OrderMapper orderMapper;
private final UserService userService;
// 手动写构造方法,冗余且容易漏字段
@Autowired // Spring 4.3+ 可省略,但手动写构造方法必须加
public OrderService(OrderMapper orderMapper, UserService userService) {
this.orderMapper = orderMapper;
this.userService = userService;
}
// 业务方法...
}
2. @Data(新手最易踩坑,细节全覆盖)
2.1 底层定义(精准无歧义)
Lombok 的 "组合式注解",核心作用是 "一站式生成 POJO 类的常用模板代码",本质是多个注解的集合,其中隐含 @RequiredArgsConstructor(这是新手最容易忽略的细节,也是踩坑的核心原因)。
2.2 关键细节 1:@Data 包含的所有注解(无遗漏)
@Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor,逐个拆解细节:
- @Getter:为所有字段生成 getter 方法(final 字段仅生成 getter,无 setter);
- @Setter:为所有 "非 final 字段" 生成 setter 方法(final 字段无 setter,因为 final 字段不可修改);
- @ToString:生成 toString () 方法,包含所有字段的信息(可通过 exclude 参数排除某些字段);
- @EqualsAndHashCode:生成 equals () 和 hashCode () 方法,基于所有非 static、非 transient 字段;
- @RequiredArgsConstructor:生成必需参数构造方法(规则和上文完全一致)------ 这是核心隐含逻辑,也是新手误以为 "@Data 没有构造方法" 的原因。
2.3 关键细节 2:@Data 不包含的注解(避坑核心)
无论什么场景,@Data 都不会自动生成以下两个构造方法,必须手动补充(否则踩坑):
- ❌ 无参构造方法(@NoArgsConstructor):仅当类中 "没有任何 final/@NonNull 字段" 时,@RequiredArgsConstructor 生成的构造方法会等价于无参构造(特例),否则无参构造会消失;
- ❌ 全参构造方法(@AllArgsConstructor):无论类中有没有 final/@NonNull 字段,@Data 都不会自动生成全参构造,必须手动添加注解。
2.4 新手最易踩的 3 个坑(细节拉满,附解决方案)
坑点 1:仅加 @Data,有 final/@NonNull 字段 → 无参构造丢失,框架反序列化报错
java
// 错误示例:仅加@Data
@Data
public class User {
@NonNull private String username; // @NonNull字段 → 触发@RequiredArgsConstructor
private final Integer age = 18; // final字段 → 触发@RequiredArgsConstructor
}
// 测试代码:创建无参对象 → 编译报错
User user = new User();
// 报错信息:java: 找不到符号 符号: 方法 User() 位置: 类 com.example.User
原因:@Data 隐含 @RequiredArgsConstructor,生成了 "包含 username" 的构造方法(age 有默认值,不纳入),覆盖了 JVM 默认的无参构造,导致无参构造消失。解决方案:显式添加 @NoArgsConstructor,保证无参构造存在。
坑点 2:仅加 @Data,无 final/@NonNull 字段 → 构造方法等价于无参构造,但无全参构造
java
// 示例:类中无final/@NonNull字段
@Data
public class User {
private Long id;
private String name;
private Integer age;
}
// 此时@Data生成的构造方法(等价于无参构造)
public User() {}
// 测试:无法通过全参构造创建对象 → 编译报错
User user = new User(1L, "张三", 20);
// 报错:java: 找不到符号 符号: 方法 User(java.lang.Long,java.lang.String,java.lang.Integer)
原因:@Data 隐含的 @RequiredArgsConstructor,因为没有 "必需字段",所以生成的构造方法无参数(等价于无参构造),但不会生成全参构造。解决方案:如需全参构造,显式添加 @AllArgsConstructor。
坑点 3:@Data 生成的 setter 方法,对 @NonNull 字段有非空校验,新手不知情
java
@Data
public class User {
@NonNull private String username;
}
// 测试:通过setter传入null → 报错
User user = new User("张三");
user.setUsername(null);
// 报错:java.lang.NullPointerException: username is marked non-null but is null
原因:@Data 包含 @Getter/@Setter,而 @NonNull 字段的 setter 方法会自动添加非空校验,和构造方法的校验逻辑一致。注意:如果不需要 setter 的非空校验,可手动写 setter 方法覆盖(Lombok 不会覆盖手动写的 setter)。
2.5 实体类万能写法(细节拉满,直接复用)
结合所有细节,实体类(POJO/DO/DTO)的标准写法,适配 MyBatis、Jackson 等所有框架,避免所有坑:
java
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.NonNull;
/**
* 实体类标准写法(细节说明):
* 1. @Data:生成get/set/toString等基础方法,隐含@RequiredArgsConstructor;
* 2. @NoArgsConstructor:显式生成无参构造,适配框架反序列化(必加);
* 3. @AllArgsConstructor:显式生成全参构造,按需使用(批量赋值场景);
* 4. @NonNull:给需要非空的字段加,构造方法和setter会自动校验。
*/
@Data
@NoArgsConstructor // 必加,无论什么场景
@AllArgsConstructor // 按需加,不需要可删除
public class User {
private Long id; // 普通字段,无校验
@NonNull // 非空字段,构造+setter自动校验
private String username;
private Integer age; // 普通字段,无校验
// final字段(有默认值)→ 不纳入@RequiredArgsConstructor参数
private final String defaultRole = "USER";
}
3. @NoArgsConstructor(实体类必加,细节不遗漏)
3.1 底层定义
专门生成 "无参构造方法" 的注解,无任何参数,核心作用是 "适配框架反序列化"(MyBatis、Jackson 等框架必须通过无参构造实例化对象)。
3.2 关键细节(新手易忽略)
- 细节 1:默认生成的无参构造方法是 public 访问修饰符,可通过参数设置:
@NoArgsConstructor(access = AccessLevel.PRIVATE); - 细节 2:如果类中有 final 字段,@NoArgsConstructor 生成无参构造的前提是 "final 字段必须有默认值",否则编译报错(违背 final 字段赋值规则);
- 细节 3:如果手动写了无参构造方法,@NoArgsConstructor 会失效(优先级:手动构造 > 注解生成);
- 细节 4:@NoArgsConstructor 和 @AllArgsConstructor、@RequiredArgsConstructor 可同时使用,互不冲突(Lombok 会生成多个构造方法)。
3.3 代码示例(含异常场景)
java
// 正确示例:final字段有默认值 + @NoArgsConstructor
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class User {
private final String defaultRole = "USER"; // final有默认值,编译通过
private Long id;
}
// 错误示例:final字段无默认值 + @NoArgsConstructor → 编译报错
@NoArgsConstructor
public class User {
private final String defaultRole; // final无默认值,无参构造无法赋值
private Long id;
}
// 报错:java: 变量 defaultRole 可能尚未初始化
4. @AllArgsConstructor(全参构造,含非空校验细节)
4.1 底层定义
专门生成 "包含类中所有字段" 的全参构造方法,参数顺序 = 字段在类中的声明顺序,核心作用是 "批量赋值"(手动创建对象时,一次性传入所有字段值)。
4.2 关键细节(含非空校验实现)
- 细节 1:默认无任何参数校验,即使字段加了 @NonNull,也不会自动校验(需手动配合 @NonNull 实现校验,这是核心细节);
- 细节 2:参数顺序 = 字段声明顺序,和字段类型、注解无关(比如先声明 id,再声明 name,构造参数就是 (id, name));
- 细节 3:访问修饰符默认是 public,可通过参数设置:
@AllArgsConstructor(access = AccessLevel.PRIVATE); - 细节 4:如果类中有 final 字段,全参构造方法会为 final 字段赋值(无论 final 字段是否有默认值,都会覆盖默认值);
- 细节 5:手动写全参构造方法后,@AllArgsConstructor 会失效;
- 细节 6:非空校验实现:给需要校验的字段加 @NonNull,@AllArgsConstructor 会自动为该字段添加 null 校验(和 @RequiredArgsConstructor 的校验逻辑一致)。
4.3 代码示例(含非空校验)
java
运行
java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
// 全参构造 + 非空校验 + 无参构造 + 基础方法
@Data
@NoArgsConstructor
@AllArgsConstructor // 全参构造
public class User {
private Long id; // 普通字段,无校验
@NonNull // 加@NonNull,全参构造会自动校验null
private String username;
@NonNull // 加@NonNull,全参构造会自动校验null
private String phone;
private Integer age; // 普通字段,无校验
}
4.4 等价手动代码(含校验逻辑)
java
public class User {
private Long id;
@NonNull
private String username;
@NonNull
private String phone;
private Integer age;
// 无参构造(@NoArgsConstructor生成)
public User() {}
// 全参构造(@AllArgsConstructor生成,含@NonNull校验)
public User(Long id, String username, String phone, Integer age) {
// 自动为@NonNull字段添加非空校验
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");
}
// 为所有字段赋值(包括final字段,会覆盖默认值)
this.id = id;
this.username = username;
this.phone = phone;
this.age = age;
}
// getter/setter/toString等(@Data生成)
}
4.5 复杂校验场景(自定义提示 / 格式校验)
如果需要自定义非空提示、格式校验(如手机号格式),@AllArgsConstructor + @NonNull 无法满足,需手动写全参构造方法(细节如下):
java
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor // 保留无参构造
// 移除@AllArgsConstructor,手动写全参构造
public class User {
private Long id;
private String username;
private String phone;
private Integer age;
// 手动写全参构造,实现自定义校验
public User(Long id, String username, String phone, Integer age) {
// 1. 自定义非空提示
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空且不能为空白");
}
// 2. 手机号格式校验
if (phone == null) {
throw new IllegalArgumentException("手机号不能为空");
}
String phoneRegex = "^1[3-9]\\d{9}$";
if (!phone.matches(phoneRegex)) {
throw new IllegalArgumentException("手机号格式不正确,需为11位有效手机号");
}
// 3. 年龄范围校验
if (age != null && (age < 0 || age > 150)) {
throw new IllegalArgumentException("年龄需在0-150之间");
}
// 4. 赋值(校验通过后)
this.id = id;
this.username = username;
this.phone = phone;
this.age = age;
}
}
细节:手动写全参构造后,依然要保留 @NoArgsConstructor,否则框架反序列化报错。
二、核心注解关系与区别(细节对比,不混淆)
2.1 注解包含关系(精准无歧义)
- @Data 包含 @RequiredArgsConstructor(唯一包含关系);
- @Data 不包含 @NoArgsConstructor、@AllArgsConstructor(必须手动补充);
- @NoArgsConstructor、@AllArgsConstructor、@RequiredArgsConstructor 三者互不包含,可同时使用。
2.2 核心区别对比表(细节拉满,一目了然)
| 注解名 | 生成构造方法类型 | 参数包含范围 | 是否自动非空校验 | 适用场景 | 易错点 |
|---|---|---|---|---|---|
| @RequiredArgsConstructor | 必需参数构造 | final 字段(无默认值)、@NonNull 字段 | 是(仅 @NonNull 字段) | Spring 服务类(构造器注入) | final 有默认值时不纳入参数 |
| @Data | 隐含必需参数构造 | 同 @RequiredArgsConstructor | 是(仅 @NonNull 字段) | 实体类(POJO/DO/DTO) | 无无参 / 全参构造,需手动补充 |
| @NoArgsConstructor | 无参构造 | 无参数 | 无 | 实体类(适配框架反序列化) | final 字段无默认值时编译报错 |
| @AllArgsConstructor | 全参构造 | 所有字段(按声明顺序) | 仅 @NonNull 字段自动校验 | 批量赋值场景 | 默认无校验,需配合 @NonNull |
三、实战场景细节(不同类的注解选型,直接复用)
3.1 实体类(POJO/DO/DTO)
核心需求:适配框架反序列化、有基础 get/set 方法、按需批量赋值、非空校验,细节如下:
- 必加注解:@Data + @NoArgsConstructor(缺一不可);
- 按需加注解:@AllArgsConstructor(需要全参构造时);
- 非空校验:给需要非空的字段加 @NonNull(构造 + setter 自动校验);
- final 字段:如果有默认值,不影响构造方法;如果无默认值,需通过 @RequiredArgsConstructor(@Data 隐含)的构造方法赋值。
标准示例:
java
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.NonNull;
@Data
@NoArgsConstructor // 必加
@AllArgsConstructor // 按需加
public class UserDTO {
private Long id;
@NonNull // 非空校验
private String username;
@NonNull // 非空校验
private String phone;
private Integer age;
// final字段(有默认值)
private final String defaultRole = "USER";
}
3.2 Spring 服务类(@Service/@Controller)
核心需求:构造器注入依赖、代码简洁、无冗余,细节如下:
- 必加注解:@RequiredArgsConstructor;
- 依赖字段:必须用 final 修饰(否则无法纳入构造参数,无法注入);
- 无需加 @Autowired:Spring 4.3+ 会自动识别构造器注入,无需手动加;
- 避免加 @Data:服务类无需 get/set/toString,加 @Data 会生成冗余方法,仅需 @RequiredArgsConstructor。
标准示例:
java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor // 必加
public class UserService {
// 所有依赖用final修饰,自动构造器注入
private final UserMapper userMapper;
private final OrderMapper orderMapper;
private final RedisTemplate<String, Object> redisTemplate;
// 业务方法,直接使用依赖
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
3.3 工具类(无状态,无需注入)
核心需求:无构造方法(或私有构造)、避免实例化,细节如下:
- 无需加任何构造相关注解;
- 手动写私有无参构造,避免外部实例化:
java
public class StringUtils {
// 私有无参构造,避免外部实例化
private StringUtils() {}
// 静态工具方法
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
}
四、所有易错点汇总(细节全覆盖,避免踩坑)
- 易错点 1:仅加 @Data,有 final/@NonNull 字段 → 无参构造丢失,框架反序列化报错 → 解决方案:加 @NoArgsConstructor;
- 易错点 2:@Data 包含 @RequiredArgsConstructor,误以为 @Data 有全参构造 → 解决方案:需手动加 @AllArgsConstructor;
- 易错点 3:@RequiredArgsConstructor 的参数不包含 "有默认值的 final 字段" → 无需处理,属于正常逻辑;
- 易错点 4:@NoArgsConstructor 用于有 final 字段的类,未给 final 字段赋默认值 → 编译报错 → 解决方案:给 final 字段加默认值;
- 易错点 5:@AllArgsConstructor 默认无校验,以为加了 @AllArgsConstructor 就有非空校验 → 解决方案:给字段加 @NonNull;
- 易错点 6:Spring 服务类的依赖未用 final 修饰,加了 @RequiredArgsConstructor 也无法注入 → 解决方案:依赖字段用 final 修饰;
- 易错点 7:手动写了构造方法,注解生成的构造方法失效 → 解决方案:要么删除手动构造,要么保留手动构造(按需选择);
- 易错点 8:@NonNull 字段的 setter 方法会自动校验 null,新手不知情导致报错 → 解决方案:避免给 setter 传入 null,或手动写 setter 覆盖校验逻辑。
五、总结(细节闭环,吃透核心)
- 核心注解分工:@RequiredArgsConstructor(Spring 注入)、@Data(实体类基础)、@NoArgsConstructor(实体类必加)、@AllArgsConstructor(批量赋值);
- @Data 的核心坑:隐含 @RequiredArgsConstructor,无无参 / 全参构造,必须手动补充 @NoArgsConstructor(必加)、@AllArgsConstructor(按需);
- 非空校验:@RequiredArgsConstructor、@AllArgsConstructor 配合 @NonNull 实现,复杂校验需手动写构造方法;
- 实战选型:实体类(@Data+@NoArgsConstructor±@AllArgsConstructor)、Spring 服务类(@RequiredArgsConstructor+final 依赖);
- 所有细节围绕 "避免手动写模板代码、适配框架、避免空指针",记住核心规则,就能避开所有坑,高效使用 Lombok。