Lombok学习实战

Lombok 提供一组注解来代替 bean 类中的构造器、getter/setter、toString() 这样的样板代码,以减少 bean 类的代码冗余。如下是该项目官网中的介绍。

Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.
Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.

有意思的是,类似于 Java / Jakarta 等名字的来源,Lombok 也是一个地名,而且也与印尼有关,它是印尼的一个小岛的名称。使用地名,尤其是岛屿名称作为项目或计算机语言名似乎已经形成了一种传统,可参考这篇 趣文

如下地图中集齐了 Java, Jakarta 以及 Lombok。

lombok-island


使用Lombok

要使用 Lombok ,首先要引入该依赖。在 https://mvnrepository.com/ 中搜索 lombok ,找到需要的版本的 lombok 页面 (如下以 1.18.24 为例) 。

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>

若为 springboot 开发场景,由于 springboot 已经管理 Lombok。在 springboot-helloworldpom.xml 文件中引入如下依赖。

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

接着在 idea 中搜索并安装 Lombok 插件并启用 (勾选)。

plugin-lombok

此时即可基于 Idea 使用 Lombok 。


主要注解一览

Lombok 主要注解。

参考

注解 属性 描述
@Getter AccessLevel 作为字段注解时,生成该字段的 getter
作为类注解时,生成所有字段的 getter
static 修饰的字段无效
final 修饰的字段有效
@Setter AccessLevel 作为类注解时,生成所有字段的 setter
作为字段注解时,生成该字段的 setter
对 static 或 final 修饰的字段均无效
@ToString exclude
of
类注解。重写 toString()
@NoArgsConstructor 类注解。生成无参构造器。
@RequiredArgsConstructor 类注解。生成带参构造器,参数为如下:
final 修饰的且未赋值的字段
@NonNull 标注的字段
@AllArgsConstructor 类注解。生成全参构造器。
@EqualsAndHashCode exclude
of
类注解。重写 equals() / hashcode() 方法。
@Data 类注解。如下注解的集合
@Getter
@Setter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
@NonNull 作为参数注解时,不允许参数为 null,若为 null 则抛出 NullPoiterException
作为字段注解时,不允许字段为 null
@Log 类注解。注入日志类 Logger 实例 (字段名 log) 。
@Slf4j 类注解。注入日志类 Slf4j 实例 (字段名 log) 。
@Builder 类注解。在其所标注的类中生成静态方法 build() 和内部建造者类,使得创建类对象时,参数可通过建造者模式写入。
@Cleanup 局部变量注解。对于 InputStream, OutputStream 这样需要关闭资源的局部变量,自动生成 try-finally 代码。
val 作用类似于 Java 10 推出的类型推导新特性 var

@Getter & @Setter

  • 作为字段注解时,生成该字段的 getter / setter

  • 作为类注解时,生成所有字段的 getter / setter

  • 对 static 修饰的字段无效

  • 对 final 修饰的字段,getter 有效而 setter 无效


字段注解

@Getter 作为字段注解时如下,User.java 中虽未写 getId() 方法,但在 Structure 中可看到存在该方法,且经过 Idea 反编译 .class 可以看出 .class 中已经存在了 getId() 方法。

getter-field-type

@Setter 字段注解同理。

setter-field-type


类注解

@Getter 作为字段注解时,生成所有字段的 Getter 方法,@Setter 同理。

getter-setter-type


特殊情况

  • 对 static 修饰的字段无效。

  • 对 final 修饰的字段,getter 有效而 setter 无效

getter-setter-static-final


注解属性

可以通过 value 属性设置 Getter / Setter 方法的可见性,如下。

  • 字段注解优先于类注解。
  • AccessLevel.NONE 表示不创建对应方法。

getter-setter-accessLevel


@ToString

类注解,重写该类的 toString() 方法。

无 Getter 时。

ToString-no-Getter

有 Getter 时。

ToString-with-Getter


注解属性

of

可以通过 of 属性指定 toString() 方法输出的字段。指定多个时以 of = {"xx", "yy"} 形式书写。

ToString-of


exclude

可以通过 exclude 属性排除 toString() 方法输出的字段。指定多个时以 exclude = {"xx", "yy"} 形式书写。

ToString-exclude


@EqualsAndHashCode

类注解,重写该类的 equals()hashCode() 方法。

无 Getter 时。

EqualsAndHashCode-no-Getter

equals()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof User)) {
return false;
} else {
User other = (User)o;
if (!other.canEqual(this)) {
return false;
} else {
label59: {
Object this$id = this.id;
Object other$id = other.id;
if (this$id == null) {
if (other$id == null) {
break label59;
}
} else if (this$id.equals(other$id)) {
break label59;
}

return false;
}

this.getClass();
Object this$country = "CHINA";
other.getClass();
Object other$country = "CHINA";
if (this$country == null) {
if (other$country != null) {
return false;
}
} else if (!this$country.equals(other$country)) {
return false;
}

Object this$userName = this.userName;
Object other$userName = other.userName;
if (this$userName == null) {
if (other$userName != null) {
return false;
}
} else if (!this$userName.equals(other$userName)) {
return false;
}

Object this$password = this.password;
Object other$password = other.password;
if (this$password == null) {
if (other$password != null) {
return false;
}
} else if (!this$password.equals(other$password)) {
return false;
}

return true;
}
}
}

canEqual()

1
2
3
protected boolean canEqual(Object other) {
return other instanceof User;
}

hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int hashCode() {
int PRIME = true;
int result = 1;
Object $id = this.id;
result = result * 59 + ($id == null ? 43 : $id.hashCode());
this.getClass();
Object $country = "CHINA";
result = result * 59 + ($country == null ? 43 : $country.hashCode());
Object $userName = this.userName;
result = result * 59 + ($userName == null ? 43 : $userName.hashCode());
Object $password = this.password;
result = result * 59 + ($password == null ? 43 : $password.hashCode());
return result;
}

有 Getter 时。

EqualsAndHashCode-with-Getter

equals()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public boolean equals(Object o) {
// 若为同一对象,返回 true
if (o == this) {
return true;
} else if (!(o instanceof User)) { // 判断比较对象 o 是否为 User 的实例,不是则返回 false
return false;
} else { // 比较对象 o 是 User 的实例时
User other = (User)o; // 将 o 的类型转为 User
// 判断 other 是否是 User 的实例 (实际上前面已经判断过了,这里是一次冗余判断)
if (!other.canEqual(this)) {
return false;
} else {
label59: { // 反编译后会出现 label 标签
Object this$id = this.getId();
Object other$id = other.getId();
if (this$id == null) {
if (other$id == null) {
break label59; // 如果比较的两个对象的 id 都为 null,则 id 视作相等,继续下一个字段的比较
}
} else if (this$id.equals(other$id)) { // 如果两个对象的 id 相等 (不为空),继续下一个字段的比较
break label59;
}

return false; // 否则两个对象不相等
}

// 以下是 country 字段的比较
Object this$country = this.getCountry();
Object other$country = other.getCountry();
if (this$country == null) {
if (other$country != null) {
return false;
}
} else if (!this$country.equals(other$country)) {
return false;
}

// 以下是 userName 字段的比较
Object this$userName = this.getUserName();
Object other$userName = other.getUserName();
if (this$userName == null) {
if (other$userName != null) {
return false;
}
} else if (!this$userName.equals(other$userName)) {
return false;
}

// 以下是 password 字段的比较
Object this$password = this.getPassword();
Object other$password = other.getPassword();
if (this$password == null) {
if (other$password != null) {
return false;
}
} else if (!this$password.equals(other$password)) {
return false;
}

return true; // 所有字段都相等,则两个对象相等
}
}
}

canEqual()

1
2
3
protected boolean canEqual(Object other) {
return other instanceof User;
}

hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
public int hashCode() {
int PRIME = true;
int result = 1;
Object $id = this.getId();
result = result * 59 + ($id == null ? 43 : $id.hashCode());
Object $country = this.getCountry();
result = result * 59 + ($country == null ? 43 : $country.hashCode());
Object $userName = this.getUserName();
result = result * 59 + ($userName == null ? 43 : $userName.hashCode());
Object $password = this.getPassword();
result = result * 59 + ($password == null ? 43 : $password.hashCode());
return result;
}

注解属性

of

可以通过 of 属性指定 equals() 方法使用哪些字段完成判断,以及使用哪些字段用于 hashCode() 计算。指定多个时以 of = {"xx", "yy"} 形式书写。


exclude

可以通过 exclude 属性指定 equals() 方法不使用哪些字段完成判断,以及不使用使用哪些字段用于 hashCode() 计算。指定多个时以 exclude = {"xx", "yy"} 形式书写。


@NonNUll

  • 作为参数注解时,该参数的值不允许为 null

  • 作为字段注解时,该字段的值不允许为 null


参数注解

作为参数注解时,会在编译时加入判断是否为 null 的代码,若为 null ,则抛出 NPE 异常。

NonNull-parameter


字段注解

作为字段注解时,会在该字段对应 Getter 中加入方法注解 @NonNull ,表示该方法不会返回 null ;同时在该字段对应的 Setter 的方法参数重加入参数注解 @NonNull ,表示若传入参数为 null ,则抛出 NPE 异常。

NonNull-field


创建构造器的类注解

三个用于自动创建构造器的类注解。@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor


@NoArgsConstructor

类注解,用于生成无参构造器。

当类中不显式写出构造器时 (即无有参构造器) ,默认有无参构造器。但当显式写出有参构造器时,默认无无参构造器,此时若需要无参构造器,可以为该类加上类注解 @NoArgsConstructor

NoArgsConstructor


@RequiredArgsConstructor

类注解。生成 (被要求的) 带参构造器,参数为如下:

  • final 修饰的 且未赋值 的字段 (一旦赋值,就不会作为带参构造器中的参数)
  • @NonNull 标注的字段

RequiredArgsConstructor

当不存在所要求的参数时,不会创建带参构造器,但会创建无参构造器。

RequiredArgsConstructor1


@AllArgsConstructor

类注解。生成带参构造器,参数为所有类字段 (不包括已经赋值的由 final 修饰实例字段,另外注意 static 修饰的字段不是实例字段)。

如下,如果 country 已赋值,则不会出现在带参构造器参数中。

AllArgsConstructor


@Data

@Data 注解是如下注解的组合。

  • @Getter
  • @Setter
  • @RequiredArgsConstructor
  • @ToString
  • @EqualsAndHashCode

Data


@Builder

类注解。在其所标注的类中生成静态方法 build() 和内部建造者类,使得创建类对象时,参数可通过 建造者模式 写入。

如下,可以看到, @Builder 类注解做了如下处理。

  • 创建了一个 UserBuilder 内部建造者类,其中以各字段名为名的建造方法均返回其自身 (UserBuilder) 。
  • UserBuilder 内部建造者类可通过 build() 方法返回 User 对象。
  • User 类创建了包可见的带参构造器。
  • User 类创建了类静态方法 builder() ,返回 UserBuilder 建造者类。

Builder


日志类注解

类注解。注入日志类实例 (字段名 log) 。根据 官网 说明,Lombok 支持绝大多数流行的日志类,如 @CommonsLog, @Flogger, @JBossLog, @Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j, @CustomLog 。他们的用法都是类似的。下面以 @Log 为例。


@Log

Log


@Cleanup

局部变量注解。对于 InputStream, OutputStream 这样需要关闭资源的局部变量,自动生成 try-finally 代码。

例如对于如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.yukiyama.lombok;

import lombok.Cleanup;

import java.io.*;

public class User {

public static void main(String[] args) throws IOException {
@Cleanup InputStream in = new FileInputStream("");
@Cleanup OutputStream out = new FileOutputStream("");
byte[] b = new byte[10000];
while (true) {
int r = in.read(b);
if (r == -1) break;
out.write(b, 0, r);
}
}

}

Idea 反编译后可看到实际上为如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yukiyama.lombok;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;

public class User {
public User() {
}

public static void main(String[] args) throws IOException {
InputStream in = new FileInputStream("");

try {
OutputStream out = new FileOutputStream("");

try {
byte[] b = new byte[10000];

while(true) {
int r = in.read(b);
if (r == -1) {
return;
}

out.write(b, 0, r);
}
} finally {
if (Collections.singletonList(out).get(0) != null) {
out.close();
}

}
} finally {
if (Collections.singletonList(in).get(0) != null) {
in.close();
}

}
}
}

Cleanup


原理

JSR 269