前面在介紹使用 Spring Data JPA 的時候,在關聯的部分有碰到一些使用 Lombok 的問題,這篇就來介紹一下個別的註解作用,也可以讓大家使用的時候也知道背後運作的邏輯。

Lombok 是一個套件可以幫助我們定義物件時省去許多重複冗長的 code ,可以大幅提升開發效率,下面是常見的註解。

套件引入

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

常用註解介紹

https://ithelp.ithome.com.tw/upload/images/20240928/20150977eUFDzWPaBM.png

小補充: 其實有很多方法如果是用 Intellij 都可以在物件內點選右鍵使用 generate 產生,但是如果可以用一個註解就解決這些還是來得方便很多,但建議剛開始新手可以先用這些 generate 來產生對應方法,也可以藉此了解每個註解背後的原始 code 是怎麼運作。

https://ithelp.ithome.com.tw/upload/images/20240928/20150977gPXIq87igK.png

https://ithelp.ithome.com.tw/upload/images/20240928/20150977mvcWRtENVH.png

@Getter / @Setter

進行物件導向的撰寫必備的方法,用來取得或修改物件屬性

1
2
3
4
5
6
@Getter
@Setter
public class product {
private Integer id;
private String name;
}

(原始定義方法) 效果同上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class product {
private Integer id;
private String name;

public Integer getId() {
return id;
}

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

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

public String getName() {
return name;
}
}

@ToString

自動 override toString()  方法,使用時會印出所有屬性

1
2
3
4
5
@ToString
public class product {
private Integer id;
private String name;
}

(註解生效原始碼) 效果同上

1
2
3
4
5
6
7
8
9
10
11
public class product {
private Integer id;
private String name;

public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

@NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructor

起始子 (Constructor) 的創建,有三種註解是根據創建時是否包含內部屬性作為參數來決定。

例如有個 product 有 id, name 的屬性

  • @AllArgsConstructor 自動帶入當前所有屬性作為可建立時的值
1
2
3
4
5
@AllArgsConstructor
public class product {
private Integer id;
private String name;
}

(註解生效原始碼) 效果同上,使用註解表示我們獲得一個創建物件時可以帶入這些屬性並賦予值的方法

1
2
3
4
5
6
7
8
9
public class product {
private Integer id;
private String name;

public Product(Integer id, String name) {
this.id = id;
this.name = name;
}
}
  • @NoArgsConstructor 自動產生一個空的起始子
1
2
3
4
5
@NoArgsConstructor
public class product {
private Integer id;
private String name;
}

(註解生效原始碼) 效果同上,創建時就只是建立出空的物件,但沒辦法帶入任何屬性

1
2
3
4
5
6
7
8
public class product {
private Integer id;
private String name;

public Product() {

}
}
  • @RequiredArgsConstructor 可以讓你建立一個包含 “final 修飾屬性” 的起始子。
1
2
3
4
5
@RequiredArgsConstructor
public class product {
private final Integer id;
private String name;
}

(註解生效原始碼) 效果同上,使用註解後可以看到只有帶 final 的屬性可以被帶入建立

1
2
3
4
5
6
7
8
public class product {
private final Integer id;
private String name;

public Product(Integer id) {
this.id = id;
}
}

**補充如果所有的屬性都沒有用 final 修飾的話,那就會生成一個沒有參數的起始子,等同 @NoArgsConstructor

@EqualsAndHashCode

可以產生 equals(Object other)  和  hashcode()  方法

1
2
3
4
5
6
@EqualsAndHashCode
public class Product {
private int id;
private String name;
private int price;
}

(註解生效原始碼) 效果同上,可以看到這兩個方法會 Override 原本 equals(0 和 hashcode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Product {
private int id;
private String name;
private int price;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return id == product.id && price == product.price && Objects.equals(name, product.name);
}

@Override
public int hashCode() {
return Objects.hash(id, name, price);
}
}

舉例來說如果比較兩個物件,內部屬性相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
SpringApplication.run(MybatisDemoApplication.class, args);
Product product = Product.builder()
.id(1)
.name("book")
.price(100)
.build();

Product product1 = Product.builder()
.id(1)
.name("book")
.price(100)
.build();

// 判斷物件是否相等
System.out.println(product.equals(product1));
System.out.println(product.hashCode() == product1.hashCode());
}

