天天看點

面試官:你天天用 Lombok,說說它什麼原理?我竟然答不上來…

作者:九年義務教育漏網之魚

相信大家在項目中都使用過Lombok,因為能夠簡化我們許多的代碼,但是該有的功能一點也不少。那麼lombok到底是個什麼呢,lombok是一個可以通過簡單的注解的形式來幫助我們簡化消除一些必須有但顯得很臃腫的 Java 代碼的工具。

簡單來說,比如我們建立了一個類,然後在其中寫了幾個字段,然後通常情況下我們需要手動去建立getter和setter方法啊,構造函數啊之類的,lombok的作用就是為了省去我們手動建立這些代碼的麻煩,它能夠在我們編譯源碼的時候自動幫我們生成這些方法。

那麼Lombok到底是如何做到這些的呢?其實底層就是用到了編譯時注解的功能。

Lombok如何使用

Lombok是一個開源項目,代碼是在lombok中,如果是gradle項目的話直接在項目中引用如下即可。

compile ("org.projectlombok:lombok:1.16.6")      

功能

那麼Lombok是做什麼呢?其實很簡單,一個最簡單的例子就是能夠通過添加注解自動生成一些方法,使我們代碼更加簡潔易懂。例如下面一個類。

@Data
public class TestLombok {
    private String name;
    private Integer age;

    public static void main(String[] args) {
        TestLombok testLombok = new TestLombok();
        testLombok.setAge(12);
        testLombok.setName("zs");
    }
}      

我們使用Lombok提供的​

​Data​

​​注解,在沒有寫​

​get、set​

​​方法的時候也能夠使用其​

​get、set​

​​方法。我們看它編譯過後的​

​class​

​​檔案,可以看到它給我們自動生成了​

​get、set​

​方法。

public class TestLombok {
    private String name;
    private Integer age;

    public static void main(String[] args) {
        TestLombok testLombok = new TestLombok();
        testLombok.setAge(12);
        testLombok.setName("zs");
    }

    public TestLombok() {
    }

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

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

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

    public void setAge(Integer age) {
        this.age = age;
    }

}      

當然Lombok的功能不止如此,還有很多其他的注解幫助我們簡便開發,網上有許多的關于Lombok的使用方法,這裡就不再啰嗦了。正常情況下我們在項目中自定義注解,或者使用​

​Spring​

​​架構中​

​@Controller、@Service​

​​等等這類注解都是​

​運作時注解​

​​,運作時注解大部分都是通過反射來實作的。而​

​Lombok​

​是使用編譯時注解實作的。那麼編譯時注解是什麼呢?

編譯時注解

注解(也被成為中繼資料)為我們在代碼中添加資訊提供了一種形式化的方法,使我們可以在稍後某個時刻非常友善地使用這些資料。 ——————摘自《Thinking in Java》

Java中的注解分為運作時注解和編譯時注解,運作時注解就是我們經常使用的在程式運作時通過反射得到我們注解的資訊,然後再做一些操作。而編譯時注解是什麼呢?就是在程式在編譯期間通過注解處理器進行處理。

  • 編譯期:Java語言的編譯期是一段不确定的操作過程,因為它可能是将​

    ​*.java​

    ​​檔案轉化成​

    ​*.class​

    ​​檔案的過程;也可能是指将位元組碼轉變成機器碼的過程;還可能是直接将​

    ​*.java​

    ​編譯成本地機器代碼的過程
  • 運作期:從JVM加載位元組碼檔案到記憶體中,到最後使用完畢以後解除安裝的過程都屬于運作期的範疇。

注解處理工具apt

注解處理工具apt(Annotation Processing Tool),這是Sun為了幫助注解的處理過程而提供的工具,apt被設計為操作Java源檔案,而不是編譯後的類。

它是javac的一個工具,中文意思為編譯時注解處理器。APT可以用來在編譯時掃描和處理注解。通過APT可以擷取到注解和被注解對象的相關資訊,在拿到這些資訊後我們可以根據需求來自動的生成一些代碼,省去了手動編寫。注意,擷取注解及生成代碼都是在代碼編譯時候完成的,相比反射在運作時處理注解大大提高了程式性能。APT的核心是AbstractProcessor類。

正常情況下使用APT工具隻是能夠生成一些檔案(不僅僅是我們想象的class檔案,還包括xml檔案等等之類的),并不能修改原有的檔案資訊。

但是此時估計會有疑問,那麼​

​Lombok​

​​不就是在我們原有的檔案中新增了一些資訊嗎?我在後面會有詳細的解釋,這裡簡單介紹一下,其實​

​Lombok​

​是修改了Java中的抽象文法樹​

​AST​

​才做到了修改其原有類的資訊。

接下來我們示範一下如何用​

​APT​

​​工具生成一個class檔案,然後我們再說​

​Lombok​

​是如何修改已存在的類中的屬性的。

定義注解

首先當然我們需要定義自己的注解了

