1. 程式人生 > 實用技巧 >Lombok之@Builder註解

Lombok之@Builder註解

Lombok之@Builder註解

前言

Lombok大家都知道,在使用POJO過程中,它給我們帶來了很多便利,省下大量寫getset方法、構造器、equaltoString方法的時間。除此之外,通過@Builder註解,lombok還可以方便的實現建造者模式。

認識@Builder註解

lombok註解在java進行編譯時進行程式碼的構建,對於java物件的建立工作它可以更優雅,不需要寫多餘的重複的程式碼,這對於JAVA開發人員是很重要的,在出現lombok之後,物件的建立工作更提供Builder方法,它提供在設計資料實體時,對外保持private setter,而對屬性的賦值採用Builder

的方式,這種方式最優雅,也更符合封裝的原則,不對外公開屬性的寫操作!
@Builder宣告實體,表示可以進行Builder方式初始化,@Value註解,表示只公開getter,對所有屬性的setter都封閉,即private修飾,所以它不能和@Builder現起用

簡單使用

在專案生成的實體類上,只需要我們新增@Builder註解即可。示例程式碼:

package com.zy.pagehelper.model;

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;

@Data
@Builder
public class Admin implements Serializable {

    private Long id;

    private String department;

    private String email;

    private String encodedpassword;

    private String name;

    private String username;

    private static final long serialVersionUID = 1L;
}

專案中使用。程式碼例項:

 Admin admins =  Admin.builder()
                      .id(admin.getId())
                      .name(admin.getName())
                      .email(admin.getEmail())
                      .department(admin.getDepartment())
                      .username(admin.getUsername())
                      .build();

根據上面的示例,我們可以對@Builder註解有一個簡單的認識。當我們向一個物件賦值的時候,可以通過@Builder註解類似於鏈式的呼叫物件進行賦值。它的主要優點就是可以優雅的給物件賦值,修改物件,省去了set方法來定義屬性內容。

深入探究--原理

如果對建造者設計模式不太清楚的,可以先了解一下:建造者模式

@Builder註釋為你的類生成相對略微複雜的構建器API@Builder可以讓你以下面顯示的那樣類似於鏈式的呼叫你的程式碼,來初始化你的例項物件:

 Admin admins =  Admin.builder()
                      .id(admin.getId())
                      .name(admin.getName())
                      .email(admin.getEmail())
                      .department(admin.getDepartment())
                      .username(admin.getUsername())
                      .build();

@Builder可以放在類,構造器或方法上。雖然“基於類”和“基於構造器”模式是最常見的用例,但使用“方法”用例最容易解釋。
@Builder註解的方法(從現在開始稱為target)將生成以下7件事:

  1. 一個內部靜態類,名為FooBuilder,其型別引數與靜態方法相同(稱為builder)
  2. 在構建器中:目標的每個引數有一個私有的非靜態非最終欄位
  3. 在builder中:包私有的無引數空建構函式
  4. 在builder中:對目標的每個引數使用類似於“ setter”的方法:與該引數具有相同的型別和相同的名稱。如上例所示,它返回構建器本身,以便可以將setter呼叫連結起來
  5. 在builder中:build()呼叫該方法的方法,並在每個欄位中傳遞。它返回與目標返回相同的型別
  6. 有意義的toString()實現
  7. 在包含target的類中:一個builder()方法,該方法建立builder的新例項

下面我們通過class類,與我們上面的每一條進行對比:

@Builder
public class Card {
    private int id;
    private String name;
    private boolean sex;
}

使用@Builder註解反編譯後的class類:

public class Card {
    private int id;
    private String name;
    private boolean sex;

    Card(int id, String name, boolean sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }
	//註解在編譯後使得Card類中多了一個名為Card.CardBuilder的靜態內部類
    public static Card.CardBuilder builder() {
        return new Card.CardBuilder();
    }

    public static class CardBuilder {
        private int id;
        private String name;
        private boolean sex;

        CardBuilder() {
        }
	//通過靜態內部類,實現了name、sex、id等的屬性方法
	//其實這些方法和setAttribute十分類似,只是額外返回了例項本身,這使得它可以使用類似於鏈式呼叫的寫法。
        public Card.CardBuilder id(int id) {
            this.id = id;
            return this;
        }

        public Card.CardBuilder name(String name) {
            this.name = name;
            return this;
        }

        public Card.CardBuilder sex(boolean sex) {
            this.sex = sex;
            return this;
        }
	//build方法呼叫Card類的全參構造方法來生成Card例項
	//Card類還是實現了builder方法,這個方法生成一個空的Card.CardBuilder例項。
        public Card build() {
            return new Card(this.id, this.name, this.sex);
        }

        public String toString() {
            return "Card.CardBuilder(id=" + this.id + ", name=" + this.name + ", sex=" + this.sex + ")";
        }
    }
}

使用@Builder註解有無繼承

一、 無繼承父類的情況

可以將@Builder註解直接放置在類上,示例程式碼:

@Data
@Builder
public class Student {

    private String schoolName;
    private String grade;

    public static void main(String[] args) {

        Student student = Student.builder().schoolName("清華附小").grade("二年級").build();
        // Student(schoolName=清華附小, grade=二年級)
        System.out.println(student);
    }
}

二、有繼承父類的情況

  1. 對於父類,使用@AllArgsConstructor註解
  2. 對於子類,手動編寫全引數構造器,內部呼叫父類全引數構造器,在子類全引數構造器上使用@Builder註解
    通過這種方式,子類Builder物件可以使用父類的所有私有屬性。但是這種解法也有兩個副作用:
  • 因為使用@AllArgsConstructor註解,父類建構函式欄位的順序由宣告欄位的順序決定,如果子類建構函式傳參的時候順序不一致,欄位型別還一樣的話,出了錯不好發現
  • 如果父類欄位有增減,所有子類的構造器都要修改

示例程式碼父類

@Data
// 對於父類,使用@AllArgsConstructor註解
@AllArgsConstructor
public class Person {

    private int weight;
    private int height;
}

示例程式碼子類

@Data
@ToString(callSuper = true)
public class Student extends Person {

    private String schoolName;
    private String grade;

// 對於子類,手動編寫全引數構造器,內部呼叫父類全引數構造器,在子類全引數構造器上使用@Builder註解
    @Builder
    public Student(int weight, int height, String schoolName, String grade) {
        super(weight, height);
        this.schoolName = schoolName;
        this.grade = grade;
    }

    public static void main(String[] args) {

        Student student = Student.builder().schoolName("清華附小").grade("二年級")
                .weight(10).height(10).build();

        // Student(super=Person(weight=10, height=10), schoolName=清華附小, grade=二年級)
        System.out.println(student.toString());
    }
}

@Builder註解導致預設值無效問題

@Builder註解導致預設值無效---解決方案

看完上面的內容我們知道@Builder註解它可以讓我們很方便的使用builder模式構建物件。但是當我們給物件賦有預設值的時候會被@Builder註解清除掉。
從下面一段程式碼中,我們可以更加清楚的認識到這一點:

public class BuilderTest {
    @lombok.Builder
    @lombok.Data
    private static class Builder {
        private String name = "1232";
    }

    @Test
    public void test() {
        Builder builder = Builder.builder().build();
        System.out.println(builder.getName());
    }
}
---列印結果---
null

那麼不想讓這個預設值被清除,就只能用另外一個註解來對屬性進行設定:@lombok.Builder.Default

public class BuilderTest {
    @lombok.Builder
    @lombok.Data
    private static class Builder {
        @lombok.Builder.Default
        private String name = "1232";
    }

    @Test
    public void test() {
        Builder builder = Builder.builder().build();
        System.out.println(builder.getName());
    }
}
---列印結果---
1232
  • 需要注意的是@lombok.Builder.Default這個註解是後來才有的,目前已知的是1.2.X沒有,1.6.X中有這個註解。