預設的狀況會是兩個都是 False,因為只要新產生一個物件預設就是會有不同的記憶體位置,所以 reference 不同導致這兩個比較都 False,但是我們透過註解改寫上面兩個方法可以根據內部屬性的值是否都相等來判斷這兩個物件是否相等,hashcode 也會將內部的屬性值帶入去比較,就會得到兩個都是 True

@Data

等於同時加了以下注解

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

使用上可以讓 code 很簡潔,但須要注意這個註解實做哪些方法,才不會不知道為什麼有些東西不是如你所想的在運作,這註解也會導致一些部分的問題,像是有物件繼承時,@EqualsAndHashCode 這註解預設是不考慮父類別的屬性,可能就會導致你物件比較時會出錯。詳細可以閱讀參考資料4

@Value

等於同時加了以下注解,通常用來宣告給只讀取資料的物件使用,不提供任何 Setter

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

@Builder

可以讓你使用特殊的流式語法來創建物件,簡化物件的創建過程。用了建造者模式(Builder Pattern),使得創建物件時更具靈活性和可讀性。

1
2
3
4
5
6
@Builder
public class Product {
private int id;
private String name;
private int price;
}

流式寫法,可讀性提高,也可以選擇性創建屬性提高靈活性,例如你可以只帶入 name , price 不帶 id 創建。

1
2
3
4
5
Product product = Product.builder()
.id(1)
.name("book")
.price(100)
.build();

不過使用這個通常還是會搭配 @Setter 因為有許多時候還是會操作物件去進行 set,所以通常 Builder 會配合 @Data 一起使用。

@Slf4j

自動產生 Log 的物件可以提供當前物件內使用,除了 @Slf4j 之外,lombok 也提供其他日誌框架,像是 @Log、@Log4j…等

註解使用後就可以直接使用 Slf4j 相關的方法

1
2
3
4
5
6
@Slf4j
public class LogDemo {
public static void main(String... args) {
log.info("hello");
}
}

(原始定義方法)未使用需自行建立 Slf4j 物件

1
2
3
4
5
6
7
public class LogDemo {
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogDemo.class.getName());

public static void main(String... args) {
log.info("hello");
}
}

總結:

其實在進一步認識了解之後,發現這個套件其實有蠻多討論針對是否要引用,甚至有許多團隊開發會不建議使用,其中我也是了解了一下這些部分,工作開發上我有碰過有使用也有碰過沒有使用,自己是開發個人小專案基本會使用,但多人開發上其實團隊內需要好好討論是否使用及一些規範需要訂出來,畢竟有許多缺點都是多人協作下會有問題,下面總結一些優缺點:

優點:

  • 提高開發效率:透過註解的形式自動生成方法,讓代碼變得簡潔
  • 減少維護成本:屬性做修改時,也簡化維護這些屬性所生成的 getter/setter 方法等

缺點:

  • 侵入性高:如果有一個人使用了 Lombok,那麼其他依賴這個專案的專案或是協作開發者,就須要安裝 IDE 插件或是引入套件。
  • 技術債提升:如果對於各種註解的底層原理不理解的話,很容易產生問題,像是 @Data 這種簡化多種註解及方法的就需要考量是否該使用。
  • 影響升級:如需要升級到某個新版本的 JDK 的時候,若在 Lombok 中不支持的話就會受到影響。
  • 破壞封裝:使用 getter, setter 註解後會讓每個屬性的方法都開放,如果有些部分需要自行控制就會破壞封裝性,可能會讓一些部份不被

參考資料:

  1. projectlombok
  2. 五分鐘學會 Lombok 用法
  3. 新來個技術總監,禁止我們使用 Lombok!
  4. Lombok @Data 的魔法與陷阱:深入探討 @EqualsAndHashCode