一、前言
前一段時間寫過一篇 《使用 lombok @Builder 註解,設置默認值時要千萬小心!》 的文章,文章提到使用 @Builder 註解將會導致設置的默認值失效問題。
最近讀了一篇文章:《Oh !! Stop using @Builder》[1]也頗受啓發,發現很多人的確被 @Builder 註解的名字誤導了。
大多數同學使用 @Builder 無非就是爲了鏈式編程,然而 @Builder 並不是鏈式編程的最佳實踐,它會額外創建內部類,存在繼承關係時還需要使用 @SuperBuilder 註解[2],設置默認值時也需要額外的 @Builder.Default 去設置默認值[3],無疑增加了很多不必要的複雜度。
有些同學可能說如果你把這些問題都瞭解就不會遇到這些坑,沒必要“因噎廢食”,可是這些問題已經讓無數人一次次趟坑,說明它並不是最佳實踐,如果有更簡便辦法可以實現鏈式編程,又何必徒增這麼多複雜度呢?其實很多人選擇使用 @Builder 實現鏈式編程,無非是它“更常見”和“最熟悉” !
二、爲什麼?
(1)@Builder 生成的構造器不是完美的,它不能區分哪些參數是必須的,哪些是可選的。如果沒有提供必須的參數,構造器可能會創建出不完整或者不合法的對象。
可以看第三部分的例子, @Builder 註解產生的 Builder 類的構造方法默認並不能限定必傳參數。
(2)很多人 喜歡 @Builder 和 @Data 搭配使用,導致生成的構造器是可變的,它允許使用 setter 方法修改構造器的狀態。這違反了構造器模式的原則,構造器應該是不可變的,一旦創建就不能被修改。
如果非要使用 @Builder ,那麼不要用 @Data ,要用 @Getter。相對來說,反而 @Accessors 的行爲更符合這個要求。
(3)@Builder 生成的構造器不適合用於短暫的對象,它會增加代碼的複雜度和冗餘。構造器模式更適合用於生命週期較長、有多種變體的對象。
實際使用中經常發現 @Builder 濫用的情況,有些僅僅一兩個屬性的類也都要用 @Builder,真的沒必要用,直接用全參的構造方法都比這更簡潔。
(4)@Builder 生成的構造器不能處理抽象類型的參數,它只能接受具體類型的對象。這限制了構造器的靈活性和擴展性,不能根據不同的需求創建不同風格的對象。(5)繼承關係時,子類需要使用 @SuperBuilder。對象繼承後,子類的 Builder 因爲構造函數的問題,使用不當大概率會報錯,並且無法設置父類的屬性,還需要使用 @SuperBuilder 來解決問題。(6)設置默認值需要使用 @Builder.Default。很容易因爲對此不瞭解,導致默認值不符合預期導致出現 BUG。
三、怎麼做?
正如 《Oh !! Stop using @Builder》 所推薦的一樣,建議使用 @Accessors(chain = true) 來代替。
3.1 不使用 @Builder
package io.gitrebase.demo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class APIResponse<T> {
private T payload;
private Status status;
}
使用示例:
package io.gitrebase.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice(assignableTypes = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public APIResponse handleException(Exception exception) {
log.error("Unhandled Exception", exception);
Status status = new Status();
status.setResponseCode("RESPONSE_CODE_IDENTIFIER");
status.setDescription("Bla Bla Bla");
APIResponse response = new APIResponse();
response.setStatus(status);
return response;
}
}
3.2 使用 @Builder
package io.gitrebase.demo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class APIResponse<T> {
private T payload;
private Status status;
}
等價代碼:
package io.gitrebase.demo;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
// Make constructor private so object cannot be created outside the class
// Allowing only PersonBuilder.build to create an instance of the class
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Person {
private String firstName;
private String lastName;
private String email;
private String mobile;
// PersonBuilder can only be created with firstName, lastName
// which implies that Person object ll always be instantiated with firstName & lastName
public static PersonBuilder builder(String firstName, String lastName) {
return new PersonBuilder(firstName, lastName);
}
public static class PersonBuilder {
private final String firstName;
private final String lastName;
private String email;
private String mobile;
// Make constructor private so object cannot be created outside the class
// Allowing only Person.builder to create an instance of PersonBuilder
private PersonBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public PersonBuilder email(String email) {
this.email = email;
return this;
}
public PersonBuilder mobile(String mobile) {
this.mobile = mobile;
return this;
}
public Person build() {
return new Person(firstName, lastName, email, mobile);
}
}
}
使用示例:
package io.gitrebase.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public APIResponse handleException(Exception exception) {
log.error("Unhandled Exception", exception);
return APIResponse.builder().status(Status.builder()
.responseCode("RESPONSE_CODE_IDENTIFIER")
.description("Bla Bla Bla")
.build())
.build();
}
}
3.3 使用 @Accessors 註解
package io.gitrebase.demo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class APIResponse<T> {
private T payload;
private Status status;
}
等價代碼:
package io.gitrebase.demo;
import lombok.experimental.Accessors;
@Accessors(chain = true)
public class APIResponse<T> {
private T payload;
private Status status;
public T getPayload() {
return this.payload;
}
public APIResponse<T> setPayload(T payload) {
this.payload = payload;
return this;
}
public Status getStatus() {
return this.status;
}
public APIResponse<T> setStatus(Status status) {
this.status = status;
return this;
}
}
使用示例:
package io.gitrebase.demo;
import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public APIResponse handleException(Exception exception) {
log.error("Unhandled Exception", exception);
var status = new Status().setResponseCode("RESPONSE_CODE_IDENTIFIER").setDescription("Bla Bla Bla");
return new APIResponse().setStatus(status);
}
}
原文中給的示例相對簡單,實際運用時,如果屬性較多,且分爲必傳屬性和選填屬性時,可以將必傳參數定義在構造方法中,加上 @Accessors 註解,這樣就可以實現必傳參數的傳入,又可以實現選填參數的鏈式調用。
假設 Student 類,它的 學生ID和年級和班級是必填的,姓名、性別、住址是選填的,那麼示例代碼如下:
import lombok.Accessors;
// 使用 @Accessors 註解,設置 chain = true,生成返回 this 的 setter 方法
@Accessors(chain = true)
public class Student {
// 定義必傳屬性,使用 final 修飾,不提供 setter 方法
private final int studentId; // 學生ID
private final int grade; // 年級
private final int classNum; // 班級
// 定義選填屬性,提供 setter 方法
private String name; // 姓名
private String gender; // 性別
private String address; // 住址
// 定義構造方法,接收必傳參數
public Student(int studentId, int grade, int classNum) {
this.studentId = studentId;
this.grade = grade;
this.classNum = classNum;
}
// 省略 getter 和 setter 方法
}
// 使用示例
Student student = new Student(1001, 3, 8) // 創建一個學生對象,傳入必傳參數
.setName("張三") // 設置姓名
.setGender("男") // 設置性別
.setAddress("北京市朝陽區"); // 設置住址
另外該註解還有很多設置項,感興趣可以進一步研究:
/**
* A container for settings for the generation of getters, setters and "with"-ers.
* <p>
* Complete documentation is found at <a href="https://projectlombok.org/features/experimental/Accessors">the project lombok features page for @Accessors</a>.
* <p>
* Using this annotation does nothing by itself; an annotation that makes lombok generate getters, setters, or "with"-ers
* such as {@link lombok.Setter} or {@link lombok.Data} is also required.
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
/**
* If true, accessors will be named after the field and not include a {@code get} or {@code set}
* prefix. If true and {@code chain} is omitted, {@code chain} defaults to {@code true}.<br>
* NB: This setting has no effect on {@code @With}; they always get a "with" prefix.<br>
* <strong>default: false</strong>
*
* @return Whether or not to make fluent methods (named {@code fieldName()}, not for example {@code setFieldName}).
*/
boolean fluent() default false;
/**
* If true, setters return {@code this} instead of {@code void}.
* <strong>default: false</strong>, unless {@code fluent=true}, then <strong>default: true</strong>
*
* @return Whether or not setters should return themselves (chaining) or {@code void} (no chaining).
*/
boolean chain() default false;
/**
* If true, generated accessors will be marked {@code final}.
* <strong>default: false</strong>
*
* @return Whether or not accessors should be marked {@code final}.
*/
boolean makeFinal() default false;
/**
* If present, only fields with any of the stated prefixes are given the getter/setter treatment.
* Note that a prefix only counts if the next character is NOT a lowercase character or the last
* letter of the prefix is not a letter (for instance an underscore). If multiple fields
* all turn into the same name when the prefix is stripped, an error will be generated.
*
* @return If you are in the habit of prefixing your fields (for example, you name them {@code fFieldName}, specify such prefixes here).
*/
String[] prefix() default {};
}
3.4 使用 IDEA 默認 Setter 生成功能
如果有些同學擔心 @Accessors 在 lombok.experimental 包不夠“穩定”,可以參考 @Accessors 的功能自己寫一份即可。如果嫌麻煩可以調用 Generate 快捷鍵(mac 上 Command +N)選擇生成 Getter、Setter ,其中 Setter 選擇 Builder 即可實現 set 方法返回 this 的效果,乾淨純粹,無其他依賴!
四、啓發
有些同學認爲如果能夠深入瞭解 @Builder 註解的每個細節,包括如何正確設置默認值,那麼就不容易出錯。可是這無疑增加了額外的複雜度,我們常見的使用 @Builder 註解無非就是爲了鏈式編程,而 @Accessors 就可以輕鬆實現這個效果而且還可以避免底層創建一個 Builder 對象,也可以避免默認值的坑,而且當類存在繼承關係時,還需要使用 @SuperBuilder 註解,問題的複雜度又提高了。所以,何必執着於使用 @Builder 註解呢?
本文主要指出 @Builder 存在的一些問題,指出它並不是鏈式編程的最佳實踐。但如果你對 @Builder 瞭如指掌,情有獨鍾,繼續使用無可厚非,如果你認爲這的確徒增了很多不必要的複雜度,可以考慮使用 @Accessors 或者自己手動改造 Setter 方法。
大家在使用 lombok 註解時,必須對每個註解的效果非常瞭解,推薦大家主動查看編譯後的代碼,也可以使用 JClaslib 等插件查看對應的字節碼,避免因爲理解偏差造成誤用。
另外,“大家都這麼用”、“工具庫提供的功能” 並不意味着就是對的(否則很多知名工具類庫中也不會有各種 @Deprecated 標識的過期類了)或者是“對的” 但不是最好的。大家在使用某個功能時要獨立思考和判斷能力,如果不合理可以進行改進。
參考鏈接:
[1]https://medium.com/gitrebase/oh-stop-using-builder-9061a5911d8c
[2]https://www.baeldung.com/lombok-builder-inheritance
[3]https://www.baeldung.com/lombok-builder-default-value
作者:悟鳴
點擊立即免費試用雲產品 開啓雲上實踐之旅!
本文爲阿里雲原創內容,未經允許不得轉載。