@Builder註解導致預設值無效----分析原因

由上面文章內容,我們可以知道,當我們使用@Builder註解時,編譯後會生成一個靜態內部類,通過靜態內部類,最終才實現屬性的方法,但是現實的方法,並沒有預設值,這就導致當我們builder之後的實體類的屬性值是null。

示例程式碼:

//編譯前
@lombok.Builder
class Example {
    private String name = "123";
}
//編譯後class類
class Example {
    private String name;

    private Example(String name) {
        this.name = name;
    }

    public static ExampleBuilder builder() {
        return new ExampleBuilder();
    }

    public static class ExampleBuilder {
        private String name;

        private ExampleBuilder() {}
		//這裡我們可以看到,靜態內部類實現了屬性方法,但是並沒有對預設值做處理,
		//所有builder之後返回的屬性值為null
        public ExampleBuilder name(String name) {
            this.name = name;
            return this;
        }

        @java.lang.Override public String toString() {
            return "Example(name = " + name + ")";
        }

        public Example build() {
            return new Example(name);
        }
    }
}

推薦參考blog@Builder註解構造器生成的詳解

@Builder相關注解

@Builder.Default 使用

比如有這樣一個實體類:

@Builder
@ToString
public class User {
    @Builder.Default
    private final String id = UUID.randomUUID().toString();
    private String username;
    private String password;
    @Builder.Default
    private long insertTime = System.currentTimeMillis();
}

在類中我在idinsertTime上都添加註解@Builder.Default,當我在使用這個實體物件時,我就不需要在為這兩個欄位進行初始化值,如下面這樣:

public class BuilderTest {
    public static void main(String[] args) {
        User user = User.builder()
                .password("jdkong")
                .username("jdkong")
                .build();
        System.out.println(user);
    }
}

// 輸出內容:
User(id=416219e1-bc64-43fd-b2c3-9f8dc109c2e8, username=jdkong, password=jdkong, insertTime=1546869309868)

lombok在例項化物件時就為我們初始化了這兩個欄位值。
當然,你如果再對這兩個欄位進行設值的話,那麼預設定義的值將會被覆蓋掉,如下面這樣:

public class BuilderTest {
    public static void main(String[] args) {
        User user = User.builder()
                .id("jdkong")
                .password("jdkong")
                .username("jdkong")
                .build();
        System.out.println(user);
    }
}
// 輸出內容
User(id=jdkong, username=jdkong, password=jdkong, insertTime=1546869642151)

@Builder 詳細配置

@Target({TYPE, METHOD, CONSTRUCTOR})
@Retention(SOURCE)
public @interface Builder {
	// 如果@Builder註解在類上,可以使用 @Builder.Default指定初始化表示式
	@Target(FIELD)
	@Retention(SOURCE)
	public @interface Default {}
	// 指定實體類中建立 Builder 的方法的名稱,預設為: builder (個人覺得沒必要修改)
	String builderMethodName() default "builder";
	// 指定 Builder 中用來構件實體類的方法的名稱,預設為:build (個人覺得沒必要修改)
	String buildMethodName() default "build";
	// 指定建立的建造者類的名稱,預設為:實體類名+Builder
	String builderClassName() default "";
	// 使用toBuilder可以實現以一個例項為基礎繼續建立一個物件。(也就是重用原來物件的值)
	boolean toBuilder() default false;
	
	@Target({FIELD, PARAMETER})
	@Retention(SOURCE)
	public @interface ObtainVia {
		// 告訴lombok使用表示式獲取值
		String field() default "";
		// 告訴lombok使用表示式獲取值
		String method() default "";

		boolean isStatic() default false;
	}
}

@Builder 全域性配置

# 是否禁止使用@Builder
lombok.builder.flagUsage = [warning | error] (default: not set)
#是否使用Guaua
lombok.singular.useGuava = [true | false] (default: false)
# 是否自動使用singular,預設是使用
lombok.singular.auto = [true | false] (default: true)