囉嗦的 java,簡潔的 lombok —— lombok 的使用及簡單實現單例模式註解

lombok 是什麼?

lombok 是一個很是神奇的 java 類庫,會利用註解自動生成 java Bean 中煩人的 Getter、Setting,還能自動生成 logger、ToString、HashCode、Builder 等 java
特點的函數或是符合設計模式的函數,可以讓你 java Bean 更簡潔,更美觀。html

來先看下使用 lombok 後的 java bean 看起來是怎樣的java

@Data
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;

    public static void main(String[] args) {
        User user = new User(1L,"張三",18);
        System.out.println("toString:"+user);
        System.out.println("name:"+user.getName());
    }
}

輸出以下json

toString():User(id=1, name=張三, age=18)
getName:張三

看到了嗎,僅僅在類中添加 @Data 註解就能作到自動生成 GetterToString 函數了! 僅僅添加了 @AllArgsConstructor 註解就不再用寫那些讓你心煩的構造函數了!設計模式

我第一次使用 lombok 的時候就很喜歡它了。以爲它的思想很是好的,便是不該該花時間去寫重複的代碼,應該讓之自動化api

固然自動化的手段是什麼也很重要,我能夠經過 ide 的生成器功能、或者是本身寫的代碼生成器,自動生成 Getter、Setting、ToString,就算不用 lombok 也是能夠作到的。而我喜歡 lombok 最主要緣由就在於它會讓代碼更簡潔、閱讀起來更清晰。安全

可能會有人說這玩意只適合我的項目,不適合大型合做。。。但知乎有位大神說過亞馬遜(重度使用 java 的公司) 內部的項目都在用。無論如何仍是先了解再決定吧,我我的仍是很喜歡用這種方式。bash

上面的介紹了一下 lombok 有點簡陋,因此下面會介紹得更詳細的一點。固然也許會有讀者對 lombok 這種像是魔法的寫法感到好奇,究竟它是怎麼自動生成代碼的呢?因此文章的後面會對 lombok 進行簡單的探討,但願讀者會喜歡。服務器

lombok 的安裝

安裝

得現代的依賴管理 ,引入 lombok 依賴及其簡單數據結構

meavn

使用 meavn 的朋友在 pom.xml 文件中添加依賴便可app

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.6</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

gradle

用 gradle 的朋友 在 build.gradle 在添加依賴便可

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.6'
    annotationProcessor 'org.projectlombok:lombok:1.18.6'
}

idea 的插件

你看 pom.xml 的依賴的做用域(scope) 是 provided,也就是說 lombok 是在編譯的時候才起做用的。所以 idea 在正確使用 lombok 的時候也會報錯的

1409313-20190331165746794-47856150.md.png
因此你要安裝 lombok 插件才能正常使用。

1409313-20190331170210429-178360424.md.png

使用

註解的類型

類型 解釋
val,var 神奇的類型推到,能夠表明任意類型
@Getter and @Setter
@ToString
@EqualsAndHashCode
@NonNull
@AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor 構造函數部分,針對不一樣狀況的構造函數
@Data 至關於 @Getter + @Setter + @ToString + @EqualsAndHashCode + RequiredArgsConstructor
@Value 類變成只讀模式
@Builder builder 模式,會建立內 Builder
@Singular 要配合 builder 使用,會對(List、Set)等生成更方便函數
@Cleanup 告別煩人的釋放的資源
@Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j
@CommonsLog, @JBossLog, @Flogger
不一樣框架的日誌註解
@SneakyThrows 偷偷摸摸地拋出異常
@Delegate 帶實驗性質的,能很是方便實現代理模式
@Accessors 帶實驗性質的存取器
@Wither 帶實驗性質的,根據被修飾的成員變量建立類

val,var

能夠表示任何類型!
var 能夠用來表示變量,相似其餘語言中的 let
val 能夠用來表示常量(final),相似其餘語言中的 const

var str = "hello world";
val list = Arrays.asList(1,2,3,4);
System.out.println(str);
for(val item : list){
    System.out.printf("%d\t",item);
}

等價於

String str = "hello world";
final List<Integer> list = Arrays.asList(1,2,3,4);
System.out.println(str);
for(final Integer item : list){
    System.out.printf("%d\t",item);
}

@Getter、@Setter

