从一行 @Builder 说起:重新拾起 Java 的 Lombok、注解与 Builder 模式

本科主学 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 生态中扮演三种角色:

  1. 编译时处理:Lombok、MapStruct 等在编译期生成代码
  2. 运行时处理:Spring、JUnit 等框架通过反射读取注解实现动态行为
  3. 配置替代 :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 模式的典型场景。


小结

一条线索串起来:

  1. 看到项目代码里类上的 @Builder → 了解到这是 Lombok 提供的注解
  2. Lombok 怎么做到的?→ 深入到 APT / AST 原理,发现它在编译期修改语法树注入代码
  3. APT 又是基于什么?→ Java 注解机制,三种 Retention 策略决定了注解在哪个阶段起作用
  4. @Builder 的本质是什么?→ Builder 设计模式,解决多参数对象构建的可读性问题

本科的时候这些知识点是分散的、应试式的。研究生阶段精力转到了前端,Java 的东西就渐渐生疏了。这次因为实际项目代码里的一个注解,把它们重新串成了一条完整的链路------这大概就是"带着问题学习"的好处吧。

相关推荐
考虑考虑10 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯11 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
青石路15 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
像我这样帅的人丶你还18 小时前
Java 后端详解(五):Redis 缓存
java·后端·全栈
plainGeekDev20 小时前
GreenDAO → Room
android·java·kotlin
亦暖筑序1 天前
Java 8老系统AI Workflow实战:把一次性AI对话升级成可恢复工作流
java·后端
敲代码的彭于晏1 天前
Bean 生命周期完全图解:前端同学也能看懂的 Spring 核心机制
java·前端·后端
plainGeekDev1 天前
ButterKnife → ViewBinding
android·java·kotlin
像我这样帅的人丶你还2 天前
Java 后端详解(四):分页与搜索
java·javascript·后端