Lombok的使用詳解(最詳盡的解釋,覆蓋講解所有可用注解),解決@Builder.Default默認值問題


 

原文:https://blog.csdn.net/f641385712/article/details/82081900

 

前言

Lombok是一款Java開發插件,使得Java開發者可以通過其定義的一些注解來消除業務工程中冗長和繁瑣的代碼,尤其對於簡單的Java模型對象(POJO)。在開發環境中使用Lombok插件后,Java開發人員可以節省出重復構建,諸如hashCode和equals這樣的方法以及各種業務對象模型的accessor和ToString等方法的大量時間。對於這些方法,它能夠在編譯源代碼期間自動幫我們生成這些方法,並沒有如反射那樣降低程序的性能。

 

它所有的增強都是通過注解實現,所以了解其使用主要了解一下注解即可

 

注解列表

當前使用版本為2018年最新版本:

 

<dependency>

    <groupId>org.projectlombok</groupId>

    <artifactId>lombok</artifactId>

    <scope>provided</scope>

    <version>1.18.2-RELEASE</version>

</dependency>

  

先介紹這一波最常用的注解:

@NoArgsConstructor/@RequiredArgsConstructor /@AllArgsConstructor
這三個注解都是用在類上的,第一個和第三個都很好理解,就是為該類產生無參的構造方法和包含所有參數的構造方法,第二個注解則使用類中所有帶有@NonNull注解的或者帶有final修飾的成員變量生成對應的構造方法,當然,和前面幾個注解一樣,成員變量都是非靜態的。另外,如果類中含有final修飾的成員變量,是無法使用@NoArgsConstructor注解的。
三個注解都可以指定生成的構造方法的訪問權限,還可指定生成一個靜態方法

使用案例:
 

@AllArgsConstructor
public class Demo {

private String name;
private int age;
}

@AllArgsConstructor
class Parent {
private Integer id;
} 

編譯后的兩個class文件如下

public class Demo {
    private String name;
    private int age;