添加了註解後會根據字段生成對應的 get、set 函數,能夠修飾成員變量或者類

@Getter
@Setter
public class User {
    private Long id;
    private String name;
    private Integer age;
}

靈活的 lombok 能夠經過,下面的方式指定訪問級別(PUBLIC、PROTECTED、PACKAGE、PRIVATE)

@Getter 
@Setter
public class User {   
    private Long id;
    
    private String name;
    
    @Setter(AccessLevel.PROTECTED)
    private Integer age;
}

@ToString

@ToString
public class User {
    private Long id;
    private String name;
    private Integer age;
}

ToString 生成後代碼大概以下

public String toString() {
    return "User(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ")";
}

選項:

一、@ToString(includeFieldNames=false) 不顯示變量名,會直接輸出值

public String toString() {
    return "User(" + this.id + ", " + this.name + ", " + this.age + ")";
}

二、@ToString(exclude = {"age"}) 生成的結果會排出 age 變量

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

三、@ToString(of = {"id","name"}) 生成的結果包括

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

@EqualsAndHashCode

只能用於修飾類。

@EqualsAndHashCode
public class User {
    //...
}

和 ToString 相似,能夠用 of 以及 exclude 來排出成員變量

@NonNull

能夠用於成員變量、本地變量、參數、方法

@Setter
public class User {

    private Long id;

    @NonNull
    private String name;

    private Integer age;
    
}

setName 函數實際上會變成這樣

public void setName(@NonNull String name) {
    if (name == null) {
        throw new NullPointerException("name is marked @NonNull but is null");
    } else {
        this.name = name;
    }
}

構造函數 @AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor

這三者都是處理構造函數的註解,都只能修飾類,都能經過staticName 建立靜態工廠方法,使用access控制訪問級別。
不一樣之處在於 @AllArgsConstructor 會把全部的成員變量都歸入到構造函數中, @RequiredArgsConstructor 只會把 final@NonNull 修飾的成員變量歸入、@NoArgsConstructor 全部的成員變量都不會歸入到構造函數。

@AllArgsConstructor

構造函數會包含全部字段

@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;
}

會自動生成

public User(Long id, String name, Integer age) {
    this.id = id;
    this.name = name;
    this.age = age;
}

關於 staticNameaccess 的選項,能夠看下面的例子

@AllArgsConstructor(staticName = "of",access = AccessLevel.PRIVATE)
public class User {
    private Long id;
    private String name;
    private Integer age;
}

會看到構造函數和靜態工廠函數的訪問級別都變成 private

public class User {
    private Long id;
    private String name;
    private Integer age;

    private User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    private static User of(Long id, String name, Integer age) {
        return new User(id, name, age);
    }
}

@RequiredArgsConstructor

final 修飾和 @NonNull 修飾的參數纔會加入構造函數

@RequiredArgsConstructor
public class User {
    @NonNull
    private Long id;
    private final String name;
    private Integer age;
}

生成的結果大概是這樣

public class User {
    @NonNull
    private Long id;
    private final String name;
    private Integer age;

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

@NoArgsConstructor

顧名思義,使用 @NoArgsConstructor 會生成沒有參數的構造函數

但若是是用 final 修飾的成員函數呢?

答:這樣會編譯出錯的,除非是用 @NoArgsConstructor(force=true),那麼全部的 final 字段會被定義爲0,false,null等。

若是使用使用的是 @NonNull 修飾的成員字段呢?那麼使用無參數的構造函數構造出來的實例成員變量不就是 null 了嗎?不就矛盾了嗎?

答:是的。。。

好比

@NoArgsConstructor
@Getter
public class User {
    private  Long id ;
    private @NonNull String name;
    private Integer age;

    public static void main(String[] args) {
        System.out.println(new User().getName());
    }
}

輸出結果是 null

所以若是有 @NonNull 修飾的成員的變量就不要用 @NoArgsConstructor 修飾類

@Data

@Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor 就是那麼的方便

選項,能夠經過 staticConstructor 建立靜態工廠函數

@Data(staticConstructor = "of")
public class User {
    private  Long id ;
    private @NonNull String name;
    private Integer age;
}

@Value

將類變成只讀模式。會讓全部類成員變量都變成 final,而後 + @RequiredArgsConstructor + @ToString + @EqualsAndHashCode

@Builder

面對複雜的數據結構,使用 builder 模式能夠抽離複雜的構造方式,能保證線程安全,我在這篇文章中也有對 Builder 的進行粗略的探討。

使用 Builder 模式很爽,好比是這樣的

User user = User.builder().id(1L)
                        .name("張三")
                        .age(12).build();

這樣雖然好爽,但

代價是什麼呢?

就是好多冗餘的東西要寫。好比是這樣

public class User {
    private Long id;
    private String name;
    private Integer age;

