lombok

Lombok 常用注解速查:每个注解配一段用法,以及它在编译期展开成的完整代码

Lombok 是在编译期往语法树里插方法,所以下面每一节的「展开后」都是真实存在于 .class 里的代码,只是源码里看不见。

这些展开不是手写的,是 java -jar lombok.jar delombok src -d out(Lombok 1.18.38 + JDK 17)跑出来的原样产物,只做了两处削减:每个生成的成员头上都有 @SuppressWarnings("all")@lombok.Generated 两行注解,这里只在第一个例子里保留、后面都删掉了;java.lang.Object 这类全限定名收短成了 Object

构造函数相关

@NoArgsConstructor

无参构造函数。

java
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
}

展开后(这一份保留了 Lombok 打的两行注解,好让你知道它们一直在):

java
public class User {
    private Long id;
    private String name;

    @java.lang.SuppressWarnings("all")
    @lombok.Generated
    public User() {
    }
}

类里有未初始化的 final 字段时它生成不出来(final 必须在构造函数里赋值),直接编译报错。加 force = true 让 Lombok 把它们填成零值:

java
@NoArgsConstructor(force = true)
public class User {
    private final Long id;
    private final int age;
}

展开后:

java
public class User {
    private final Long id;
    private final int age;

    public User() {
        this.id = null;
        this.age = 0;
    }
}

@AllArgsConstructor

全参构造,含有属性 @AllArgsConstructor(access = AccessLevel.PRIVATE),这个属性指定权限的。

java
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class User {
    private Long id;
    private String name;
}

展开后:

java
public class User {
    private Long id;
    private String name;

    private User(final Long id, final String name) {
        this.id = id;
        this.name = name;
    }
}

参数顺序就是字段的声明顺序,static 字段和已经初始化过的 final 字段不参与。注意生成的参数都带 final

@RequiredArgsConstructor

生成的构造函数是规定需要的。哪些字段算「需要」,两条:

一、当属性被加了 lombok 注解 @NonNull,则该属性会被包含在构造函数中,并且带一句非空校验。

二、当属性加了 final 且没有初始化,该属性被加在构造函数中。

java
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository repo;   // final 未初始化 → 进
    @NonNull private Clock clock;         // @NonNull → 进
    private int retry = 3;                // 普通字段 → 不进
    private final String prefix = "ORD";  // final 但已初始化 → 不进
}

展开后:

java
public class OrderService {
    private final OrderRepository repo;
    @NonNull
    private Clock clock;
    private int retry = 3;
    private final String prefix = "ORD";

    public OrderService(final OrderRepository repo, @NonNull final Clock clock) {
        if (clock == null) {
            throw new NullPointerException("clock is marked non-null but is null");
        }
        this.repo = repo;
        this.clock = clock;
    }
}

这正是 Spring 构造器注入最省事的写法:依赖全写成 private final,类上一个 @RequiredArgsConstructor 就够了,不需要 @Autowired

@NonNull

它自己不产生任何代码,只是给别的生成器留的一个标记,所以放的位置决定了它由谁来兑现:

放在哪谁来生成判空
方法 / 构造函数的参数上直接在方法体最顶上插一句判空
字段上只有 Lombok 替你生成的 setter 和构造函数会带判空,自己手写的赋值一行检查都没有
java
public class NonNullDemo {
    @Setter
    @NonNull
    private String name;

    public void save(@NonNull String id, String memo) {
        System.out.println(id + memo);
    }
}

展开后:

java
public class NonNullDemo {
    @NonNull
    private String name;

    public void save(@NonNull String id, String memo) {
        if (id == null) {
            throw new NullPointerException("id is marked non-null but is null");
        }
        System.out.println(id + memo);
    }

    public void setName(@NonNull final String name) {
        if (name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        }
        this.name = name;
    }
}

字段上那个 @NonNull 是被 @Setter 兑现的——把 @Setter 去掉,这个类里就只剩 save 那一处判空了。

四条细则:

一、构造函数里这句判空插在显式的 this() / super() 调用之后,其余情况一律在方法体第一行。

二、方法开头已经有等价的判空(if 抛异常、assert、或者 Objects.requireNonNull 这类调用),Lombok 就不再插一句。