    public Demo(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

//第二個類
class Parent {
    private Integer id;

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

由此課件,此注解並不會把父類的屬性id拿到Demo的構造器里面去,這是需要注意的地方。並且它也沒有默認的構造器了

@AllArgsConstructor(access = AccessLevel.PROTECTED, staticName = "test")
public class Demo {
    private final int finalVal = 10;
    private String name;
    private int age;
}  

生成如下:

public class Demo {
    private final int finalVal = 10;
    private String name;
    private int age;

    private Demo(String name, int age) {
        this.name = name;
        this.age = age;
    }

    protected static Demo test(String name, int age) {
        return new Demo(name, age);
    }
}  

看出來的效果為:可以指定生成的構造器的訪問權限。但是,但是如果指定了一個靜態方法,那么構造器會自動會被private,只通過靜態方法對外提供反問,並且我們發現final的屬性值,是不會放進構造函數里面的。

NoArgsConstructor的使用方式同上,RequiredArgsConstructor看看效果: 

@RequiredArgsConstructor
public class Demo {
    private final int finalVal = 10;

    @NonNull
    private String name;
    @NonNull
    private int age;
} 

編譯后:

public class Demo {
    private final int finalVal = 10;
    @NonNull
    private String name;
    @NonNull
    private int age;

    public Demo(@NonNull String name, @NonNull int age) {
        if (name == null) {
            throw new NullPointerException("name is marked @NonNull but is null");
        } else {
            this.name = name;
            this.age = age;
        }
    }
} 

解釋:該注解會識別@nonNull字段,然后以該字段為元素產生一個構造函數。備注:如果所有字段都沒有@nonNull注解,那效果同NoArgsConstructor

@Builder 提供了一種比較推崇的構建值對象的方式
非常推薦的一種構建值對象的方式。缺點就是父類的屬性不能產於builder 

@Builder
public class Demo {
    private final int finalVal = 10;

    private String name;
    private int age;
}

編譯后:

public class Demo {
    private final int finalVal = 10;
    private String name;
    private int age;

    Demo(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static Demo.DemoBuilder builder() {
        return new Demo.DemoBuilder();
    }

    public static class DemoBuilder {
        private String name;
        private int age;

        DemoBuilder() {
        }

        public Demo.DemoBuilder name(String name) {
            this.name = name;
            return this;
        }

        public Demo.DemoBuilder age(int age) {
            this.age = age;
            return this;
        }

        public Demo build() {
            return new Demo(this.name, this.age);
        }

        public String toString() {
            String var10000 = this.name;
            return this.age;
        }
    }
} 

因此我們構造一個對象就可以優雅的這么來:

public static void main(String[] args) {
        Demo demo = Demo.builder().name("aa").age(10).build();
        System.out.println(demo); 
    } 

里面有一些自定義參數,我表示,完全沒有必要去自定義。

@Cleanup 能夠自動釋放資源
這個注解用在變量前面,可以保證此變量代表的資源會被自動關閉,默認是調用資源的close()方法。如果該資源有其它關閉方法,可使用@Cleanup(“methodName”)來指定要調用的方法,就用輸入輸出流來舉個例子吧: 

public static void main(String[] args) throws Exception {
        @Cleanup InputStream in = new FileInputStream(args[0]);
        @Cleanup OutputStream out = new FileOutputStream(args[1]);
        byte[] b = new byte[1024];
        while (true) {
            int r = in.read(b);
            if (r == -1) break;
            out.write(b, 0, r);
        }
    } 

編譯后:

public static void main(String[] args) throws Exception {
        FileInputStream in = new FileInputStream(args[0]);

        try {
            FileOutputStream out = new FileOutputStream(args[1]);

            try {
                byte[] b = new byte[1024];

                while(true) {
                    int r = in.read(b);
                    if (r == -1) {
                        return;
                    }

                    out.write(b, 0, r);
                }
            } finally {
                if (Collections.singletonList(out).get(0) != null) {
                    out.close();
                }

            }
        } finally {
            if (Collections.singletonList(in).get(0) != null) {
                in.close();
            }

        }
    } 

就這么簡單的一個注解,就實現了優雅的關流操作喲。

@Data 強悍的組合功能包
相當於注解集合。效果等同於**@Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor** 由於生成的代碼篇幅太長,這里就不給demo了,反正效果同上5個注解的效果,強悍

需要注意的是,這里不包括@NoArgsConstructor和@AllArgsConstructor

@Value注解和@Data類似,區別在於它會把所有成員變量默認定義為private final修飾,並且不會生成set方法。

所以@Value更適合只讀性更強的類,所以特殊情況下,還是可以使用的。

@ToString/@EqualsAndHashCode
這兩個注解也比較好理解,就是生成toString,equals和hashcode方法,同時后者還會生成一個canEqual方法,用於判斷某個對象是否是當前類的實例。,生成方法時只會使用類中的非靜態成員變量,這些都比較好理解。畢竟靜態的東西並不屬於對象本身 

@ToString
public class Demo {
    private final int finalVal = 10;

    private transient String name = "aa";
    private int age;

}


 public static void main(String[] args) throws Exception {
        Demo demo = new Demo();
        System.out.println(demo); //Demo(finalVal=10, age=0)
    } 

我們發現靜態字段它是不輸出的。
有些關鍵的屬性,可以控制toString的輸出,我們可以了解一下:

@ToString(
        includeFieldNames = true, //是否使用字段名
        exclude = {"name"}, //排除某些字段
        of = {"age"}, //只使用某些字段
        callSuper = true //是否讓父類字段也參與 默認false
) 

備注:大多數情況下,使用默認的即可,畢竟大多數情況都是POJO

@Generated:暫時貌似沒什么用
@Getter/@Setter
這一對注解從名字上就很好理解,用在成員變量上面或者類上面,相當於為成員變量生成對應的get和set方法,同時還可以為生成的方法指定訪問修飾符,當然,默認為public

這兩個注解直接用在類上,可以為此類里的所有非靜態成員變量生成對應的get和set方法。如果是final變量,那就只會有get方法
 

@Getter
@Setter
public class Demo {
    private final int finalVal = 10;

    private String name;
    private int age;

}

編譯后:

public class Demo {
    private final int finalVal = 10;
    private String name;
    private int age;

    public Demo() {
    }

    public int getFinalVal() {
        Objects.requireNonNull(this);
        return 10;
    }

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

    public int getAge() {
        return this.age;
    }

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

    public void setAge(int age) {
        this.age = age;
    }
} 
@NonNull

這個注解可以用在成員方法或者構造方法的參數前面,會自動產生一個關於此參數的非空檢查,如果參數為空,則拋出一個空指針異常。

//成員方法參數加上@NonNull注解
public String getName(@NonNull Person p){
    return p.getName();
}

編譯后:

public String getName(@NonNull Person p){
    if(p==null){
        throw new NullPointerException("person");
    }
    return p.getName();
}

@Singular 默認值 暫時也沒太大用處
@SneakyThrows
這個注解用在方法上,可以將方法中的代碼用try-catch語句包裹起來,捕獲異常並在catch中用Lombok.sneakyThrow(e)把異常拋出,可以使用@SneakyThrows(Exception.class)的形式指定拋出哪種異常

@SneakyThrows(UnsupportedEncodingException.class)
    public String utf8ToString(byte[] bytes) {
        return new String(bytes, "UTF-8");
    } 

編譯后:

@SneakyThrows(UnsupportedEncodingException.class)
    public String utf8ToString(byte[] bytes) {
        try{
            return new String(bytes, "UTF-8");
        }catch(UnsupportedEncodingException uee){
            throw Lombok.sneakyThrow(uee);
        }
    } 

這里有必要貼出來Lombok.sneakyThrow的代碼:

public static RuntimeException sneakyThrow(Throwable t) {
        if (t == null) {
            throw new NullPointerException("t");
        } else {
            return (RuntimeException)sneakyThrow0(t);
        }
    }

    private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T {
        throw t;
    } 

其實就是轉化為了RuntimeException,其實我想說,這個注解也沒大用。畢竟我們碰到異常,是希望自己處理的

@Synchronized
這個注解用在類方法或者實例方法上,效果和synchronized關鍵字相同,區別在於鎖對象不同,對於類方法和實例方法,synchronized關鍵字的鎖對象分別是類的class對象和this對象,而@Synchronized得鎖對象分別是私有靜態final對象LOCK和私有final對象lock,當然,也可以自己指定鎖對象
 

@Synchronized
    public static void hello() {
        System.out.println("world");
    }

    @Synchronized
    public int answerToLife() {
        return 42;
    }

    @Synchronized("readLock")
    public void foo() {
        System.out.println("bar");
    }
 

編譯后:

public static void hello() {
        Object var0 = $LOCK;
        synchronized($LOCK) {
            System.out.println("world");
        }
    }

    public int answerToLife() {
        Object var1 = this.$lock;
        synchronized(this.$lock) {
            return 42;
        }
    }

    public void foo() {
        Object var1 = this.readLock;
        synchronized(this.readLock) {
            System.out.println("bar");
        }
    } 

我只能說,這個注解也挺雞肋的。

@Val 很強的類型推斷 var注解,在Java10之后就不能使用了
class Parent {

    //private static final val set = new HashSet<String>(); //編譯不通過

    public static void main(String[] args) {
        val set = new HashSet<String>();
        set.add("aa");
        System.out.println(set); //[aa]
    }

} 

編譯后:

class Parent {
    Parent() {
    }

    public static void main(String[] args) {
        HashSet<String> set = new HashSet();
        set.add("aa");
        System.out.println(set);
    }
} 

這個和Java10里的Var很像,強大的類型推斷。並且不能使用在全局變量上,只能使用在局部變量的定義中。

@Log、CommonsLog、Slf4j、XSlf4j、Log4j、Log4j2等日志注解
這個注解用在類上,可以省去從日志工廠生成日志對象這一步,直接進行日志記錄,具體注解根據日志工具的不同而不同,同時,可以在注解中使用topic來指定生成log對象時的類名。不同的日志注解總結如下(上面是注解,下面是實際作用):
 

@CommonsLog
private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);
@JBossLog
private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LogExample.class);
@Log
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());
@Log4j
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);
@Log4j2
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);
@Slf4j
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
@XSlf4j
private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);
  