    User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public static User.UserBuilder builder() {
        return new User.UserBuilder();
    }

    public static class UserBuilder {
        private Long id;
        private String name;
        private Integer age;

        UserBuilder() {
        }

        public User.UserBuilder id(Long id) {
            this.id = id;
            return this;
        }

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

        public User.UserBuilder age(Integer age) {
            this.age = age;
            return this;
        }

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

        public String toString() {
            return "User.UserBuilder(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ")";
        }
    }
}

用 lombok 後

@Builder
public class User {
    private Long id;
    private String name;
    private Integer age;
}

清晰明瞭,爽!

可是這裏有個很嚴重的問題,就是不符合 java bean 的規範,java bean 要求有一個無參數的構造函數的。不符號 java bean 要求會有什麼後果能?好比:json 字符不能反序列化成 java 對象。
解決方式是寫成這樣,要同時寫上 @AllArgsConstructor 和 @NoArgsConstructor 才行

@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;
}

@Singular

Singular 要和 Builder 一塊兒使用的,會對 List、Set 等集合類生出、處理 addOne、addAll、clear 方法
好比源碼是這樣的

@Builder
public class User {
    private Long id;
    private String name;
    private Integer age;
    private @Singular Set<String> Girlfriends;
}

生成的代碼 User 的靜態內部類 UserBuilder 會在增添

public User.UserBuilder Girlfriend(String Girlfriend) {
    if (this.Girlfriends == null) {
        this.Girlfriends = new ArrayList();
    }
    this.Girlfriends.add(Girlfriend);
        return this;
    }

public User.UserBuilder Girlfriends(Collection<? extends String> Girlfriends) {
    if (this.Girlfriends == null) {
        this.Girlfriends = new ArrayList();
    }

    this.Girlfriends.addAll(Girlfriends);
    return this;
}

public User.UserBuilder clearGirlfriends() {
    if (this.Girlfriends != null) {
        this.Girlfriends.clear();
    }

    return this;
}

@Cleanup

處理煩人的資源釋放的神奇手段

在 java 7 以前的資源釋放是使用 try-catch-finally 的方式處理的,繁瑣而容易出錯

