本科主学 Java,研究生期间转了前端。今年校招入职后,开始接触后端项目代码。打开项目,领域层的一个模型对象让我愣了一下:
java
@Builder
@Data
public class OrderItem {
private Long id;
private String productName;
private BigDecimal amount;
private Integer quantity;
}
@Builder?@Data 我倒是认识,但 @Builder 是什么?更让我困惑的是,代码里到处都在用 OrderItem.builder().productName("咖啡").quantity(2).build() 这样的写法,但类里明明没有 builder() 方法。
带着这个疑问,我花了一个下午把 Lombok、Java 注解机制、Builder 模式串了一遍,才发现这三个东西其实是一条线上的。
Lombok:那些注解到底做了什么
先回答最直接的问题:@Builder 和 @Data 从哪来的?
答案是 Lombok------一个通过注解在编译期自动生成代码的工具库。你标上注解,它在编译的时候帮你把 getter、setter、toString、构造方法等"样板代码"悄悄写好,写进最终的字节码里。
最常用的几个注解
java
@Data // = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor
public class User {
private Long id;
private String name;
private String email;
}
一行 @Data,等于手写了五六个方法的代码。对于一个有十几个字段的领域模型来说,代码量直接减少 60% 以上。
其他常用的还有:
| 注解 | 生成内容 |
|---|---|
@Getter / @Setter |
字段的 getter / setter |
@ToString |
toString() 方法 |
@EqualsAndHashCode |
equals() 和 hashCode() |
@NoArgsConstructor |
无参构造方法 |
@AllArgsConstructor |
全参构造方法 |
@Builder |
Builder 模式构造方法 |
@Data |
上述前四个的组合 |
深入原理:编译期的"魔法"
这是我花时间最多、也觉得最有趣的部分。
Lombok 的核心原理是 Java Annotation Processing Tool(APT) 。它在编译阶段介入,通过修改**抽象语法树(AST)**来注入代码。
什么意思呢?Java 编译的过程大致是这样的:
arduino
源码 → 词法/语法分析 → AST(抽象语法树)→ 语义分析 → 字节码生成 → .class 文件
Lombok 做的事情是:在 AST 生成之后、字节码生成之前,"偷偷"修改这棵树------给 User 节点加上 getName()、setName() 等方法节点。等编译器继续往下走生成字节码时,这些方法就已经存在了。
这意味着:
- 运行时零开销:生成的代码和手写的没有区别,不需要反射
- 不需要额外的运行时依赖(除了 Lombok 本身的注解定义)
- IDE 需要安装 Lombok 插件:否则 IDE 看不到生成的方法,会报红------这也是为什么我一开始觉得代码"缺了方法"
💡 有趣的是,这种修改 AST 的做法其实并不在 Java 官方支持的 API 范围内。Lombok 使用了一些内部 API(
com.sun.tools.javac.tree),这也是为什么每次 Java 大版本升级,Lombok 都可能出兼容性问题。
⚠️ 实战中的坑
了解到原理后,一些"坑"也就好理解了:
- JPA Entity 慎用
@Data:@Data包含的@EqualsAndHashCode默认使用所有字段。如果实体有关联的延迟加载字段,equals()比较时可能触发代理对象的加载,导致异常或 N+1 查询 - 继承场景注意 :如果父类也用了
@EqualsAndHashCode,子类默认不会调用父类的equals,可能遗漏父类字段的比较。需要显式加@EqualsAndHashCode(callSuper = true) - 构造方法容易缺 :
@Data只生成@RequiredArgsConstructor(包含final和@NonNull字段)。如果你需要无参构造,得额外加@NoArgsConstructor
Java 注解:这一切的底层基础
搞懂了 Lombok 的原理,自然就引出了一个问题:注解到底是什么?
注解(Annotation)是 Java 5 引入的元数据机制。本质上,它是一种特殊的接口,用 @interface 定义。它本身不直接做任何事情------它只是"标记",具体的行为由读取它的工具来决定。
三种保留策略
注解的生命周期由 @Retention 决定,这是理解整个机制的关键:
java
@Retention(RetentionPolicy.SOURCE) // 编译后丢弃
@Retention(RetentionPolicy.CLASS) // 保留在 class 文件中,但运行时不可见
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取
三种策略对应了三种完全不同的用途:
- SOURCE 级别 :如
@Override,编译器用做语法检查,编译后直接丢弃。你在.class文件里找不到它 - CLASS 级别 :如 Lombok 的注解,保留在 class 文件中,但主要由编译阶段的 APT 处理。Lombok 读到
@Data,就往 AST 里注入代码 - RUNTIME 级别 :如 Spring 的
@Autowired、@RequestMapping,通过反射在运行时读取,框架据此执行依赖注入、路由分发等逻辑
回到 Lombok 的话题:Lombok 的注解(如
@Data、@Builder)实际上是 CLASS 级别的。它们在编译期被 APT 读取并处理,处理完后生成的代码写入字节码。运行时不需要再"读取"这些注解------代码已经在那里了。
注解的三大作用
归纳一下,注解在 Java 生态中扮演三种角色:
- 编译时处理:Lombok、MapStruct 等在编译期生成代码
- 运行时处理:Spring、JUnit 等框架通过反射读取注解实现动态行为
- 配置替代 :Spring Boot 的
@Configuration、@Bean替代了以前繁琐的 XML 配置
理解了这三种策略,再看到任何注解,你都能快速判断它是在哪个阶段起作用的。
Builder 模式:@Builder 背后的设计思想
最后回到最初让我困惑的那个 @Builder。
为什么需要 Builder?
想象一个场景:你有一个领域模型,有 20 个字段,其中 15 个是可选的。如果用构造方法:
java
// 这谁看得懂第 7 个参数是什么?
User user = new User(1L, "张三", null, null, 25, null, "Beijing",
null, null, null, "13800138000", null, null, null, null,
null, null, null, null, "2026-01-01");
Builder 模式让这个问题迎刃而解:
java
User user = User.builder()
.id(1L)
.name("张三")
.age(25)
.city("Beijing")
.phone("13800138000")
.build();
每个字段名都清晰可见,可选参数可以随意省略,代码可读性大幅提升。
两种实现方式
手动实现(经典 Builder):
java
public class User {
private Long id;
private String name;
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Long id;
private String name;
public Builder id(Long id) {
this.id = id;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public User build() {
User user = new User();
user.id = this.id;
user.name = this.name;
return user;
}
}
}
Lombok @Builder(一行搞定):
java
@Builder
@Data
public class User {
private Long id;
private String name;
}
// 使用方式完全一致
User user = User.builder()
.id(1L)
.name("张三")
.build();
Lombok 在编译期帮你生成了上面那一大段 Builder 内部类。
何时使用 Builder?
- 对象有 4 个以上可选参数时
- 需要构建不可变对象 (所有字段
final,线程安全) - 参数之间存在依赖关系 ,需要在
build()中做校验 - 构建过程需要分步进行,中间可以插入校验或日志
在我看的那个项目代码里,领域模型用了
@Builder是因为模型字段多、大部分可选,而且构建时需要保证某些字段组合的合法性------这正是 Builder 模式的典型场景。
小结
一条线索串起来:
- 看到项目代码里类上的
@Builder→ 了解到这是 Lombok 提供的注解 - Lombok 怎么做到的?→ 深入到 APT / AST 原理,发现它在编译期修改语法树注入代码
- APT 又是基于什么?→ Java 注解机制,三种 Retention 策略决定了注解在哪个阶段起作用
@Builder的本质是什么?→ Builder 设计模式,解决多参数对象构建的可读性问题
本科的时候这些知识点是分散的、应试式的。研究生阶段精力转到了前端,Java 的东西就渐渐生疏了。这次因为实际项目代码里的一个注解,把它们重新串成了一条完整的链路------这大概就是"带着问题学习"的好处吧。