這個注解還是非常有用的,特別是Slf4j這個,在平時開發中挺有用的

@Slf4j
class Parent {
}

編譯后:

class Parent {
    private static final Logger log = LoggerFactory.getLogger(Parent.class);

    Parent() {
    }
}

也可topic的名稱:

@Slf4j
@CommonsLog(topic = "commonLog")
class Parent {
}

編譯后:

class Parent {
    private static final Logger log = LoggerFactory.getLogger("commonLog");

    Parent() {
    }
}

lombok中有experimental的包:
實驗性因為:

我們可能想將這些特性和更完全的性質支持概念融為一體(普通話:這些性能還在研究)
新特性-需要社區反饋
@Accessors 一個為getter和setter設計的更流暢的注解
這個注解要搭配@Getter與@Setter使用,用來修改默認的setter與getter方法的形式。所以單獨使用是沒有意義的
 

@Accessors(fluent = true)
@Getter
@Setter
public class Demo extends Parent {
    private final int finalVal = 10;

    private String name;
    private int age;

} 

編譯后:

public class Demo extends Parent {
    private final int finalVal = 10;
    private String name;
    private int age;

    public Demo() {
    }

    public int finalVal() {
        Objects.requireNonNull(this);
        return 10;
    }

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

    public int age() {
        return this.age;
    }

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