三、默认抛 NullPointerException,消息固定是 字段名 is marked non-null but is null。想换成别的,在项目根的 lombok.config 里写一行,可选值是 NullPointerException / IllegalArgumentException / JDK(走 Objects.requireNonNull)/ Guava(走 Preconditions.checkNotNull)/ Assertion

properties
lombok.nonNull.exceptionType = IllegalArgumentException

四、⚠ 别和 javax.validation / jakarta.validation@NotNull 搞混。那个是给校验框架读的元数据,自己不生成任何判空代码,只有走到 @Valid 那条链路上才会被检查。

@Getter@Setter

放在类上面,表示对所有的属性生成 getter 和 setter 方法。

如果不想在某些属性上生成,可以在那属性上加上 @Getter@Setter,并设置属性 valueAccessLevel.NONE

java
@Getter
@Setter
public class User {
    private Long id;
    private boolean vip;
    @Setter(AccessLevel.NONE)
    private LocalDateTime createdAt;   // 只读
}

展开后(先按字段顺序出 getter,再出 setter;createdAt 的 setter 如愿没有):

java
public class User {
    private Long id;
    private boolean vip;
    private LocalDateTime createdAt;

    public Long getId() {
        return this.id;
    }

    public boolean isVip() {
        return this.vip;
    }

    public LocalDateTime getCreatedAt() {
        return this.createdAt;
    }

    public void setId(final Long id) {
        this.id = id;
    }

    public void setVip(final boolean vip) {
        this.vip = vip;
    }
}

自己写的优先级更高

⚠ 只有基本类型 boolean 的读方法叫 isVip(),包装类型 Boolean vip 生成的是 getVip()。序列化框架按方法名反推字段名,这两者在 JSON 里的键并不一样。

@ToString

生成 toString 方法,他有个属性是 of,值是一个字符串数组,填入属性,那么只生成那个属性组的 toString

java
@ToString(of = {"id", "name"})
public class User {
    private Long id;
    private String name;
    private String password;
}

展开后:

java
public class User {
    private Long id;
    private String name;
    private String password;

    @Override
    public String toString() {
        return "User(id=" + this.id + ", name=" + this.name + ")";
    }
}

exclude 属性恰恰相反,是排除属性作为 toString,上面那个类也可以写成:

java
@ToString(exclude = "password")

callSuper 指定是否调用父类的 toString 方法 + 本类的 toString

java
@ToString(callSuper = true)
public class Admin extends User {
    private String level;
}

展开后:

java
public class Admin extends User {
    private String level;

    @Override
    public String toString() {
        return "Admin(super=" + super.toString() + ", level=" + this.level + ")";
    }
}

两件事:

一、上面两份展开里读的都是 this.id 这样的字段,是因为那个类没有 getter。字段有 getter 时 Lombok 会改成调 this.getId(),想强行按字段读就写 @ToString(doNotUseGetters = true)

二、⚠ ofexclude 是老写法,官方文档里已标记为不推荐,新代码用成对的 @ToString.Exclude / @ToString.Include——重命名字段时它们跟着走,而字符串数组里的名字写错了不会报错,只会静默少打一个字段:

java
@ToString
public class User {
    private Long id;
    private String name;
    @ToString.Exclude
    private String password;
}

@EqualsAndHashCode

也含有属性 of,只生成 of 里面的属性来生成 equalshashCode 方法。

java
@EqualsAndHashCode(of = "id")
public class User {
    private Long id;
    private String name;
}

展开后(name 全程不出现,两个只有 name 不同的对象是相等的):

java
public class User {
    private Long id;
    private String name;

    @Override
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof User)) return false;
        final User other = (User) o;
        if (!other.canEqual((Object) this)) return false;
        final Object this$id = this.id;
        final Object other$id = other.id;
        if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false;
        return true;
    }

    protected boolean canEqual(final Object other) {
        return other instanceof User;
    }

    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final Object $id = this.id;
        result = result * PRIME + ($id == null ? 43 : $id.hashCode());
        return result;
    }
}