@Retention(RetentionPolicy.SOURCE) // 注解隻在源碼中保留
@Target(ElementType.TYPE) // 用于修飾類
public @interface GeneratePrint {

    String value();
}      

​Retention​

​​注解上面有一個屬性value,它是​

​RetentionPolicy​

​​類型的枚舉類,​

​RetentionPolicy​

​枚舉類中有三個值。

public enum RetentionPolicy {

    SOURCE,

    CLASS,

    RUNTIME
}      
  • ​SOURCE​

    ​修飾的注解:修飾的注解,表示注解的資訊會被編譯器抛棄,不會留在class檔案中,注解的資訊隻會留在源檔案中
  • ​CLASS​

    ​修飾的注解:表示注解的資訊被保留在class檔案(位元組碼檔案)中當程式編譯時,但不會被虛拟機讀取在運作的時候
  • ​RUNTIME​

    ​修飾的注解:表示注解的資訊被保留在class檔案(位元組碼檔案)中當程式編譯時,會被虛拟機保留在運作時。是以它能夠通過反射調用,是以正常運作時注解都是使用的這個參數

​Target​

​​注解上面也有個屬性value,它是​

​ElementType​

​類型的枚舉。是用來修飾此注解作用在哪的。

public enum ElementType {
    TYPE,

    FIELD,

    METHOD,

    PARAMETER,

    CONSTRUCTOR,

    LOCAL_VARIABLE,

    ANNOTATION_TYPE,

    PACKAGE,

    TYPE_PARAMETER,

    TYPE_USE
}      

定義注解處理器

推薦一個 Spring Boot 基礎教程及實戰示例:

​​https://github.com/javastacks/spring-boot-best-practice​​

我們要定義注解處理器的話,那麼就需要繼承​

​AbstractProcessor​

​類。繼承完以後基本的架構類型如下

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("aboutjava.annotion.MyGetter")
public class MyGetterProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return true;
    }
}      

我們可以看到在子類中上面有兩個注解,注解描述如下

  • ​@SupportedSourceVersion​

    ​:表示所支援的Java版本
  • ​@SupportedAnnotationTypes​

    ​:表示該處理器要處理的注解

繼承了父類的兩個方法,方法描述如下

  • init方法:主要是獲得編譯時期的一些環境資訊
  • process方法:在編譯時,編譯器執行的方法。也就是我們寫具體邏輯的地方

我們是示範一下如何通過繼承​

​AbstractProcessor​

​​類來實作在編譯時生成類,是以我們在​

​process​

​方法中書寫我們生成類的代碼。如下所示。

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    StringBuilder builder = new StringBuilder()
            .append("package aboutjava.annotion;\n\n")
            .append("public class GeneratedClass {\n\n") // open class
            .append("\tpublic String getMessage() {\n") // open method
            .append("\t\treturn \"");
    // for each javax.lang.model.element.Element annotated with the CustomAnnotation
    for (Element element : roundEnv.getElementsAnnotatedWith(MyGetter.class)) {
        String objectType = element.getSimpleName().toString();
        // this is appending to the return statement
        builder.append(objectType).append(" says hello!\\n");
    }
    builder.append("\";\n") // end return
            .append("\t}\n") // close method
            .append("}\n"); // close class
    try { // write the file
        JavaFileObject source = processingEnv.getFiler().createSourceFile("aboutjava.annotion.GeneratedClass");
        Writer writer = source.openWriter();
        writer.write(builder.toString());
        writer.flush();
        writer.close();
    } catch (IOException e) {
        // Note: calling e.printStackTrace() will print IO errors
        // that occur from the file already existing after its first run, this is normal
    }
    return true;
}      

定義使用注解的類(測試類)

上面的兩個類就是基本的工具類了,一個是定義了注解,一個是定義了注解處理器,接下來我們來定義一個測試類(TestAno.java)。我們在類上面加上我們自定的注解類。

@MyGetter
public class TestAno {

    public static void main(String[] args) {
        System.out.printf("1");
    }
}      

這樣我們在編譯期就能生成檔案了,接下來示範一下在編譯時生成檔案,此時不要着急直接進行javac編譯,​

​MyGetter​

​​類是注解類沒錯,而​

​MyGetterProcessor​

​​是注解類的處理器,那麼我們在編譯​

​TestAno​

​Java檔案的時候就會觸發處理器。是以這兩個類是無法一起編譯的。

先給大家看一下我的目錄結構

aboutjava
    -- annotion
        -- MyGetter.java
        -- MyGetterProcessor.java
        -- TestAno.java      

是以我們先将注解類和注解處理器類進行編譯

javac aboutjava/annotion/MyGett*      

接下來進行編譯我們的測試類,此時在編譯時需要加上​

​processor​

​參數,用來指定相關的注解處理類。

javac -processor aboutjava.annotion.MyGetterProcessor aboutjava/annotion/TestAno.java      

大家可以看到動态圖中,自動生成了Java檔案。

面試官:你天天用 Lombok,說說它什麼原理?我竟然答不上來…