FileInputStream  fis = null;
FileOutputStream fos = null;
try {
    fis = new FileInputStream(new File("a.txt"));
    fos = new FileOutputStream("b.txt");
    byte[] buffer = new byte[1024];
    int len;
    while ( (len = fis.read(buffer)) != -1){
        fos.write(buffer,0,len);
    }
}catch (IOException e){
    e.printStackTrace();
}finally {
    if(fis!=null){
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    if(fos != null){
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

固然 java 7 以後是使用簡化了一下,實現了 Closeable 的類能夠用 try-with-resource 自動關閉鏈接

public static void main(String[] args) {
    try (
        FileInputStream fis = new FileInputStream(new File("a.txt"));
        FileOutputStream fos = new FileOutputStream("b.txt");){
        byte[] buffer = new byte[1024];
        int len;
        while ( (len = fis.read(buffer)) != -1){
            fos.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

而 lombok 則只需添加 @Cleanup 則能夠完成釋放資源,但一樣須要類自己實現了 Closeable 接口

try {
    @Cleanup FileInputStream fis = new FileInputStream(new File("a.txt"));
    @Cleanup FileOutputStream fos = new FileOutputStream("b.txt");
    byte[] buffer = new byte[1024];
    int len;
    while ((len = fis.read(buffer)) != -1) {
        fos.write(buffer, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
}

日誌 @Log4j2

java 有不少日誌的框架,這裏用就只以 Log4j2 爲例了

@Log4j2
public class User {
}

會生成

public class User {
    private static final Logger log = LogManager.getLogger(User.class);
}

@SneakyThrows

算是一個比較有爭議的,意思是近悄悄地拋出異常,要謹慎使用。check exception 會轉成 unchecked 的。

@SneakyThrows(FileNotFoundException.class)
public static void read() {
    FileReader reader = new FileReader("sdf.txt");
}

調用的使用也不用再聲明throws FileNotFoundException

public static void main(String[] args)  {      
    read();
}

@Delegate

利用 @Delegate 能很是方便地實現代理模式。下面用個場景介紹一下,好比:有臺代理服務器,有臺文件服務器,而咱們只能經過代理服務去訪問文件服務器。

最終調用大概如此

public class Main  {
    public static void main(String[] args) throws IOException {
        ProxyServer proxyServer = new ProxyServer();
        proxyServer.loadFile("avatar.png");
    }
}

假設獲取接口是這樣的

public interface Server {
    byte[] loadFile(String fileName) throws IOException;
}

文件服務器的簡單實現是如此

public class FileServer implements Server {

    @Override
    public byte[] loadFile(String fileName) throws IOException {
        return Files.readAllBytes(new File(fileName).toPath());
    }
}

不用 lombok 時要這樣

public class ProxyServer implements Server{    
    FileServer realServer = new FileServer();

    @Override
    public byte[] loadFile(String fileName) throws IOException {
        return realServer.loadFile(fileName);
    }
}

代理服務器利用 @Delegate 便可,簡單快捷

public class ProxyServer implements Server{
    @Delegate
    FileServer realServer = new FileServer();
}

@Accessors

使用 @Accessors 生成 Setter 會是鏈式的

@Accessors(fluent = true)
@Setter
public class User {
    private Long id;
    private String name;
    private Integer age;

    public User() {
    }

    public User id(Long id) {
        this.id = id;
        return this;
    }

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

    public User age(Integer age) {
        this.age = age;
        return this;
    }
}

它有三個選項:(以 name 成員變量爲例)

  • fluent: 默認值是 false。若是是 false ,setter 生成的函數是 String setName(String name); 若是是 true, setter 是生成的是 User name(String name) ,返回的是 this,是種鏈式 Setter,同時 chain 會是 true。
  • chain: 默認值是 false。若是是 true,setter 生成的函數是 User setName(String name)。不然的是void setName(String name)
  • prefix: 好比成員變量是 gk_name,加上註解 @Accessors(prefix = "gk_"),生成的 Getter 和 Setter 中則沒有 gk_ 前綴

@Wither

用於修飾成員變量,能夠根據用於被修飾的成員變量建立一個對象。

@AllArgsConstructor
public class User {
    private Long id;
    @Wither private String name;
    private Integer age;
}

生成後的代碼

public class User {
    private Long id;
    private String name;
    private Integer age;

    public User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public User withName(String name) {
        return this.name == name ? this : new User(this.id, name, this.age);
    }
}

原理

或者你會好奇,這麼神奇的魔法原理是什麼呢?函數是如何生成的?

想一想編譯原理 ,想一想 java 文件編譯成 class 文件的過程,
Java 源碼編譯由如下三個過程組成:

  1. 詞法分析、語法分,輸出結果是 符號表 和 AST 語法樹
  2. 註解處理
  3. 語義分析和生成 class 文件

那麼 lombok 是在那裏添加要插入的代碼呢?估計是註解處理處理部分吧。

要如何生成呢?猜一下,估計多是在註解處理時期 javac 能調用一個藉口處理註解,咱們並能夠從中獲取當 AST 樹,而後就能夠根據咱們的想法,直接修改語法樹了

在網上搜索的時候找到這樣一篇文章《Project Lombok: Creating Custom Transformations》stackoverflow 的 問題正好能解決咱們的疑惑

lombok 的執行過程以下圖

和我猜測的差很少

上面一個答案說根據 JSR269 提案的 process 的 javax.annotation.processing.AbstractProcessor api 可能弄出來,還有一個回答說 lombok 作的東西比 process 多,還有針對 eclipse、javac 處理的 handle。找了一下文章看,確實如此,不只設計的接口好,並且有豐富的例子能夠給你參考。有關 java ast 的 jcTree 是有文檔,但我只找到 api 文檔,沒什麼例子。但我下面仍是會用 a

目標:單例模式

關於單例模式,我在以前的博客中也探討過了。
我選取其中的一個例子吧。但願生成的結果是這樣的

public class SingletonRegistry {
    private SingletonRegistry() {}
     
    private static class SingletonRegistryHolder {
        private static SingletonRegistry instance = new SingletonRegistry();
    }
     
    public static SingletonRegistry getInstance() {
        return SingletonRegistryHolder.instance;
    }
     
    // other methods
}

而源碼只需這樣就能夠了,

@Singleton
public class SingletonRegistry{}

建立註解

在 com.jojo.annotation.processor 目錄下建立註解,其中

  • @Target(ElementType.TYPE) 意思是說註解只能修飾類,不能修飾方法、變量等,
  • @Retention(RetentionPolicy.SOURCE) 意思是說,註解保留範圍是源代碼,也就是在編譯以後就看不到了,在 class 文件看不到,運行的時候用反射拿也就拿不到了
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Singleton {
}

HelloWorld

萬物起頭 Hello World。咱們要確認下,實現 AbstractProcessor 接口後。是如何編譯、調用、調試的。
在 com.jojo.annotation.processor 建立 SingletonProcessor 類

@SupportedAnnotationTypes("com.jojo.annotation.processor.Singleton")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SingletonProcessor extends AbstractProcessor {

    Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();     //編譯的時候用於輸出
    }

     @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
        set.forEach(element -> {
            note("hello world");
        });
        return true ;
    }

    //簡單封裝的函數
    private void note(String message){
        this.messager.printMessage(Diagnostic.Kind.NOTE, message);
    }
}

編譯運行 命令行方式

在 com.jojo 中建立用來測試的類 SingletonRegistry

@Singleton
public class SingletonRegistry {
}

編譯是個問題,怎麼編譯呢?編譯 SingletonRegistry.java 的時候就要編譯器(javac)看到 SingletonProcessor 才能處理啊。。。也就是說編譯 SingletonRegistry.java 要 SingletonProcessor 已經編譯好了。

先用命令行處理一下
在 maven 項目的根目錄下 建立 compile.sh

#!/usr/bin/env bash
if [ -d target ]; then
    rm -rf target;
fi

mkdir target

source=src/main/java/com/jojo
javac -cp $JAVA_HOME/lib/tools.jar  ${source}/annotation/*.java ${source}/processor/*.java -d target
javac -cp target -processor com.jojo.processor.SingletonProcessor ${source}/SingletonRegistry.java -d target

在命令行中執行 sh compile.sh 便可看到輸出 Note: hello world

debug

由於真的不知道該如何 debug 。以前一直用上面的方式看輸出進行調試。後面在 stackoverflower 上找到一篇文章了發現本身傻逼了

我簡單描述一下吧。在 meavn 上添加 google 的 auto-service 依賴

1. 添加依賴

<dependencies>
    <dependency>
        <groupId>com.google.auto.service</groupId>
        <artifactId>auto-service</artifactId>
        <version>1.0-rc4</version>
    </dependency>
</dependencies>

這個東東可以在打包成 jar 時候自動生成 META-INF/services/javax.annotation.processing.Processor

文件內容以下

com.jojo.processor.SingletonProcessor

javac 會檢測 jar 中 javax.annotation.processing.Processor 文件,並將文件對應的類註冊爲 processor。編譯的時候就會調用到。

2. 添加 auto-service 註解

在 SingletonProcessor 下添加註解 @AutoService(Processor.class)

@SupportedAnnotationTypes("com.jojo.annotation.processor.Singleton")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class SingletonProcessor extends AbstractProcessor {
    //....
}

添加後會在 javax.annotation.processing.Processor 文件中寫入被註解的類路徑。

3. maven 編譯配置

<build>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.3</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

4. 配置調試器

配置參數結果以下

5. 建立 src/test/java 下建立測試類

類是以 Test 結尾,並添加上要用的註解

@Singleton
public class SingletonRegistryTest {
}

6. 在要調試的地方打上斷點

7. 在終端中輸入 mvnDebug clean install

這樣會進入 meavn debug 模式。運行後會看到輸出

Preparing to execute Maven in debug mode
Listening for transport dt_socket at address: 8000

8. 在 idea 中點擊運行按鈕

看到輸出 Connected to the target VM, address: 'localhost:8000', transport: 'socket'
也就鏈接成功,這樣會停在你打斷點的地方了

SingletonProcessor

迴歸正傳,要生成一個上面所言的單例模式要怎樣作呢?

  1. 獲取註解對應的類的 AST 樹
  2. 添加一個私有的無參數構造器
  3. 添加一個靜態內聯類,內聯類裏面要添加一個成員 instance 並完成初始化
  4. 添加一個成員函數,而後 instance

看起來不難,但實際上還真的有點煩的,下面是代碼的實現

1. 添加依賴

獲取 AST 樹要用到 sdk 中的tools.jar,因此要進入依賴

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

2. 配置工具類

public class SingletonProcessor extends AbstractProcessor {

    private Messager messager;
    private JavacTrees trees;
    private TreeMaker treeMaker;
    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();     //用於編譯時的輸出
        this.trees = JavacTrees.instance(processingEnv); //AST 樹
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);    //封裝了定義方法、變量、類等等的方法
        this.names = Names.instance(context);            //用於建立標識符
    }

    //還有更多的函數
}

3. 獲取語法樹

public class SingletonProcessor extends AbstractProcessor {
    //...省略上面的

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //獲取被 Singleton 註解標註的類的集合
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);

        set.forEach(element -> {
            //獲取到對應的 AST 樹
            JCTree jcTree = trees.getTree(element);
        });
        return true;
    }
}

我打了個斷點,你能夠看到 jcTree 的定義是怎樣的。我關注的地方是 defs, 如今能夠 defs 只定義了一個構造函數、名字也獨特叫 

4. 建立構造函數

這個函數的目的是去掉默認的公有的無參數的構造函數、添加一個私有的無參數構造函數

private void createPrivateConstructor(JCTree.JCClassDecl singletonClass) {
    JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PRIVATE);
    JCTree.JCBlock block = treeMaker.Block(0L, nil());
    JCTree.JCMethodDecl constructor = treeMaker
            .MethodDef(
                    modifiers,                   //修飾符
                    names.fromString("<init>"),  //函數名
                    null,                        //方法返回的類型
                    nil(),                       //泛型參數
                    nil(),                       //參數
                    nil(),                       //throw
                    block,                       //函數代碼塊,這裏是空代碼塊
                    null);                       //默認值

    //去掉默認的構造函數
    ListBuffer<JCTree> out = new ListBuffer<>();
    for (JCTree tree : singletonClass.defs) {
        if (isPublicDefaultConstructor(tree)) {//是否公有無參數的構造函數
            continue;
        }
        out.add(tree);
    }
    out.add(constructor);
    singletonClass.defs = out.toList();
}

private boolean isPublicDefaultConstructor(JCTree jcTree) {
    if (jcTree.getKind() == Tree.Kind.METHOD) {
        JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) jcTree;
        if (isConstructor(jcMethodDecl)
                && isNoArgsMethod(jcMethodDecl)
                && isPublicMethod(jcMethodDecl)) {
            return true;
        }
    }
    return false;
}

private static boolean isConstructor(JCTree.JCMethodDecl jcMethodDecl) {
    String name = jcMethodDecl.name.toString();
    if ("<init>".equals(name)) {
        return true;
    }
    return false;
}

private static boolean isNoArgsMethod(JCTree.JCMethodDecl jcMethodDecl) {
    List<JCTree.JCVariableDecl> jcVariableDeclList = jcMethodDecl.getParameters();
    if (jcVariableDeclList == null
            || jcVariableDeclList.size() == 0) {
        return true;
    }
    return false;
}

private  boolean isPublicMethod(JCTree.JCMethodDecl jcMethodDecl) {
    JCTree.JCModifiers jcModifiers = jcMethodDecl.getModifiers();
    Set<Modifier> modifiers = jcModifiers.getFlags();
    if (modifiers.contains(Modifier.PUBLIC)) {
        return true;
    }
    return false;
}

在 process 函數中處理構造函數

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
    set.forEach(element -> {
        JCTree jcTree = trees.getTree(element);
        //修改 jcTree 的方式,能夠修改類的定義
        jcTree.accept(new TreeTranslator() {
            @Override
            public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                createPrivateConstructor(jcClassDecl);
            }
        });
    });
    return true ;
}

在終端中輸入 mvn clean install,編譯成功後,你能夠在 target/test-classes/ 中看到編譯後的 SingletonRegistryTest。
javap 反編譯看一下 javap -p SingletonRegistryTest.class

Compiled from "SingletonRegistryTest.java"
public class SingletonRegistryTest {
  private SingletonRegistryTest();
}

5. 建立靜態內聯類

要達到的結果是這樣的

private static class SingletonRegistryHolder {
    private static SingletonRegistry instance = new SingletonRegistry();
}
private JCTree.JCClassDecl createInnerClass(JCTree.JCClassDecl singletonClass) {
    JCTree.JCClassDecl innerClass = treeMaker.ClassDef(
            treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC),
            names.fromString(singletonClass.name+"Holder"),  //類名
            nil(),                                           //泛型參數
            null,                                            //extending
            nil(),                                           //implementing
            nil()                                            //類定義的詳細語句,包括字段,方法定義等
    );
    addInstanceVar(innerClass, singletonClass);              //給類添加添加 instance變量
    singletonClass.defs = singletonClass.defs.append(innerClass);
    return innerClass;
}

private void addInstanceVar(JCTree.JCClassDecl innerClass, JCTree.JCClassDecl singletonClass) {
    JCTree.JCIdent singletonClassType = treeMaker.Ident(singletonClass.name); //獲取註解的類型
    //new SingletonRegistry() 的語句
    JCTree.JCNewClass newKeyword = treeMaker.NewClass( null,                 //encl,enclosingExpression lambda 箭頭嗎?不太清楚
                                                       nil(),                //參數類型列表
                                                       singletonClassType,   //待建立對象的類型
                                                       nil(),                //參數蕾西
                                                       null);                //類定義

    JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL);
    //定義變量
    JCTree.JCVariableDecl instanceVar = treeMaker.VarDef(
            fieldMod,                                                        //修飾符
            names.fromString("instance"),                                    //變量名
            singletonClassType,                                              //類型
            newKeyword);                                                     //賦值語句
    innerClass.defs = innerClass.defs.prepend(instanceVar);
}

6. 建立一個成員函數,返回內聯類中的 instance 變量

目標完成

public static SingletonRegistry getInstance() {
    return SingletonRegistryHolder.instance;
}
private void createReturnInstance(JCTree.JCClassDecl singletonClass,JCTree.JCClassDecl innerClass){

    JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC);

    JCTree.JCIdent singletonClassType = treeMaker.Ident(singletonClass.name);
    //獲取 return 語句塊
    JCTree.JCBlock body = addReturnBlock(innerClass);
    //建立方法
    JCTree.JCMethodDecl  methodDec = treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC|Flags.STATIC), 
                                                        this.names.fromString("getInstance"), 
                                                        singletonClassType, nil(), nil(), nil(), body , null);

    singletonClass.defs = singletonClass.defs.prepend(methodDec);
}


private JCTree.JCBlock addReturnBlock(JCTree.JCClassDecl holderInnerClass) {

    JCTree.JCIdent holderInnerClassType = treeMaker.Ident(holderInnerClass.name);

    JCTree.JCFieldAccess instanceVarAccess = treeMaker.Select(holderInnerClassType, names.fromString("instance")); //獲取內聯類中的靜態變量
    JCTree.JCReturn returnValue = treeMaker.Return(instanceVarAccess);//建立 return 語句

    ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
    statements.append(returnValue);

    return treeMaker.Block(0L, statements.toList());
}

最後的 processor 函數像是這樣的

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);

    set.forEach(element -> {
        JCTree jcTree = trees.getTree(element);
        jcTree.accept(new TreeTranslator() {
            @Override
            public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                createPrivateConstructor(jcClassDecl);
                JCTree.JCClassDecl innerClass= createInnerClass(jcClassDecl);
                createReturnInstance(jcClassDecl,innerClass);
            }
        });
    });

    return true;
}

完成了,mvn clean install 一下,使用 idea 查看對應的 .class 文件,生成的結果就好漂亮了。
```java
public class SingletonRegistryTest {
public static SingletonRegistryTest getInstance() {
return SingletonRegistryTest.SingletonRegistryTestHolder.instance;
}

private SingletonRegistryTest() {
}

private static class SingletonRegistryTestHolder {
    private static final SingletonRegistryTest instance = new SingletonRegistryTest();

    private SingletonRegistryTestHolder() {
    }
}

}

參考連接

相關文章
相關標籤/搜索