canEqual 是给继承用的:子类重写它之后,「父类对象 equals 子类对象」会被判成 false,而不是单向成立。只要类是 final 且直接继承 Object,Lombok 就不生成它——@Value 那一节的展开里就没有。

callSuper 默认是 false,也就是父类字段一概不比;类上有 extends 却没写 callSuper 时 Lombok 会给一条警告。

@Data

相当于 @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor

java
@Data
public class User {
    private final Long id;
    private String name;
}

展开后一共七个方法——这是全篇最长的一份,正好把上面几节的产物合在一起看:

java
public class User {
    private final Long id;
    private String name;

    public User(final Long id) {
        this.id = id;
    }

    public Long getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(final String name) {
        this.name = name;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof User)) return false;
        final User other = (User) o;
        if (!other.canEqual((Object) this)) return false;
        final Object this$id = this.getId();
        final Object other$id = other.getId();
        if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false;
        final Object this$name = this.getName();
        final Object other$name = other.getName();
        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
        return true;
    }

    protected boolean canEqual(final Object other) {
        return other instanceof User;
    }

    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final Object $id = this.getId();
        result = result * PRIME + ($id == null ? 43 : $id.hashCode());
        final Object $name = this.getName();
        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "User(id=" + this.getId() + ", name=" + this.getName() + ")";
    }
}

三处细节值得对着上面这段看:idfinal,所以只有 getter 没有 setter;构造函数只收 id 一个参数,这就是 @RequiredArgsConstructor 的部分;equals / hashCode / toString 这次全走 getXxx() 而不是直接读字段,因为 getter 已经被生成出来了。

⚠ 末尾那个 @RequiredArgsConstructor 最常被忘掉:类里一旦有 final@NonNull 字段,@Data 给出的就不是无参构造,而 Jackson 反序列化和 JPA 实体都要求有无参构造。两样都要就自己补一行:

java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
}

@Value

@Data 的不可变版本。官方给的等价式是:

java
final @ToString @EqualsAndHashCode @AllArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter

所以字段连 private final 都不用写,它会补上:

java
@Value
public class Money {
    String currency;
    long cents;
}

展开后:

java
public final class Money {
    private final String currency;
    private final long cents;

    public Money(final String currency, final long cents) {
        this.currency = currency;
        this.cents = cents;
    }

    public String getCurrency() {
        return this.currency;
    }

    public long getCents() {
        return this.cents;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof Money)) return false;
        final Money other = (Money) o;
        if (this.getCents() != other.getCents()) return false;
        final Object this$currency = this.getCurrency();
        final Object other$currency = other.getCurrency();
        if (this$currency == null ? other$currency != null : !this$currency.equals(other$currency)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $cents = this.getCents();
        result = result * PRIME + (int) ($cents >>> 32 ^ $cents);
        final Object $currency = this.getCurrency();
        result = result * PRIME + ($currency == null ? 43 : $currency.hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "Money(currency=" + this.getCurrency() + ", cents=" + this.getCents() + ")";
    }
}

@Data 逐项对照:

@Data@Value
类本身普通类final
字段保持你写的修饰符自动补 private final
setter没有
构造函数@RequiredArgsConstructor@AllArgsConstructor
canEqual生成不生成(类是 final,没有子类要防)

想放开某一项,用 lombok.experimental 包里那两个:字段上加 @NonFinal@PackagePrivate,类上加 @NonFinal 去掉 final

⚠ 没有 setter、也没有无参构造,Jackson 和 JPA 同样要单独处理——JPA 更是直接用不了,它要求实体可变且有无参构造。

⚠ JDK 16 起有 record,纯粹的数据载体优先用它,编译器给的不可变保证比注解更硬。@Value 还有用武之地的场合是项目停在 8 / 11,或者需要继承一个父类——record 不能 extends

@Accessors

@Data 注解上加上 @Accessors 来设置链式编程,需要设置属性 chaintrue

下面三个例子为了让展开短一点,用的是 @Getter + @Setter;换成 @Data 的话只是再多出 equals / hashCode / toString / 构造函数那几个,和上一节一模一样。

java
@Getter
@Setter
@Accessors(chain = true)
public class User {
    private Long id;
    private String name;
}

展开后 setter 的返回值从 void 变成了它自己:

java
public class User {
    private Long id;
    private String name;

    public Long getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public User setId(final Long id) {
        this.id = id;
        return this;
    }

    public User setName(final String name) {
        this.name = name;
        return this;
    }
}

于是能连着写:

java
User u = new User().setId(1L).setName("张三");

fluent 属性设置为 true(此时 chain 也会被设置为 true),那么就能让方法 .setXxx() 变为 .xxx()

java
@Getter
@Setter
@Accessors(fluent = true)
public class User {
    private Long id;
    private String name;
}

展开后读写同名,都不带 get / set 前缀,写的那一半仍然返回 this

java
public class User {
    private Long id;
    private String name;

    public Long id() {
        return this.id;
    }

    public String name() {
        return this.name;
    }

    public User id(final Long id) {
        this.id = id;
        return this;
    }

    public User name(final String name) {
        this.name = name;
        return this;
    }
}

还有一个 prefix,用来剥掉字段的命名前缀:

java
@Getter
@Setter
@Accessors(prefix = "m")
public class User {
    private Long mId;
}

展开后方法名里的 m 没了,但参数名还跟着字段叫 mId

java
public class User {
    private Long mId;

    public Long getId() {
        return this.mId;
    }

    public void setId(final Long mId) {
        this.mId = mId;
    }
}

他自己不做事,需要 @Data 注解或者 @Getter@Setter

fluentprefix 生成的方法名不符合 JavaBean 规范,Jackson、MyBatis、Spring 的表单绑定这些按 getXxx / setXxx 反射的框架会直接读不到字段。给 DTO 用之前先确认这条链路上没有它们。

@Builder

使用建造者设计模式生成代码。

java
@Builder
public class User {
    private Long id;
    private String name;
    @Builder.Default
    private int retry = 3;
}

用法:

java
User u = User.builder().id(1L).name("张三").build();

展开后(这是全篇唯一会长出一个内部类的注解):

java
public class User {
    private Long id;
    private String name;
    private int retry;

    private static int $default$retry() {
        return 3;
    }

    User(final Long id, final String name, final int retry) {
        this.id = id;
        this.name = name;
        this.retry = retry;
    }

    public static User.UserBuilder builder() {
        return new User.UserBuilder();
    }

    public static class UserBuilder {
        private Long id;
        private String name;
        private boolean retry$set;
        private int retry$value;

        UserBuilder() {
        }

        public User.UserBuilder id(final Long id) {
            this.id = id;
            return this;
        }

        public User.UserBuilder name(final String name) {
            this.name = name;
            return this;
        }

        public User.UserBuilder retry(final int retry) {
            this.retry$value = retry;
            retry$set = true;
            return this;
        }

        public User build() {
            int retry$value = this.retry$value;
            if (!this.retry$set) retry$value = User.$default$retry();
            return new User(this.id, this.name, retry$value);
        }

        @Override
        public String toString() {
            return "User.UserBuilder(id=" + this.id + ", name=" + this.name + ", retry$value=" + this.retry$value + ")";
        }
    }
}

对着这段能看清 @Builder.Default 的机制:字段上写的 = 3 被搬进了 $default$retry(),builder 里多出一个 retry$set 布尔位记着「这次到底设没设过」,build() 里才决定用哪个值。

三件容易踩的:

一、@Builder.Default 不加,字段上写的初始值在 builder 这条路上会整个丢掉,编译时 Lombok 会给一条警告:

@Builder will ignore the initializing expression entirely. If you want the initializing expression to serve as default, add @Builder.Default.

实测同一个类里放两个字段,一个加一个不加,三条路径跑出来的结果是:

