Lombok @Data 和 Final 欄位:解決「基底類別中的預設建構子」錯誤
1. 簡介
使用 Lombok 時, @Data
註解是一種流行的選擇,因為它可以自動產生樣板程式碼,如 getter、setter、 toString()
、 equals()
、 hashCode()
和建構子。
然而,當我們將 Lombok 與繼承結合時,事情會變得棘手。具體來說,如果用@Data
註解的子類擴展了一個包含final
字段的抽象基類,我們可能會遇到以下錯誤:
lombok needs a default constructor in the base class
在本教程中,我們將探討此錯誤發生的原因,介紹重現問題的範例,並示範修復該問題的解決方案。
2. 理解問題
讓我們從一個抽象基底類別開始:
public abstract class BaseEntity {
private final String createdBy;
protected BaseEntity(String createdBy) {
this.createdBy = createdBy;
}
public String getCreatedBy() {
return createdBy;
}
}
在此設計中,字段createdBy
被宣告為final
,這表示在建構物件時必須始終為其賦值,且之後不能重新賦值。為了強制執行此規則,該類別僅提供了一個接受createdBy
參數的建構子。
由於沒有提供無參數(預設)建構函數,因此每個子類別在呼叫基底類別建構函數時都必須明確提供此值。
現在考慮一下當我們創建一個子類別並用 Lombok 的@Data
註解它時會發生什麼:
@Data
public class User extends BaseEntity {
private String name;
public User(String createdBy, String name) {
super(createdBy);
this.name = name;
}
}
此時,編譯失敗並顯示以下錯誤訊息:
lombok needs a default constructor in the base class
發生此錯誤的原因是@Data
產生的建構子期望基底類別具有無參數建構子。當基底類別包含final
欄位但未定義此類建構子時,Lombok 無法正確連結建構子。由於 Java 要求每個子類別建構函式都呼叫其父類別建構函式之一,因此產生的程式碼無效,編譯失敗。
3.使用@NoArgsConstructor(force = true)
為了解決這個問題,一種方法是在基類上使用 Lombok 的@NoArgsConstructor
註解,並設定force = true
選項。這會告訴 Lombok 產生一個無參數的建構函數,該函數使用預設值(例如null
、 0
或false:
初始化final
欄位:
@NoArgsConstructor(force = true)
public abstract class BaseEntity {
private final String createdBy;
public BaseEntity(String createdBy) {
this.createdBy = createdBy;
}
}
透過這樣做,Lombok 確保子類別可以編譯,因為所需的預設建構函式現在可用了。然而,這種方法並不總是安全的,因為它允許 final 欄位以無意義的預設值開頭。
4. 提供顯式預設建構函數
更安全的替代方案是,我們自己在基底類別中明確定義一個預設建構子。這個建構子可以為final
欄位賦一個合理的預設值,確保物件即使在沒有參數的情況下創建也處於有效狀態:
public abstract class BaseEntity {
private final String createdBy;
protected BaseEntity() {
this.createdBy = "system";
}
protected BaseEntity(String createdBy) {
this.createdBy = createdBy;
}
}
在這種情況下,任何未提供createdBy
值的子類別都會自動使用「 system
」作為預設值。當我們想要為領域模型維護有意義的預設值時,這種方法是更可取的。
5. 避免使用@Data
並使用目標註釋
另一個解決方案是完全避免使用@Data
註解,而是使用更有針對性的 Lombok 註解,例如@Getter
和@Setter
。這使我們能夠更好地控制 Lombok 產生哪些方法,同時防止它創建有問題的構造函數:
@Getter
@Setter
public class User extends BaseEntity {
private String name;
public User(String createdBy, String name) {
super(createdBy);
this.name = name;
}
}
在這個例子中, User
類別保持乾淨和簡潔,同時避免了@Data
建構函式產生的陷阱。
6. JPA 實體的真實範例
這個問題通常出現在使用 JPA 的 Spring Boot 應用程式中。一種典型的模式是定義一個BaseEntity
類別來儲存審計訊息,例如createdAt
或createdBy:
public abstract class BaseEntity {
@Column(nullable = false, updatable = false)
private final LocalDateTime createdAt;
protected BaseEntity() {
this.createdAt = LocalDateTime.now();
}
}
然後, User
實體可能會擴展此基類:
@Data
@Entity
public class User extends BaseEntity {
@Id
@GeneratedValue
private Long id;
private String username;
}
透過在基底類別中明確提供預設建構函數,錯誤得到解決,並且實體可以與 Lombok 和 JPA 一起正常工作。
7. 結論
在本文中,我們探討了當子類別使用@Data,
而父類別包含final
欄位且沒有無參數建構函式時,為何Lombok 會顯示「lombok needs a default constructor in the base class」錯誤。我們也介紹了一些簡單的修復方法,例如加入預設構造函數、使用@NoArgsConstructor(force = true)
或切換到有針對性的 Lombok 註解。
與往常一樣,原始碼可在 GitHub 上取得。