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
无参构造函数。
@NoArgsConstructor
public class User {
private Long id;
private String name;
}展开后(这一份保留了 Lombok 打的两行注解,好让你知道它们一直在):
public class User {
private Long id;
private String name;
@java.lang.SuppressWarnings("all")
@lombok.Generated
public User() {
}
}类里有未初始化的 final 字段时它生成不出来(final 必须在构造函数里赋值),直接编译报错。加 force = true 让 Lombok 把它们填成零值:
@NoArgsConstructor(force = true)
public class User {
private final Long id;
private final int age;
}展开后:
public class User {
private final Long id;
private final int age;
public User() {
this.id = null;
this.age = 0;
}
}@AllArgsConstructor
全参构造,含有属性 @AllArgsConstructor(access = AccessLevel.PRIVATE),这个属性指定权限的。
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class User {
private Long id;
private String name;
}展开后:
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 且没有初始化,该属性被加在构造函数中。
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository repo; // final 未初始化 → 进
@NonNull private Clock clock; // @NonNull → 进
private int retry = 3; // 普通字段 → 不进
private final String prefix = "ORD"; // final 但已初始化 → 不进
}展开后:
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 和构造函数会带判空,自己手写的赋值一行检查都没有 |
public class NonNullDemo {
@Setter
@NonNull
private String name;
public void save(@NonNull String id, String memo) {
System.out.println(id + memo);
}
}展开后:
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:
lombok.nonNull.exceptionType = IllegalArgumentException四、⚠ 别和 javax.validation / jakarta.validation 的 @NotNull 搞混。那个是给校验框架读的元数据,自己不生成任何判空代码,只有走到 @Valid 那条链路上才会被检查。
@Getter和@Setter
放在类上面,表示对所有的属性生成 getter 和 setter 方法。
如果不想在某些属性上生成,可以在那属性上加上 @Getter 和 @Setter,并设置属性 value 为 AccessLevel.NONE。
@Getter
@Setter
public class User {
private Long id;
private boolean vip;
@Setter(AccessLevel.NONE)
private LocalDateTime createdAt; // 只读
}展开后(先按字段顺序出 getter,再出 setter;createdAt 的 setter 如愿没有):
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。
@ToString(of = {"id", "name"})
public class User {
private Long id;
private String name;
private String password;
}展开后:
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,上面那个类也可以写成:
@ToString(exclude = "password")callSuper 指定是否调用父类的 toString 方法 + 本类的 toString:
@ToString(callSuper = true)
public class Admin extends User {
private String level;
}展开后:
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)。
二、⚠ of 和 exclude 是老写法,官方文档里已标记为不推荐,新代码用成对的 @ToString.Exclude / @ToString.Include——重命名字段时它们跟着走,而字符串数组里的名字写错了不会报错,只会静默少打一个字段:
@ToString
public class User {
private Long id;
private String name;
@ToString.Exclude
private String password;
}@EqualsAndHashCode
也含有属性 of,只生成 of 里面的属性来生成 equals 和 hashCode 方法。
@EqualsAndHashCode(of = "id")
public class User {
private Long id;
private String name;
}展开后(name 全程不出现,两个只有 name 不同的对象是相等的):
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。
@Data
public class User {
private final Long id;
private String name;
}展开后一共七个方法——这是全篇最长的一份,正好把上面几节的产物合在一起看:
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() + ")";
}
}三处细节值得对着上面这段看:id 是 final,所以只有 getter 没有 setter;构造函数只收 id 一个参数,这就是 @RequiredArgsConstructor 的部分;equals / hashCode / toString 这次全走 getXxx() 而不是直接读字段,因为 getter 已经被生成出来了。
⚠ 末尾那个 @RequiredArgsConstructor 最常被忘掉:类里一旦有 final 或 @NonNull 字段,@Data 给出的就不是无参构造,而 Jackson 反序列化和 JPA 实体都要求有无参构造。两样都要就自己补一行:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
}@Value
@Data 的不可变版本。官方给的等价式是:
final @ToString @EqualsAndHashCode @AllArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter所以字段连 private final 都不用写,它会补上:
@Value
public class Money {
String currency;
long cents;
}展开后:
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 来设置链式编程,需要设置属性 chain 为 true。
下面三个例子为了让展开短一点,用的是 @Getter + @Setter;换成 @Data 的话只是再多出 equals / hashCode / toString / 构造函数那几个,和上一节一模一样。
@Getter
@Setter
@Accessors(chain = true)
public class User {
private Long id;
private String name;
}展开后 setter 的返回值从 void 变成了它自己:
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;
}
}于是能连着写:
User u = new User().setId(1L).setName("张三");fluent 属性设置为 true(此时 chain 也会被设置为 true),那么就能让方法 .setXxx() 变为 .xxx():
@Getter
@Setter
@Accessors(fluent = true)
public class User {
private Long id;
private String name;
}展开后读写同名,都不带 get / set 前缀,写的那一半仍然返回 this:
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,用来剥掉字段的命名前缀:
@Getter
@Setter
@Accessors(prefix = "m")
public class User {
private Long mId;
}展开后方法名里的 m 没了,但参数名还跟着字段叫 mId:
public class User {
private Long mId;
public Long getId() {
return this.mId;
}
public void setId(final Long mId) {
this.mId = mId;
}
}他自己不做事,需要 @Data 注解或者 @Getter、@Setter。
⚠ fluent 和 prefix 生成的方法名不符合 JavaBean 规范,Jackson、MyBatis、Spring 的表单绑定这些按 getXxx / setXxx 反射的框架会直接读不到字段。给 DTO 用之前先确认这条链路上没有它们。
@Builder
使用建造者设计模式生成代码。
@Builder
public class User {
private Long id;
private String name;
@Builder.Default
private int retry = 3;
}用法:
User u = User.builder().id(1L).name("张三").build();展开后(这是全篇唯一会长出一个内部类的注解):
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.Default) | keep(加了) | |
|---|---|---|
builder().build() | 0 | 7 |
new User()(Lombok 生成的无参构造) | 3 | 7 |
| 自己手写的构造函数 | 3 | 0 |
最后一行是另一半陷阱:@Builder.Default 的值只有 Lombok 生成的构造函数(@NoArgsConstructor 这些)会去取,自己手写的构造函数里字段的初始化表达式已经被挪走了,拿到的是 0 / null。手写构造函数里补一句 this(); 才能把默认值带回来。
二、@Builder 会顺带生成一个包级私有的全参构造(上面那个不带修饰符的 User(...)),于是类里不再有默认的无参构造。要给 Jackson 或 JPA 用就得把 @NoArgsConstructor 和 @AllArgsConstructor 一起补上。
三、想在已有对象的基础上只改一两个字段,用 toBuilder:
@Builder(toBuilder = true)
public class User { /* ... */ }
User v2 = u.toBuilder().name("李四").build();@Singular
集合字段加上它,builder 里就从「一次性塞一整个集合」变成「一次加一个」:
@Builder
public class Team {
@Singular
private List<String> members;
}用法(单数方法名 member 是 Lombok 自己从 members 推出来的):
Team t = Team.builder().member("张三").member("李四").build();展开后:
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
@Log
public class UserDaoListenerTest {
public void run() {
log.info("跑起来了");
}
}展开后:
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
@Slf4j
public class UserService {
public void save(Long id) {
log.info("保存用户 {}", id); // 占位符,不用自己拼字符串
log.error("保存失败", new RuntimeException());
}
}展开后:
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 都不用写。
同一族的还有几个,选哪个取决于项目里用的是哪套日志门面:
| 注解 | 生成的字段类型 |
|---|---|
@Log | java.util.logging.Logger |
@Slf4j | org.slf4j.Logger |
@XSlf4j | org.slf4j.ext.XLogger |
@Log4j | org.apache.log4j.Logger |
@Log4j2 | org.apache.logging.log4j.Logger |
@CommonsLog | org.apache.commons.logging.Log |
@JBossLog | org.jboss.logging.Logger |
@Flogger | com.google.common.flogger.FluentLogger |
字段名默认就是 log,想改 logger 的名字从注解属性走:
@Slf4j(topic = "audit") // Logger 的名字变成 "audit",不再是类的全限定名
public class UserService { }
评论
评论加载中……