    public Demo age(int age) {
        this.age = age;
        return this;
    }
} 

它的三個參數解釋:

chain 鏈式的形式 這個特別好用,方法連綴越來越方便了
fluent 流式的形式(若無顯示指定chain的值,也會把chain設置為true)
prefix 生成指定前綴的屬性的getter與setter方法,並且生成的getter與setter方法時會去除前綴
 

@Accessors(prefix = "xxx")
@Getter
@Setter
public class Demo extends Parent {
    private final int finalVal = 10;

    private String xxxName;
    private int age;

} 

編譯后:

public class Demo extends Parent {
    private final int finalVal = 10;
    private String xxxName;
    private int age;

    public Demo() {
    }

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

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

我們發現prefix可以在生成get/set的時候,去掉xxx等prefix前綴,達到很好的一致性。但是,但是需要注意,因為此處age沒有匹配上xxx前綴,所有根本就不給生成,所以使用的時候一定要注意。

屬性名沒有一個以其中的一個前綴開頭,則屬性會被lombok完全忽略掉,並且會產生一個警告。

@Delegate 注釋的屬性,會把這個屬性對象的公有非靜態方法合到當前類
代理模式,把字段的方法代理給類,默認代理所有方法。注意:公共 非靜態方法
 

public class Demo extends Parent {
    private final int finalVal = 10;

    @Delegate
    private String xxxName;
    private int age;

}

編譯后:把String類的公共 非靜態方法全拿來了 個人覺得很雞肋有木有

public class Demo extends Parent {
    private final int finalVal = 10;
    private String xxxName;
    private int age;

    public Demo() {
    }

    public int length() {
        return this.xxxName.length();
    }

    public boolean isEmpty() {
        return this.xxxName.isEmpty();
    }

    public char charAt(int index) {
        return this.xxxName.charAt(index);
    }