retry(没加 @Builder.Defaultkeep(加了)
builder().build()07
new User()(Lombok 生成的无参构造)37
自己手写的构造函数30

最后一行是另一半陷阱:@Builder.Default 的值只有 Lombok 生成的构造函数(@NoArgsConstructor 这些)会去取,自己手写的构造函数里字段的初始化表达式已经被挪走了,拿到的是 0 / null。手写构造函数里补一句 this(); 才能把默认值带回来。

二、@Builder 会顺带生成一个包级私有的全参构造(上面那个不带修饰符的 User(...)),于是类里不再有默认的无参构造。要给 Jackson 或 JPA 用就得把 @NoArgsConstructor@AllArgsConstructor 一起补上。

三、想在已有对象的基础上只改一两个字段,用 toBuilder

java
@Builder(toBuilder = true)
public class User { /* ... */ }

User v2 = u.toBuilder().name("李四").build();

@Singular

集合字段加上它,builder 里就从「一次性塞一整个集合」变成「一次加一个」:

java
@Builder
public class Team {
    @Singular
    private List<String> members;
}

用法(单数方法名 member 是 Lombok 自己从 members 推出来的):

java
Team t = Team.builder().member("张三").member("李四").build();

展开后:

java
public class Team {
    private List<String> members;

    Team(final List<String> members) {
        this.members = members;
    }

    public static Team.TeamBuilder builder() {
        return new Team.TeamBuilder();
    }

    public static class TeamBuilder {
        private java.util.ArrayList<String> members;

        TeamBuilder() {
        }

        public Team.TeamBuilder member(final String member) {
            if (this.members == null) this.members = new java.util.ArrayList<String>();
            this.members.add(member);
            return this;
        }

        public Team.TeamBuilder members(final Collection<? extends String> members) {
            if (members == null) {
                throw new NullPointerException("members cannot be null");
            }
            if (this.members == null) this.members = new java.util.ArrayList<String>();
            this.members.addAll(members);
            return this;
        }

        public Team.TeamBuilder clearMembers() {
            if (this.members != null) this.members.clear();
            return this;
        }

        public Team build() {
            List<String> members;
            switch (this.members == null ? 0 : this.members.size()) {
                case 0:
                    members = java.util.Collections.emptyList();
                    break;
                case 1:
                    members = java.util.Collections.singletonList(this.members.get(0));
                    break;
                default:
                    members = java.util.Collections.unmodifiableList(new java.util.ArrayList<String>(this.members));
            }
            return new Team(members);
        }

        @Override
        public String toString() {
            return "Team.TeamBuilder(members=" + this.members + ")";
        }
    }
}

一共多出三个方法:加一个(member)、加一批(members)、清空(clearMembers)。

⚠ 看 build() 那个 switch@Singular 造出来的集合是不可变的(emptyList / singletonList / unmodifiableList),拿到手之后再 add 会抛 UnsupportedOperationException。这是它和普通字段最大的区别,不是顺手给的语法糖。

@Log@Slf4j

对日志支持,帮我们声明日志属性。

@Log

java
@Log
public class UserDaoListenerTest {
    public void run() {
        log.info("跑起来了");
    }
}

展开后:

java
public class UserDaoListenerTest {
    private static final java.util.logging.Logger log =
        java.util.logging.Logger.getLogger(UserDaoListenerTest.class.getName());

    public void run() {
        log.info("跑起来了");
    }
}

@Slf4j

java
@Slf4j
public class UserService {
    public void save(Long id) {
        log.info("保存用户 {}", id);              // 占位符,不用自己拼字符串
        log.error("保存失败", new RuntimeException());
    }
}

展开后:

java
public class UserService {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class);

    public void save(Long id) {
        log.info("保存用户 {}", id);
        log.error("保存失败", new RuntimeException());
    }
}

两份展开里生成的都是 private static final,而且用的是全限定名——所以类里连 import 都不用写。

同一族的还有几个,选哪个取决于项目里用的是哪套日志门面:

注解生成的字段类型
@Logjava.util.logging.Logger
@Slf4jorg.slf4j.Logger
@XSlf4jorg.slf4j.ext.XLogger
@Log4jorg.apache.log4j.Logger
@Log4j2org.apache.logging.log4j.Logger
@CommonsLogorg.apache.commons.logging.Log
@JBossLogorg.jboss.logging.Logger
@Floggercom.google.common.flogger.FluentLogger

字段名默认就是 log,想改 logger 的名字从注解属性走:

java
@Slf4j(topic = "audit")   // Logger 的名字变成 "audit",不再是类的全限定名
public class UserService { }

评论

评论加载中……

登录后再评论

注册要用邮箱收个验证码,只为确认邮箱能收信,不会拿去做别的。账号设置