Lombok常用註解詳解: val, @Cleanup, @RequiredArgsConstructor

From: https://blog.csdn.net/hy6533/article/details/131030094

從零開始 Spring Boot 35:Lombok


圖源:簡書 (jianshu.com)

Lombok是一個java項目,旨在幫助開發者減少一些“模板代碼”。其具體方式是在Java代碼生成字節碼(class文件)時,根據你添加的相關Lombok註解或類來“自動”添加和生成相應的字節碼,以補完代碼所需的“模板代碼”。

實際上 Lombok 和 Spring 並沒有關聯關係,你開發任何Java應用都可以選擇使用 Lombok,只不過日常的 Spring 開發中很容易看到 Lombok 的使用,所以這裏就歸類到這個系列博客。

爲什麼要使用 Lombok
我們先看一個Spring 開發中很常見的 POJO 類是什麼樣的:

public class Book {

private Long id;
private String name;
private Long userId;
private Long publisherId;

public Book() {
}

public Book(String name, Long userId, Long publisherId) {
this.name = name;
this.userId = userId;
this.publisherId = publisherId;
}

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

public Long getId() {
return id;
}


public String getName() {
return name;
}

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

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}

public Long getPublisherId() {
return publisherId;
}

public void setPublisherId(Long publisherId) {
this.publisherId = publisherId;
}
}
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
實際上這種有一個空構造器和Getter/Setter的 Java 類,被稱作 Java Bean,最早是爲了開發 Java桌面應用提出的標準,不過目前已經被第三方 Java 框架廣泛採納和使用。

爲了能讓框架獲取或修改我們的自定義類中的屬性,我們需要提供Getter/Setter,以及可能需要的包含各種參數的構造器。顯然爲了讓一個類變成 Java Bean所添加的代碼,都是“模板代碼”,是可以通過自動化手段取代的,這裏我們就是 Lombok 的用武之地了。

如果上邊的示例中 Lombok 改寫,會變成這樣:

@NoArgsConstructor
@Setter
@Getter
public class Book {

private Long id;
private String name;
private Long userId;
private Long publisherId;

public Book(String name, Long userId, Long publisherId) {
this.name = name;
this.userId = userId;
this.publisherId = publisherId;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
這樣做的好處有:

減少了不必要的模板代碼,提高效率,以及讓代碼更簡潔。
如果新添加了屬性,無需手動添加相應的Getter/Setter。
當然,要使用 Lombok,需要在項目中添加相應的依賴:

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1
2
3
4
5
下面一一介紹Lombok的功能。

var
在 Python3 或者 Go 這類“新語言”中,“自動類型推斷”是一個很常見的語言級別功能,這個功能或多或少都會讓你的編碼工作更順暢一些。Java 自 Java 10 起,也支持類似的功能:

JEP 286: Local-Variable Type Inference (openjdk.org)
直接看示例:

package com.example.lombok;

// ...
@SpringBootApplication
public class LombokApplication {

public static void main(String[] args) {
SpringApplication.run(LombokApplication.class, args);
testVar();
}

private static void testVar() {
var names = new ArrayList<String>();
names.add("Li Lei");
System.out.println(names.get(0));
var students = new HashMap<Integer, String>();
students.put(12, "Li Lei");
students.put(20, "Han Meimei");
for (var s : students.entrySet()) {
System.out.println("number is %d, name is %s.".formatted(s.getKey(), s.getValue()));
}
students = new HashMap<>();
}

}
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
在示例中,局部變量和for的條件語句中都用var取代了具體類型。

代碼編譯成字節碼後,var會被相應的具體類型取代:

// ...
private static void testVar() {
ArrayList<String> names = new ArrayList();
names.add("Li Lei");
System.out.println((String)names.get(0));
HashMap<Integer, String> students = new HashMap();
students.put(12, "Li Lei");
students.put(20, "Han Meimei");
Iterator var2 = students.entrySet().iterator();

while(var2.hasNext()) {
Entry<Integer, String> s = (Entry)var2.next();
System.out.println("number is %d, name is %s.".formatted(new Object[]{s.getKey(), s.getValue()}));
}

new HashMap();
}
// ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
val
val是 Lombok 引入的一個類型,其功能相當於 final var:

import lombok.val;
// ...
private static void testVal() {
val names = new ArrayList<String>();
names.add("Li Lei");
System.out.println(names.get(0));
val students = new HashMap<Integer, String>();
students.put(12, "Li Lei");
students.put(20, "Han Meimei");
for (var s : students.entrySet()) {
System.out.println("number is %d, name is %s.".formatted(s.getKey(), s.getValue()));
}
}
//...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
注意,要通過import lombok.val導入val類型到當前命名空間,否則就要用lombok.val聲明變量。

與var的區別是,這裏用val聲明的局部變量都是final的,因此不能被重新賦值。此外,val是 Lombok 的類型,因此,即使是 Java10以下的版本,也可以使用。

@NonNull
在從零開始 Spring Boot 33:Null-safety - 紅茶的個人站點 (icexmoon.cn)中,我討論過Spring框架對Null安全的支持,但那些支持都不是強制性的,僅能借助IDE的相關工具在編碼階段提供一些警告信息。

相比之下,可以藉助Lombok的@NonNull註解,實現對屬性或方法參數的強制性檢查:

@NoArgsConstructor
@Setter
@Getter
public class Book {

private Long id;
private String name;
@NonNull
private Long userId;
private Long publisherId;

public Book(@NonNull String name, Long userId, Long publisherId) {
this.name = name;
this.userId = userId;
this.publisherId = publisherId;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意,這裏使用的是lombok.NonNull,而非Spring框架或者別的庫的NonNull註解。

觀察對應的字節碼:

public class Book {
// ...
@NonNull
private Long userId;
// ...
public Book(@NonNull String name, Long userId, Long publisherId) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else {
this.name = name;
this.userId = userId;
this.publisherId = publisherId;
}
}
// ...
public void setUserId(@NonNull final Long userId) {
if (userId == null) {
throw new NullPointerException("userId is marked non-null but is null");
} else {
this.userId = userId;
}
}
// ...
@NonNull
public Long getUserId() {
return this.userId;
}
}

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
可以看到,使用@NonNull註解標記參數的方法體中被自動添加了if語句檢查相應的參數是否爲null,如果是就拋出NullPointerException異常。

如果用@NonNull標記屬性,則相應由 Lombok 自動生成的方法(這裏是setUserId)中會添加對該屬性的null檢查語句。

對於Getter,僅會用@NonNull標記,表示返回的是一個非Null值,不會添加其他的語句。

和 Spring 框架的@NonNull不同,Lombok 的@NonNull主要用於標記方法參數和屬性,但如果用於方法也不會報錯,只不過不會自動生成任何語句。

@Cleanup
在使用外部資源時,我們往往需要在最後手動關閉(這通常是使用try...catch...finally語句實現)。但是有時候我們會因爲忘記添加關閉語句而導致bug。而 Lombok 提供一個@Cleanup註解,可以幫助我們。

go語言在語言層級提供關鍵字以關閉相應的資源。

直接看示例:

private static void testCleanUp() throws IOException {
ClassPathResource classPathResource = new ClassPathResource("application.properties");
@Cleanup InputStream inputStream = classPathResource.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
do {
var line = bufferedReader.readLine();
if (line == null){
break;
}
System.out.println(line);
}
while (true);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
這裏通過Spring的Resource獲取了class:application.properties文件對應的InputStream,並且逐行讀取後輸出。最後並沒有顯式調用inputStream.close(),這是因爲我們用@Cleanup標記了inputStream變量。所以 Lombok 會自動添加上相應的關閉語句,字節碼可以說明這一點:

private static void testCleanUp() throws IOException {
ClassPathResource classPathResource = new ClassPathResource("application.properties");
InputStream inputStream = classPathResource.getInputStream();

try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

while(true) {
String line = bufferedReader.readLine();
if (line == null) {
return;
}

System.out.println(line);
}
} finally {
if (Collections.singletonList(inputStream).get(0) != null) {
inputStream.close();
}

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
我不清楚爲什麼finally中使用了Collections.singletonList而非直接的inputStream,有清楚的朋友可以在下面留言。

一般來說,資源的關閉方法都會使用close命名,但如果不是,我們也可以通過@Cleanup的value屬性進行指定。

假設我們自定義一個關閉方法是destroy的BufferedReader:

public class MyBufferedReader {
private BufferedReader bufferedReader;

public MyBufferedReader(Reader reader) {
bufferedReader = new BufferedReader(reader);
}

public void destroy() throws IOException {
bufferedReader.close();
}

public String readLine() throws IOException {
return bufferedReader.readLine();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用@Cleanup來關閉相應的資源:

@Cleanup("destroy") MyBufferedReader bufferedReader = new MyBufferedReader(new InputStreamReader(inputStream));
1
生成的字節碼:

bufferedReader.destroy();
1
@Cleanup存在一個潛在問題——如果字節碼中的try塊中出現異常,且finally中(對應關閉方法)也出現異常,那麼前邊的異常會被後邊的異常“吞掉”。

比如上邊的示例,我們強制讓readLine和destroy都拋出異常:

public class MyBufferedReader {
// ...
public void destroy() throws IOException {
throw new RuntimeException("destory is called");
}

public String readLine() throws IOException {
throw new RuntimeException("readLine is called");
}
}
1
2
3
4
5
6
7
8
9
10
最後我們只會得到destory調用時產生的異常,readLine調用時產生的異常被“吞掉”了。

這可能與使用@Cleanup的一般性預期不符,但目前因爲Java語義的關係無法解決,相應的詳細說明可以看@Cleanup (projectlombok.org)。

@Getter 和 @Setter
可以藉助 Lombok 的@Getter和@Setter註解生成屬性的 getter 和 setter。

最簡單的方式是直接在屬性上使用,生成對應的 getter 和 setter:

public class User {
@Getter
@Setter
private Long id;
@Getter
@Setter
private String name;
@Getter
@Setter
private Boolean isAdmin;
@Getter
@Setter
private boolean delFlag;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
對應的字節碼:

public class User {
private Long id;
private String name;
private Boolean isAdmin;
private boolean delFlag;

public User() {
}

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

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

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

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

public Boolean getIsAdmin() {
return this.isAdmin;
}

public void setIsAdmin(final Boolean isAdmin) {
this.isAdmin = isAdmin;
}

public boolean isDelFlag() {
return this.delFlag;
}

public void setDelFlag(final boolean delFlag) {
this.delFlag = delFlag;
}
}

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
注意,對一般性的屬性,生成的getter命名是getXXX,但如果其類型是boolean(不是Boolean),其命名是isXXX。

修改訪問權限
默認情況下生成的 Getter 和 Setter 的訪問標識符都是public,可以通過@Getter或@Setter的value屬性修改:

public class User {
@Getter
@Setter(AccessLevel.NONE)
private Long id;
@Getter
@Setter(AccessLevel.PRIVATE)
private String name;
@Getter
@Setter(AccessLevel.PACKAGE)
private Boolean isAdmin;
@Getter
@Setter(AccessLevel.PROTECTED)
private boolean delFlag;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
對應的字節碼:

public class User {
// ...
private void setName(final String name) {
this.name = name;
}
// ...
void setIsAdmin(final Boolean isAdmin) {
this.isAdmin = isAdmin;
}
// ...
protected void setDelFlag(final boolean delFlag) {
this.delFlag = delFlag;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
AccessLevel枚舉對應的用途:

AccessLevel.NONE:不會生成對應的 Getter 或 Setter。
AccessLevel.PRIVATE:生成的 Getter 或 Setter 對應的訪問修飾符是private。
AccessLevel.PACKAGE:生成的 Getter 或 Setter 對應擁有包訪問權限(即沒有訪問修飾符)。
AccessLevel.PROTECTED:生成的 Getter 或 Setter 對應的訪問修飾符是protected。
可以在類上使用@Getter或@Setter,相當於對所有屬性都使用。比如上邊的示例可以改寫爲:

@Getter
@Setter
public class User {
@Setter(AccessLevel.NONE)
private Long id;
@Setter(AccessLevel.PRIVATE)
private String name;
@Setter(AccessLevel.PACKAGE)
private Boolean isAdmin;
@Setter(AccessLevel.PROTECTED)
private boolean delFlag;
}
1
2
3
4
5
6
7
8
9
10
11
12
對應的字節碼與之前的示例完全一致。

可以看到,在類上使用的@Getter和@Setter可以被屬性上使用的@Getter和@Setter的設置覆蓋。

Setter 的級聯調用
默認情況下用@Setter生成的 Setter 返回的是void,所以不能用於“級聯調用”,如果需要,可以用@Accessors註解來實現Setter的級聯調用:

@Getter
@Setter
@Accessors(chain = true)
public class User {
private Long id;
private String name;
private Boolean isAdmin;
private boolean delFlag;
}
1
2
3
4
5
6
7
8
9
這裏設置了@Accessors的屬性chain=true,現在生成的字節碼中 Setter 將返回this,而不是void:

public class User {
// ...
public User setId(final Long id) {
this.id = var1;
return this;
}
// ...
}
1
2
3
4
5
6
7
8
所以可以用級聯調用的方式使用 Setter:

User user = new User()
.setId(1L)
.setDelFlag(false)
.setIsAdmin(true)
.setName("icexmoon");
System.out.println(user.getName());
1
2
3
4
5
6
fluent
默認情況下 Lombok 生成的 Setter 命名都是setXXX, 生成的 Getter 命名都是getXXX或isXXX,如果想要更簡潔的命名,比如直接用屬性名,可以這樣:

@Getter
@Setter
@Accessors(fluent = true, chain = true)
public class Publisher {
private Long id;
private String name;
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
通過設置@Accessors的屬性fluent=ture,可以讓 Lombok 生成的Setter 和 Getter 使用簡潔的命名。

對應的字節碼:

public class Publisher {
// ...
public Long id() {
return this.id;
}

public Publisher id(final Long id) {
this.id = id;
return this;
}
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
相應的調用示例:

private static void testAccessor2() {
Publisher publisher = new Publisher()
.id(1L)
.name("海南出版社")
.createDate(LocalDate.of(1991, 10, 1));
System.out.println(publisher.name());
}
1
2
3
4
5
6
7
boolean 屬性
從很早以前我學習 Java 開始,我就習慣於將boolean屬性命名爲isXXX,但如果使用 Lombok,這就可能會產生一些潛在問題,比如:

@Getter
public class BoolExample {
private boolean isVal1;
}
1
2
3
4
這裏的屬性名爲isVal1,類型是boolean,按照前邊所說,生成的Getter應該是isIsVal1(),這樣命名多少有些古怪,實際上 Lombok 會考慮這樣的問題,所以生成的真實的字節碼是:

public class BoolExample {
private boolean isVal1;

public BoolExample() {
}

public boolean isVal1() {
return this.isVal1;
}
}
1
2
3
4
5
6
7
8
9
10
可以看到,對於命名爲isXXX的boolean屬性,Lombok 生成的 Getter 會命名爲isXXX。

乍一看這樣並沒有說明問題,但如果這樣:

@Getter
public class BoolExample {
private boolean isVal1;
private boolean val1;
}
1
2
3
4
5
按照已經說過的規則,isVal1對應的Getter應該是isVal1,但val1對應的Getter也應該命名爲isVal1,這無疑會產生衝突,實際上最後生成的字節碼是:

public class BoolExample {
private boolean isVal1;
private boolean val1;

public BoolExample() {
}

public boolean isVal1() {
return this.isVal1;
}
}
1
2
3
4
5
6
7
8
9
10
11
可以看到,val1屬性的Getter並沒有生成。

所以,最好在Java中不要將bool或Boolean類型的屬性命名爲isXXX。

@ToString
使用@ToString可以讓 Lombok 自動生成toString方法:

@Getter
@Setter
@Accessors(fluent = true, chain = true)
@ToString
public class Publisher {
private Long id;
private String name;
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
9
System.out.println(publisher);
1
輸出:

Publisher(id=1, name=海南出版社, createDate=1991-10-01)
1
默認的輸出包含類名、屬性名和屬性值。

exclude
如果不需要輸出屬性名,可以:

@ToString(includeFieldNames = false)
1
輸出:

Publisher(1, 海南出版社, 1991-10-01)
1
如果你不希望打印某些屬性,可以:

@ToString(includeFieldNames = false, exclude = {"id"})
1
輸出:

Publisher(海南出版社, 1991-10-01)
1
也可以在不希望輸出的屬性上使用@ToString.Exclude註解,效果和上邊的等同。比如:

@ToString(includeFieldNames = false)
public class Publisher {
@ToString.Exclude
private Long id;
// ...
}
1
2
3
4
5
6
include
如果你只希望輸出某些屬性,可以:

@ToString(includeFieldNames = false, onlyExplicitlyIncluded = true)
public class Publisher {
private Long id;
@ToString.Include
private String name;
@ToString.Include
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
現在toString方法只會輸出name和createDate屬性,如果有新加入的屬性,也不會輸出。

callSuper
默認情況下 Lombok 生成的toString方法並不會調用父類的toString方法,比如:

@Setter
@Getter
@Accessors(chain = true, fluent = true)
@ToString
public class SpecialPublisher extends Publisher{
private String admin;
}
1
2
3
4
5
6
7
測試:

private static void testToString() {
Publisher publisher = new SpecialPublisher()
.admin("icexmoon")
.name("海南出版社")
.id(1L)
.createDate(LocalDate.of(1991, 10, 1));
System.out.println(publisher);
}
1
2
3
4
5
6
7
8
輸出:

SpecialPublisher(admin=icexmoon)
1
輸出只包含了子類SpecialPublisher中的屬性。

如果需要包含父類的輸出,可以:

@ToString(callSuper = true)
public class SpecialPublisher extends Publisher{
private String admin;
}
1
2
3
4
輸出:

SpecialPublisher(super=Publisher(海南出版社, 1991-10-01), admin=icexmoon)
1
輸出方法返回值
如果希望 Lombok 生成的toString方法輸出中包含某些方法的返回值,可以:

@ToString(callSuper = true)
public class SpecialPublisher extends Publisher {
private String admin;

@ToString.Include
private String hello() {
return "歡迎來到" + this.name() + "出版社";
}
}
1
2
3
4
5
6
7
8
9
輸出:

SpecialPublisher(super=Publisher(海南出版社, 1991-10-01), admin=icexmoon, hello=歡迎來到海南出版社出版社)
1
最後的輸出中包含了@ToString.Include標記的方法的返回值。需要注意的是,用於輸出的方法不能是靜態(static)的,且不能包含任何參數(空參數列表)。

屬性展示名稱
可以用@ToString.Include的name屬性修改toString輸出時的屬性名稱:

@ToString
public class Publisher {
@ToString.Include(name = "編號")
private Long id;
@ToString.Include(name = "出版社名稱")
private String name;
@ToString.Include(name = "創建時間")
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
9
輸出:

Publisher(編號=1, 出版社名稱=海南出版社, 創建時間=1991-10-01)
1
排序
可以用@ToString.Include的rank屬性修改toString輸出屬性的順序:

@ToString
public class Publisher {
@ToString.Include(name = "編號")
private Long id;
@ToString.Include(name = "出版社名稱", rank = 100)
private String name;
@ToString.Include(name = "創建時間", rank = 99)
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
9
輸出:

Publisher(出版社名稱=海南出版社, 創建時間=1991-10-01, 編號=1)
1
rank越大,在輸出時越靠前。默認情況下rank是0,且rank可以爲負數。

@EqualsAndHashCode
可用註解@EqualsAndHashCode生成equals和hashCode方法:

@EqualsAndHashCode
public class Publisher {
@ToString.Include(name = "編號")
private Long id;
@ToString.Include(name = "出版社名稱", rank = 100)
private String name;
@ToString.Include(name = "創建時間", rank = 99)
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
9
生成的字節碼:

public class Publisher {
// ...
public boolean equals(final Object o) {
// ...
}
public int hashCode() {
// ...
}
}
1
2
3
4
5
6
7
8
9
equals和hashCode的詳細代碼可以下載文末的完整示例後自己編譯查看。

Include
默認情況下生成的equals和hashCode會使用所有的非static屬性,換言之,調用equals方法進行比較時,所有屬性都相等才能返回true。

有時候我們僅希望比較某些作爲“主鍵”的屬性,比如:

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Publisher {
@EqualsAndHashCode.Include
@ToString.Include(name = "編號")
private Long id;
@ToString.Include(name = "出版社名稱", rank = 100)
private String name;
@ToString.Include(name = "創建時間", rank = 99)
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
9
10
現在只要id屬性相等,兩個Publisher對象就相等(equals返回true)。

Exclude
和@ToString類似,也可以使用“排除模式”:

@EqualsAndHashCode
public class Publisher {
@ToString.Include(name = "編號")
private Long id;
@ToString.Include(name = "出版社名稱", rank = 100)
@EqualsAndHashCode.Exclude
private String name;
@ToString.Include(name = "創建時間", rank = 99)
@EqualsAndHashCode.Exclude
private LocalDate createDate;
}
1
2
3
4
5
6
7
8
9
10
11
callSuper
如果要將@EqualsAndHashCode應用於子類,通常需要考慮父類的equals和hashCode方法,這可以用@EqualsAndHashCode的callSuper屬性實現:

@EqualsAndHashCode(callSuper = true)
public class SpecialPublisher extends Publisher {
@EqualsAndHashCode.Exclude
private String admin;

@ToString.Include
private String hello() {
return "歡迎來到" + this.name() + "出版社";
}
}
1
2
3
4
5
6
7
8
9
10
在這個示例中,我們僅希望用Publisher.id這個屬性來作爲比較和生成哈希值的依據,所以子類的admin屬性也被我們排除了。

生成構造器
Lombok 提供一些註解用於自動生成構造器:

@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
@NoArgsConstructor可以生成一個空的構造器:

@NoArgsConstructor
public class User {
private Long id;
private String name;
private Boolean isAdmin;
private boolean delFlag;
}
1
2
3
4
5
6
7
字節碼:

public class User {
public User() {
}
}
1
2
3
4
如果有final屬性,這樣做會導致一個編譯錯誤:

@NoArgsConstructor
public class User {
private final Long id;
private String name;
private Boolean isAdmin;
private boolean delFlag;
}
1
2
3
4
5
6
7
錯誤信息:

java: 可能尚未初始化變量id
1
這時候可以:

@NoArgsConstructor(force = true)
1
Lombok 生成的字節碼中會將final屬性用零值強制初始化:

public class User {
private final Long id = null;
// ...
}
1
2
3
4
不過這樣做似乎沒有什麼意義,且可能造成潛在bug,所以儘量還是不要這麼做。

@RequiredArgsConstructor
@RequiredArgsConstructor可以爲“需要的屬性”生成一個用於初始化的構造器。

這裏“需要的屬性”,指用final或@NonNull修飾且沒有被初始化的屬性。

示例:

@RequiredArgsConstructor
public class User {
private final Long id;
@NonNull
private String name;
@NonNull
private Boolean isAdmin = false;
private boolean delFlag;
}
1
2
3
4
5
6
7
8
9
字節碼:

public class User {
// ...
public User(final Long id, @NonNull final String name) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else {
this.id = id;
this.name = name;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
構造器中也會加入對@NonNull字段null檢查的if語句,這點在之前的@NonNull中有過介紹。

@AllArgsConstructor
@AllArgsConstructor會爲所有屬性生成一個構造器:

@AllArgsConstructor
public class User {
private final Long id;
@NonNull
private String name;
@NonNull
private Boolean isAdmin = false;
private boolean delFlag;
}
1
2
3
4
5
6
7
8
9
字節碼:

public class User {
// ...
public User(final Long id, @NonNull final String name, @NonNull final Boolean isAdmin, final boolean delFlag) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else if (isAdmin == null) {
throw new NullPointerException("isAdmin is marked non-null but is null");
} else {
this.id = id;
this.name = name;
this.isAdmin = isAdmin;
this.delFlag = delFlag;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
staticName
上邊的構造器都提供另外一種形式——將構造器本身定義爲private,並提供一個static方法進行調用。

比如下面的示例:

@AllArgsConstructor(staticName = "of")
public class User {
private final Long id;
@NonNull
private String name;
@NonNull
private Boolean isAdmin = false;
private boolean delFlag;
}
1
2
3
4
5
6
7
8
9
對應的字節碼:

public class User {
// ...
private User(final Long id, @NonNull final String name, @NonNull final Boolean isAdmin, final boolean delFlag) {
// ...
}

public static User of(final Long id, @NonNull final String name, @NonNull final Boolean isAdmin, final boolean delFlag) {
return new User(id, name, isAdmin, delFlag);
}
}
1
2
3
4
5
6
7
8
9
10
@Data
@Data註解相當於同時使用了以下註解:

@Setter
@Getter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
比如下面的示例:

@Setter
@Getter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
public class Employee {
private final Long id;
@NonNull
private String name;
@NonNull
private Boolean delFlag;
}
1
2
3
4
5
6
7
8
9
10
11
12
和下面的是等效的:

@Data
public class Employee {
private final Long id;
@NonNull
private String name;
@NonNull
private Boolean delFlag;
}
1
2
3
4
5
6
7
8
實際上@Data通常用來爲實體類(POJO)提供基本的構造器、Setter和Getter,以及equals和hashCode方法。

如果我們需要爲某個註解提供更詳細的設置,比如將Employee的id視作主鍵,用於比較和生成哈希值,以及輸出的toString方法不包含鍵名和delFlag,可以在使用@Data註解的基礎上使用對應的註解來設置:

@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(includeFieldNames = false)
public class Employee {
@EqualsAndHashCode.Include
private final Long id;
@NonNull
private String name;
@NonNull
@ToString.Exclude
private Boolean delFlag;
}
1
2
3
4
5
6
7
8
9
10
11
12
staticConstructor
類似於@AllArgsConstructor等,@Data同樣可以將構造器設置爲私有的,同時提供一個static方法用於調用構造器,比如:

@Data
public class Student<T> {
private final Long id;
@NonNull
private String name;
@NonNull
private Integer age;
@NonNull
private T something;
}
1
2
3
4
5
6
7
8
9
10
需要用以下方式創建對象:

Student<String> s = new Student<>(1L, "icexmoon", 20, "hello");
1
可以修改爲:

@Data(staticConstructor = "of")
public class Student<T> {
private final Long id;
@NonNull
private String name;
@NonNull
private Integer age;
@NonNull
private T something;
}
1
2
3
4
5
6
7
8
9
10
此時這樣調用:

Student<String> s = Student.of(1L, "icexmoon", 20, "hello");
1
因爲靜態方法會通過傳入參數的類型來確定泛型參數,所以在使用Student.of時並不需要指定方法的泛型參數。

@Value
用@Value可以創建一些“只讀”性質的類型:

@Value
public class BookCategory {
Long id;
String name;
String desc;
}
1
2
3
4
5
6
對應的字節碼:

public final class BookCategory {
private final Long id;
private final String name;
private final String desc;

public BookCategory(final Long id, final String name, final String desc) {
this.id = id;
this.name = name;
this.desc = desc;
}

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

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

public String getDesc() {
return this.desc;
}

public boolean equals(final Object o) {
// ...
}

public int hashCode() {
// ...
}

public String toString() {
// ...
}
}

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
可以看到,在字節碼中,BookCategory的非static屬性被private final修飾,且只生成了Getter,沒有生成Setter。所以BookCategory的屬性只能在生成的構造器中被初始化,且不能通過其他方式修改。

此外,BookCategory本身也被final修飾,也就是說被@Value標記的類不能被繼承。

所有上邊這些特性,都標識——被@Value標記的類可以作爲一個只讀的“數據類”來使用。

等效寫法
實際上@Value相當於下面的寫法:

@ToString
@EqualsAndHashCode
@AllArgsConstructor
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public final class BookCategory {
Long id;
String name;
String desc;
}
1
2
3
4
5
6
7
8
9
10
示例
通常,在進行Web編程時,我們可以利用@Value來創建DTO,因爲這些DTO類用於傳遞數據,他們的屬性在初始化後就不應該被修改。

比如下面這個示例:

@Value
public class BookCategory {
Long id;
String name;
String desc;

public static BookCategory newInstance(BookCategoryController.AddCategoryDTO dto) {
return new BookCategory(null, dto.getName(), dto.getDesc());
}
}

@Value
public class Result<T> {
boolean success;
String errorCode;
String errorMsg;
T data;

public static <T> Result<T> success(T data) {
return new Result<T>(true, "", "", data);
}

public static Result<Object> success() {
return success(null);
}
}

@RestController
@RequestMapping("/book/category")
public class BookCategoryController {
@Value
public static class AddCategoryDTO {
@NotBlank String name;
@NotBlank String desc;
}

@PostMapping
public Result<Object> addCategory(@Validated @RequestBody AddCategoryDTO addCategoryDTO) {
System.out.println(addCategoryDTO);
//用DTO生成POJO
BookCategory bookCategory = BookCategory.newInstance(addCategoryDTO);
System.out.println(bookCategory);
//用POJO在持久層添加新的圖書類別
//這裏省略持久層調用
return Result.success();
}
}

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
這裏充當POJO的BookCategory、充當DTO的AddCategoryDTO,以及用於標準化返回的Result,都用@Value標識。因爲這些類實際上都充當了傳遞數據的角色,並不涉及會改變內部屬性的複雜業務邏輯。

事實上這些用於簡單傳遞數據的類,從Java 10開始,可以用標準庫的Record來實現,這點在之後的文章說明。

@Builder
利用@Builder可以爲類創建一個“創建器”,利用這個創建器可以創建對象。

比如下面這個示例:

@Builder
@Getter
@ToString
public class Person {
private final String name;
private final String city;
private final String job;
}
1
2
3
4
5
6
7
8
生成的字節碼如下:

public class Person {
private final String name;
private final String city;
private final String job;

Person(final String name, final String city, final String job) {
this.name = name;
this.city = city;
this.job = job;
}

public static Person.PersonBuilder builder() {
return new Person.PersonBuilder();
}

// ... 這裏是一些Getter 和 toString ...

public static class PersonBuilder {
private String name;
private String city;
private String job;

PersonBuilder() {
}

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

public Person.PersonBuilder city(final String city) {
this.city = city;
return this;
}

public Person.PersonBuilder job(final String job) {
this.job = job;
return this;
}

public Person build() {
return new Person(this.name, this.city, this.job);
}

public String toString() {
return "Person.PersonBuilder(name=" + this.name + ", city=" + this.city + ", job=" + this.job + ")";
}
}
}

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
@Builder會爲類創建一個包含所有非靜態屬性的構造器和一個靜態的內嵌類xxxBuilder,這個內嵌類包含所有外部類的非靜態屬性,並且可以利用這個內嵌類的一系列方法來一步步生成外部類的對象。

比如下面這樣:

private static void testBuilder() {
var p = Person.builder().name("icexmoon")
.city("NanJin")
.job("Programmer")
.build();
System.out.println(p);
}
1
2
3
4
5
6
7
這樣做的好處在於,雖然外部類Person的屬性都是final的,並且只有Getter沒有Setter,但是我們可以藉助內部類PersonBuilder來靈活地設置屬性和生成對象。這在我們想用一個“只讀”類,但是又不想用死板的構造器一次性初始化的情況下會格外有用。

要注意,生成的內嵌類xxxBuilder僅會爲外部類未初始化的屬性添加對應的內嵌類屬性,並生成對應的內嵌類的Setter方法,並最終用於構建外部類對象。外部類被顯式初始化的屬性不在此列。

@Singular
默認情況下容器類型的屬性的處理與其他屬性一致,比如:

@Builder
@Getter
@ToString
public class Person {
private final String name;
private final String city;
private final String job;
private final List<String> hobbies;
}
1
2
3
4
5
6
7
8
9
調用示例:

private static void testBuilder() {
var p = Person.builder().name("icexmoon")
.city("NanJin")
.job("Programmer")
.hobbies(List.of("play games", "travel"))
.build();
System.out.println(p);
}
1
2
3
4
5
6
7
8
這裏PersonBuilder.hobbies僅是簡單地用傳入的List作爲最終的外部類對象的hobbies屬性。

@Builder還提供一種模式:

@Builder
@Getter
@ToString
public class Person {
private final String name;
private final String city;
private final String job;
@Singular
private List<String> hobbies;
}
1
2
3
4
5
6
7
8
9
10
注意,這裏的hobbies沒有被final修飾,實際測試時如果有final,就無法生成PersonBuilder.hobbies等相關的Setter方法,不知道是不是Bug。

生成的字節碼:

// ...
public class Person {
// ...
public static class PersonBuilder {
// ...
public Person.PersonBuilder hobby(final String hobby) {
if (this.hobbies == null) {
this.hobbies = new ArrayList();
}

this.hobbies.add(hobby);
return this;
}

public Person.PersonBuilder hobbies(final Collection<? extends String> hobbies) {
if (hobbies == null) {
throw new NullPointerException("hobbies cannot be null");
} else {
if (this.hobbies == null) {
this.hobbies = new ArrayList();
}

this.hobbies.addAll(hobbies);
return this;
}
}
// ...
}
}

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
可以看到,PersonBuilder.hobbies的行爲改變了,變成用傳入的List與已有List合併,此外還有一個新的PersonBuilder.hobby方法,可以用這個方法逐一向List添加元素。

調用示例:

private static void testBuilder() {
var p = Person.builder().name("icexmoon")
.city("NanJin")
.job("Programmer")
.hobbies(List.of("play games", "travel"))
.hobbies(List.of("draw"))
.hobby("music")
.hobby("movie")
.build();
System.out.println(p);
}
1
2
3
4
5
6
7
8
9
10
11
@Value
@Builder可以和@Value一同使用,比如之前的示例可以改寫爲:

@Builder
@Value
public class Person {
String name;
String city;
String job;
@Singular
List<String> hobbies;
}
1
2
3
4
5
6
7
8
9
要注意的是,@Value會生成一個包含了所有屬性的public構造器,而@Builder會生成一個包含所有屬性的包訪問權限的構造器,兩者會發生衝突,此時後者會產生而前者不會。

對應的字節碼和調用示例與之前的幾乎一致,這裏不再展示。

奇怪的是這裏@Value會讓hobbies變成final的,但是依然可以正常生成PersonBuilder.hobbies。只能認爲之前的是個Bug。

@Builder.Default
可以用@Builder.Default爲Builder構建外部類時提供默認值(如果沒有設置相應的值的話):

@Builder
@Value
public class Person {
String name;
String city;
String job;
@Singular
List<String> hobbies;
@Builder.Default
LocalDateTime createTime = LocalDateTime.now();
}
1
2
3
4
5
6
7
8
9
10
11
對應的字節碼:

public final class Person {
// ...
private final LocalDateTime createTime;

private static LocalDateTime $default$createTime() {
return LocalDateTime.now();
}
// ...
public static class PersonBuilder {
// ...
private boolean createTime$set;
private LocalDateTime createTime$value;

public Person.PersonBuilder createTime(final LocalDateTime createTime) {
this.createTime$value = createTime;
this.createTime$set = true;
return this;
}

public Person build() {
// ...
LocalDateTime createTime$value = this.createTime$value;
if (!this.createTime$set) {
createTime$value = Person.$default$createTime();
}
return new Person(this.name, this.city, this.job, hobbies, createTime$value);
}
}
}

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
@SneakyThrows
如果我們的代碼中包含方法聲明中有throws指明會拋出一個“被檢查異常”的代碼,那我們只有兩種解決方式:

在當前方法聲明中添加throws語句,指明當前方法也可能拋出該類型的“被檢查異常”。
用try...catch捕獲該異常,並處理(通常是將其包裝成一個RuntimeException並拋出。
之所以Java會這樣設計,是因爲早期Java的設計者認爲某些異常必須要被調用放顯式處理纔行。但實際運用中,一層層調用過程中都要拋出一個“被檢查異常”是相當繁瑣的,且必須在每一層方法聲明中都添加對應的throws語句,所以將異常轉化成RuntimeException並拋出的解決方案使用頻率反而更多。

但是,這種方式需要我們編寫一些額外代碼(try...catch語句),因此 Lombok 提供一個@SneakyThrows註解,可以幫助我們更簡單的實現一個替代解決方案,並只需要添加一個註解。

看下面這個示例:

private static void callTestThrow() throws IOException{
testThrow();
}

private static void testThrow() throws IOException{
@Cleanup InputStream inputStream = new ClassPathResource("application.properties").getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
while (true){
String line = bufferedReader.readLine();
if (line == null){
break;
}
System.out.println(line);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
創建輸入流的相關代碼可能會產生一個被檢查異常IOException,因此我們需要在testThrow方法聲明中添加throws語句,這是Java語法強制要求的。並且,調用該方法的其他方法,比如callTestThrow,同樣需要處理這個被檢查異常。

當然我們可以利用try...catch將其轉換爲“非檢查異常”:

private static void callTestThrow() {
testThrow();
}

private static void testThrow() {
try {
@Cleanup InputStream inputStream = new ClassPathResource("application.properties").getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
System.out.println(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SneakyThrows給了我們第三種選擇:

private static void callTestThrow() {
testThrow();
}

@SneakyThrows(IOException.class)
private static void testThrow() {
@Cleanup InputStream inputStream = new ClassPathResource("application.properties").getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
System.out.println(line);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看到,@SneakyThrows(IOException.class)的效果與使用try...catch轉換異常是一樣的,調用方同樣不需要顯式處理異常。

我們看對應的字節碼:

private static void callTestThrow() {
testThrow();
}

private static void testThrow() {
try {
// ...
} catch (IOException var7) {
throw var7;
}
}
1
2
3
4
5
6
7
8
9
10
11
這樣看起來很奇怪,testThrow捕獲了IOException異常並原樣拋出,並且callTestThrow中也沒有處理這個“被檢查異常”,這樣並不符合Java語法。

Lombok 官方文檔對此的說法是 Lombok 通過某種方式在JVM層面“欺騙”了編譯器,所以可以實現類似的效果。

官方文檔中的示例對應的字節碼實現與這裏我實際測試中產生的字節碼有出入(throw Lombok.sneakyThrow(t)),原因不明。
Sneaky 一詞在英語中有“悄悄地”意思,因此@SneakyThrows的用途可以被理解爲“悄悄地拋出一個被檢查異常”。
關於Java異常的更多內容,可以閱讀Java編程筆記10:異常 - 紅茶的個人站點 (icexmoon.cn)。
@Synchronized
在Java中,可以通過synchronized給方法調用“加鎖”,並且這種方式可以和用synchronized語句塊用this作爲臨界區的寫法是可以協同工作的,比如:

public class ShareData {
public synchronized void func1() {
for (int i = 0; i < 5; i++) {
System.out.println("func1() is called.");
Thread.yield();
}
}

public void func2() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println("func2() is called.");
Thread.yield();
}
}
}
}

@SpringBootApplication
public class LombokApplication {
// ...
private static void testSyncronize() {
var sd = new ShareData();
new Thread(() -> sd.func1()).start();
new Thread(() -> sd.func2()).start();
}
}

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
輸出:

func1() is called.
func1() is called.
func1() is called.
func1() is called.
func1() is called.
func2() is called.
func2() is called.
func2() is called.
func2() is called.
func2() is called.
1
2
3
4
5
6
7
8
9
10
這是一種特性,但有時候你或許不希望使用它。比如你可能擔心某些用this作爲synchronized(...){}語句臨界區的代碼其本意並非是與synchronized方法互斥。

這種問題可以通過使用@Synchronized註解來解決,比如:

public class ShareData {
@Synchronized
public void func1() {
for (int i = 0; i < 5; i++) {
System.out.println("func1() is called.");
Thread.yield();
}
}

public void func2() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println("func2() is called.");
Thread.yield();
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
輸出:

func1() is called.
func2() is called.
func2() is called.
func2() is called.
func2() is called.
func1() is called.
func1() is called.
func1() is called.
func1() is called.
func2() is called.
1
2
3
4
5
6
7
8
9
10
可以看到func1和func2的相關代碼實際上是並行的,並非互斥。

對應的字節碼:

public class ShareData {
private final Object $lock = new Object[0];

public ShareData() {
}

public void func1() {
synchronized(this.$lock) {
for(int i = 0; i < 5; ++i) {
System.out.println("func1() is called.");
Thread.yield();
}
}
}

public void func2() {
synchronized(this) {
for(int i = 0; i < 5; ++i) {
System.out.println("func2() is called.");
Thread.yield();
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
可以看到,實際上@Synchronized同樣是使用synchronized(...){}語句實現的,不過臨界快並非使用的this,而是 Lombok 自己添加的靜態屬性$lock。因此,和使用this作爲臨界區的synchronized塊並不互斥。

更多synchronized和併發內容可以閱讀Java學習筆記21:併發(1) - 紅茶的個人站點 (icexmoon.cn)。

如果對靜態方法使用@Synchronized,Lombok 會創建一個$LOCK屬性作爲臨界區:

public class ShareData {
// ...
@Synchronized
public static void func3(){
for (int i = 0; i < 5; i++) {
System.out.println("func3() is called.");
Thread.yield();
}
}
}
1
2
3
4
5
6
7
8
9
10
對應的字節碼:

public class ShareData {
// ...
private static final Object $LOCK = new Object[0];
// ...
public static void func3() {
synchronized($LOCK) {
for(int i = 0; i < 5; ++i) {
System.out.println("func3() is called.");
Thread.yield();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
值得注意的是,作爲臨界區的$lock和$LOCK都是new Object[0](即一個元素類型爲Object且長度爲0的數組),而不是一般會使用的new Object。這樣做的好處是前者是可以序列化的,而後者不行。而序列化的時候必須確保所有屬性都可以被序列化,因此前者不會阻止所在的類變成一個可序列化的類(implements Serializable),而後者會,所以使用前者會更好一些。
關於序列化的更多內容可以閱讀Java編程筆記18:I/O(續) - 紅茶的個人站點 (icexmoon.cn)中的序列化部分。
指定臨界區
使用@Synchronized時也可以自己指定一個屬性作爲臨界區,比如:

public class ShareData {
private final Object lock1 = new Object[0];
private static final Object lock2 = new Object[0];

@Synchronized("lock1")
public void func1() {
//...
}
//...
@Synchronized("lock2")
public static void func3(){
//...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
對應的字節碼:

public class ShareData {
private final Object lock1 = new Object[0];
private static final Object lock2 = new Object[0];
public void func1() {
synchronized(this.lock1) {
// ...
}
}
// ...
public static void func3() {
synchronized(lock2) {
// ...
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看到,此時 Lombok 不會再添加$lock或$LOCK,而是使用指定的屬性作爲synchronized塊的臨界區。

@With
有時候雖然是一個“只讀”的類,我們依然希望修改其中的某個屬性,比如:

@Value
public class Dog {
String name;
Integer age;
}
1
2
3
4
5
Dog的屬性都是private final的,顯然它也不可能有setter。所以正常情況下我們是沒法修改其中的屬性的,但是我們可以選擇創建一個新的和對象,只不過該對象中的屬性都與原來對象一致,除了一個我們想變更的屬性。比如下面的示例:

private static void testWith() {
Dog dog = new Dog("audi",11);
Dog dog2 = new Dog(dog.getName(), 2);
System.out.println(dog);
System.out.println(dog2);
}
1
2
3
4
5
6
不過上面的寫法多少有點冗餘,這時候自然是 Lombok 派上用場的時候了:

@Value
@With
public class Dog {
String name;
Integer age;
}
1
2
3
4
5
6
對應的字節碼:

public final class Dog {
// ...
public Dog withName(final String name) {
return this.name == name ? this : new Dog(name, this.age);
}

public Dog withAge(final Integer age) {
return this.age == age ? this : new Dog(this.name, age);
}
}
1
2
3
4
5
6
7
8
9
10
要注意的是,Lombok 生成的withXXX方法的處理邏輯是用==(不是equals)比較屬性值,如果與原始值一樣,就返回原始對象(this),否則創建新對象。
withXXX方法是用構造器創建新對象,因此@With標記的類必須有一個包含所有屬性的構造器(可以用@AllArgsConstructor創建)。
此時上面的調用示例就可以改寫爲:

private static void testWith() {
Dog dog = new Dog("audi",11);
Dog dog2 = dog.withAge(2);
System.out.println(dog);
System.out.println(dog2);
}
1
2
3
4
5
6
只有兩個屬性的Dog並不能說明便利性,但假如屬性很多,使用@With就會省很多事。

特定屬性
可以只對特定屬性生成withXXX方法而非所有屬性:

@Value
public class Dog {
String name;
@With
Integer age;
}
1
2
3
4
5
6
生成的字節碼中只會有withAge方法,而不會有withName方法。

訪問權限
默認情況下@With生成的withXXX方法的訪問權限是public,也可以指定其他訪問權限,比如:

@Value
public class Dog {
String name;
@With(AccessLevel.PACKAGE)
Integer age;
}
1
2
3
4
5
6
此時生成的withAge是包訪問權限。

@Getter(lazy=true)
有時候,對於final屬性,會在聲明時進行一些複雜(消耗時間)的初始化工作,比如:

@Getter
public class LazyExample {
private final long bigFibnacci = fibonacci(30);

private static long fibonacci(int n) {
if (n <= 2) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
1
2
3
4
5
6
7
8
9
10
11
如果對這個屬性的使用並不是在對象創建後立即進行,我們可以將這種初始化動作延後,以減少對象創建時所消耗的時間。比如:

public class LazyExample {

private Long bigFibnacci;

private static long fibonacci(int n) {
if (n <= 2) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}

public Long getBigFibnacci() {
if (bigFibnacci == null){
bigFibnacci = fibonacci(30);
}
return bigFibnacci;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
這裏的優化方案實際上並沒有考慮到多線程調用的情況,因此是線程不安全的。

實際上 Lombok 的@Getter(lazy=true)可以幫助我們更容易地實現類似的代碼:

public class LazyExample {
@Getter(lazy = true)
private final Long bigFibnacci = fibonacci(30);

private static long fibonacci(int n) {
if (n <= 2) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
1
2
3
4
5
6
7
8
9
10
11
對應的字節碼:

public class LazyExample {
private final AtomicReference<Object> bigFibnacci = new AtomicReference();

public LazyExample() {
}

private static long fibonacci(int n) {
return n <= 2 ? 1L : fibonacci(n - 1) + fibonacci(n - 2);
}

public Long getBigFibnacci() {
Object value = this.bigFibnacci.get();
if (value == null) {
synchronized(this.bigFibnacci) {
value = this.bigFibnacci.get();
if (value == null) {
Long actualValue = fibonacci(30);
value = actualValue == null ? this.bigFibnacci : actualValue;
this.bigFibnacci.set(value);
}
}
}

return (Long)(value == this.bigFibnacci ? null : value);
}
}

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
可以看到,Lombok 自動幫助我們實現了類似的代碼,且使用了原子操作的相關類AtomicReference,以及synchronized語句,所以用@Getter(lazy=true)實現的類似優化(延遲初始化)是可以用於多線程的,是線程安全的。

潛在問題
就像我們看到的,如果你用了@Getter(lazy=true),那麼在類中調用該字段時就必須用getXXX獲取屬性值,否則你獲取到的就是一個AtomicReference<Object>類型的對象,並且該對象還沒有進行過“初始化”。

@Log
使用@Log註解可以更方便地輸出調試信息,比如:

@RestController
@RequestMapping("/book/category")
@Log
public class BookCategoryController {
// ...
@PostMapping
public Result<Object> addCategory(@Validated @RequestBody AddCategoryDTO addCategoryDTO) {
System.out.println(addCategoryDTO);
log.log(Level.INFO, addCategoryDTO.toString());
// ...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
輸出:

// ...
BookCategoryController.AddCategoryDTO(name=文學, desc=包括中國文學,外國文學等)
2023-06-04T10:42:03.928+08:00 INFO 17536 --- [nio-8080-exec-1] c.e.l.controller.BookCategoryController : BookCategoryController.AddCategoryDTO(name=文學, desc=包括中國文學,外國文學等)
// ...
1
2
3
4
可以看到,通過日誌輸出比起直接通過System.out輸出會顯示更多信息,比如時間、線程編號和名稱、日誌級別等。

除了以上好處外,還包括:

可以通過設置方便地輸出到文件。
可以通過設置讓不同的運行環境(開發環境、測試環境等)輸出不同包下不同的日誌級別的日誌。
當然,具體使用時還和你的Java應用使用的框架以及日誌模塊相關,比如 Spring Boot 默認使用 logback 作爲日誌模塊,且支持多種方式的日誌調用API,實際上上邊的@Log就是導入了java.util.logging的相關日誌API。這點在字節碼中有體現:

package com.example.lombok.controller;

// ...
import java.util.logging.Logger;
// ...
public class BookCategoryController {
private static final Logger log = Logger.getLogger(BookCategoryController.class.getName());
// ...
}
1
2
3
4
5
6
7
8
9
當然也可以使用別的API,比如l4j2的:

@Log4j2
public class BookCategoryController {
// ...
@PostMapping
public Result<Object> addCategory(@Validated @RequestBody AddCategoryDTO addCategoryDTO) {
System.out.println(addCategoryDTO);
log.debug(addCategoryDTO);
// ...
}
}
1
2
3
4
5
6
7
8
9
10
對應的字節碼:

package com.example.lombok.controller;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
// ...
public class BookCategoryController {
private static final Logger log = LogManager.getLogger(BookCategoryController.class);
// ...
}
1
2
3
4
5
6
7
8
9
Spring Boot 默認不輸出 DEBUG 級別日誌,所以這裏還需要在配置文件中添加logging.level.com.example.lombok=debug。

最後,總結一下,通過使用 Lombok 日誌相關注解,可以更方便的引入和調用不同日誌模塊的API。

如果想了解其它日誌模塊對應的 Lombok 註解,可以閱讀@Log (and friends) (projectlombok.org)。
如果想了解 Spring Boot 中的日誌使用,可以閱讀從零開始 Spring Boot 10:日誌 - 紅茶的個人站點 (icexmoon.cn)和從零開始 Spring Boot 34:日誌 II - 紅茶的個人站點 (icexmoon.cn)。
如果想了解 Lombok 的更多用法和說明,可以前往官方文檔。

The End,謝謝閱讀。

本文的完整示例可以從這裏獲取。

參考資料
JEP 286: Local-Variable Type Inference (openjdk.org)
var (projectlombok.org)
@NonNull (projectlombok.org)
Introduction to Project Lombok
@Cleanup (projectlombok.org)
@Getter and @Setter (projectlombok.org)
@ToString (projectlombok.org)
@EqualsAndHashCode (projectlombok.org)
@Data (projectlombok.org)
Introduction to Project Lombok | Baeldung
@Value (projectlombok.org)
@Builder (projectlombok.org)
@Synchronized (projectlombok.org)
@With (projectlombok.org)
@Getter(lazy=true)(lazy=true) (projectlombok.org)
從零開始 Spring Boot 10:日誌 - 紅茶的個人站點 (icexmoon.cn)
從零開始 Spring Boot 34:日誌 II - 紅茶的個人站點 (icexmoon.cn)
@Log (and friends) (projectlombok.org)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章