    public int codePointAt(int index) {
        return this.xxxName.codePointAt(index);
    }
.
.
.
 

備注:它不能用於基本數據類型字段比如int,只能用在包裝類型比如Integer
參數們:

types:指定代理的方法
excludes:和types相反
@NonFinal 設置不為Final,@FieldDefaults和@Value也有這功能
@SuperBuilder 本以為它是支持到了父類屬性的builder構建,但其實,我們還是等等吧 目前還不好使
@UtilityClass 工具類 會把所有字段方法static掉,沒啥用
@Wither 生成withXXX方法,返回類實例 沒啥用,因為還有bug
@Builder和@NoArgsConstructor一起使用沖突問題
當我們這么使用時候:
 

編譯報錯:

Error:(17, 1) java: 無法將類 com.sayabc.groupclass.dtos.appoint.TeaPoolLogicalDelDto中的構造器 TeaPoolLogicalDelDto應用到給定類型;
  需要: 沒有參數
  找到: java.lang.Long,java.lang.Long,java.lang.Long,java.lang.Integer
  原因: 實際參數列表和形式參數列表長度不同
  

其實原因很簡單,自己點進去看編譯后的源碼一看便知。
只使用@Builder會自動創建全參構造器。而添加上@NoArgsConstructor后就不會自動產生全參構造器

兩種解決方式:

去掉@NoArgsConstructor
添加@AllArgsConstructor(建議使用這種,畢竟無參構造最好保證是有的)
but,枚舉值建議這樣來就行了,不要加@NoArgsConstructor

我認為這也是Lombok的一個bug,希望在后續版本中能夠修復
 

@builder注解影響設置默認值的問題

例子如下,本來我是想給age字段直接賦一個默認值的:
沒有使用lombok,我們這么寫:

public static void main(String[] args) {
        Demo demo = new Demo();
        System.out.println(demo); //Demo{id=null, age=10}
    }

    private static class Demo {
        private Integer id;
        private Integer age = 10; //放置默認值年齡

        //省略手動書寫的get、set、方法和toString方法

        @Override
        public String toString() {
            return "Demo{" +
                    "id=" + id +
                    ", age=" + age +
                    '}';
        }
    } 

我們發現,這樣運行沒有問題,默認值也生效了。但是,但是我們用了強大的lombok,我們怎么可能還願意手寫get/set呢?關鍵是,我們一般情況下還會用到它的@buider注解:

public static void main(String[] args) {
        Demo demo = new Demo();
        System.out.println(demo); //Demo{id=null, age=10}

        //采用builder構建  這是我們使用最多的場景吧
        Demo demo2 = Demo.builder().build();
        System.out.println(demo2); //PeriodAddReq.Demo(id=null, age=null)
    }

    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    private static class Demo {
        private Integer id;
        private Integer age = 10; //放置默認值年齡
    } 

代碼簡潔了不少。但是我們卻發現一個問題。new出來的對象默認值仍然沒有問題,但是buider構建出來的demo2對象,默認值卻沒有設置進去。這是一個非常隱晦的問題,一不小心,就可能留下一個驚天大坑,所以需要注意
其實在執行編譯的時候,idea開發工具已經警告我們了:

 

 

Warning:(51, 25) java: @Builder will ignore the initializing expression entirely. If you want the initializing expression to serve as default, add @Builder.Default. If it is not supposed to be settable during building, make the field final.
 

方案一:
從它的建議可以看出,把字段標為final就ok了(親測好用)。但很顯然,絕大多數我們並不希望他是final的字段。

因此我們采用第二個方案:

@Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    private static class Demo {
        private Integer id;
        @Builder.Default
        private Integer age = 10; //放置默認值年齡
    } 

lombok考慮到了這種現象,因此我們只需要在需要設置默認值的字段上面加上 @Builder.Default注解就ok了。

public static void main(String[] args) {
        Demo demo = new Demo();
        System.out.println(demo); //PeriodAddReq.Demo(id=null, age=null)

        //采用builder構建  這是我們使用最多的場景吧
        Demo demo2 = Demo.builder().build();
        System.out.println(demo2); //PeriodAddReq.Demo(id=null, age=10)
    } 

但是我們坑爹的發現:builder默認值沒問題了,但是new出來又有問題了。見鬼啊,

我認為這是lombok的一個大bug,希望后續版本中能夠修復

但是我們不能因為有這么一個問題,咱們就不使用它了。本文主要提醒讀者,在使用的時候留心這個問題即可。

備注:@Builder.Default會使得使用@NoArgsConstructor生成的無參構造沒有默認值,自己顯示寫出來的也不會給你設置默認值的,需要注意。

2019年1.18日補充內容:Lombok 1.18.4版本
上面已經指出了Lombok設置默認值的bug,果不其然。官方在1.18.4這個版本修復了這個bug。各位要有版本意識:這個版本級以上版本是好用的,比這版本低的都不行。
 

用這個版本運行上面例子,默認值沒有問題了。

Main.Demo(id=null, age=10)

Main.Demo(id=null, age=10)

我們不用自動生成空構造,顯示書寫出來呢?如下:

@Getter
    @Setter
    @Builder
    @AllArgsConstructor
    @ToString
    private static class Demo {
        private Integer id;
        @Builder.Default
        private Integer age = 10; //放置默認值年齡Default

        //顯示書寫出空構造
        public Demo() {
        }
    } 

 

 

我們發現手動書寫出來的空構造,默認值是不生效的。這點需要特別注意。

這個就不說是Lombok的bug了,因為既然你都使用Lombok了,為何還自己寫空構造呢?不是作死嗎?

Lombok背后的自定義注解原理
作為一個Java開發者來說光了解插件或者技術框架的用法只是做到了“知其然而不知其所以然”,如果真正掌握其背后的技術原理,看明白源碼設計理念才能真正做到“知其然知其所以然”。好了,話不多說下面進入本章節的正題,看下Lombok背后注解的深入原理。

可能熟悉Java自定義注解的同學已經猜到,Lombok這款插件正是依靠可插件化的Java自定義注解處理API(JSR 269: Pluggable Annotation Processing API)來實現在Javac編譯階段利用“Annotation Processor”對自定義的注解進行預處理后生成真正在JVM上面執行的“Class文件”。有興趣的同學反編譯帶有Lombok注解的類文件也就一目了然了。其大致執行原理圖如下:

 

 

 

從上面的Lombok執行的流程圖中可以看出,在Javac 解析成AST抽象語法樹之后, Lombok 根據自己編寫的注解處理器,動態地修改 AST,增加新的節點(即Lombok自定義注解所需要生成的代碼),最終通過分析生成JVM可執行的字節碼Class文件。使用Annotation Processing自定義注解是在編譯階段進行修改,而JDK的反射技術是在運行時動態修改,兩者相比,反射雖然更加靈活一些但是帶來的性能損耗更加大。

需要更加深入理解Lombok插件的細節,自己查閱其源代碼是必比可少的。

AnnotationProcessor這個類是Lombok自定義注解處理的入口。該類有兩個比較重要的方法一個是init方法,另外一個是process方法。在init方法中,先用來做參數的初始化,將AnnotationProcessor類中定義的內部類(JavacDescriptor、EcjDescriptor)先注冊到ProcessorDescriptor類型定義的列表中。其中,內部靜態類—JavacDescriptor在其加載的時候就將 lombok.javac.apt.LombokProcessor這個類進行對象實例化並注冊。在 LombokProcessor處理器中,其中的process方法會根據優先級來分別運行相應的handler處理類。Lombok中的多個自定義注解都分別有對應的handler處理類.

在Lombok中對於其自定義注解進行實際的替換、修改和處理的正是這些handler類。對於其實現的細節可以具體參考其中的代碼。 

 

 

Java6以后,java編譯器已經有了開源的版本了。Java6提供了JSR269的標准實現,提供插入式注解處理API(Pluggable Annotation Processing API)一套標准API來處理Annotations。只要代碼實現了此API,就能在javac運行的時候得到調用。Lombok就是一個實現了JSR269的程序
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM