Android Annotation-讓你的程式碼更加優雅(二)做一個Java詩人(JavaPoet)

xNPE發表於2018-10-16

上篇回顧

上一篇我們按照思維導圖,介紹了註解的基礎知識,如何定義一個註解,提示性註解,執行時註解的寫法和用法。沒有看過第一篇,又對註解知識相對陌生的同學,建議先食用第一篇。本篇將重點介紹編譯期註解,自動生成Java檔案相關內容。第一篇傳送門:

Android Annotation-讓你的程式碼更加優雅(一)

本篇食用路線

照例,這裡先給出本篇的學習導圖。方便大家掌握學習大綱。本章照例會先給出一些用來處理編譯期註解的基礎類和方法,然後通過一些具體的例子學習如何利用編譯期註解來實現一些便捷功能。本篇的食用時間可能稍長,建議收藏後慢慢食用。

Android Annotation-讓你的程式碼更加優雅(二)做一個Java詩人(JavaPoet)

編譯期靜態處理-做一個Java詩人

JavaPoet簡介

JavaPoet是square公司的開源庫,傳送門見下面。從名字就可以看出,Java詩人,即JavaPoet是一個通過註解生成java檔案的庫。我們可以利用註解,運用JavaPoet來生成一些重複的模板程式碼。從而大大提高我們程式設計效率。像我們熟知的ButterKnife,就是通過這種方法來簡化程式碼編寫的。在JavaPoet使用過程中,也需要用到一些Java API,我們會在後文一併講解。

Github-JavaPoet

使用時,引入依賴就可以了:

compile 'com.squareup:javapoet:1.7.0'
複製程式碼

“詩人”眼中的結構化Java檔案

瞭解編譯原理的同學都知道,在編譯器眼中,程式碼檔案其實就是按一定語法編寫的結構化資料。編譯器在處理Java檔案時,也是按照既定的語法,分析Java檔案的結構化資料。結構化資料就是我們日常編寫Java檔案時用到的基本元素。在Java中,對於編譯器來說程式碼中的元素結構是基本不變的,例如組成程式碼的基本元素包括包、類、函式、欄位、變數等,JDK為這些元素定義了一個基類,也就是Element,我們用Element及其子類來表示這些基本元素,它共用5個子類:

類名 表達的元素
PackageElement 表示一個包程式元素,可以獲取到包名等
TypeElement 表示一個類或介面程式元素
VariableElement 表示一個欄位、enum 常量、方法或構造方法引數、區域性變數、類成員變數或異常引數
ExecutableElement 表示某個類或介面的方法、構造方法或初始化程式(靜態或例項),包括註解型別元素
TypeParameterElement 表示一般類、介面、方法或構造方法元素的泛型引數

通過一個例子來明確一下:

package com.xm.test;    // 包名,PackageElement

public class Test<      // 類名,TypeElement
    T                   // 泛型引數,TypeParameterElement
    > {     

    private int a;       // 成員變數,VariableElement
    private Test other;  // 成員變數,VariableElement

    public Test () {}    // 成員方法,ExecuteableElement
    public void setA (   // 成員方法,ExecuteableElement
                     int newA       // 方法引數,VariableElement
    ) {
        String test;     // 區域性變數,VariableElement
    }
}
複製程式碼

當編譯器操作Java檔案中的元素時,就是通過上面這些類來進行操作的。即我們想通過JavaPoet來生成Java檔案時,就可以使用這些子類來表達結構化程式的元素。任何一個Element類物件,都可以根據實際情況,強轉成對應的子類。而Element類,實際上是一個介面,它定義了一套方法,我們來一起看一下。

public interface Element extends AnnotatedConstruct {  
    /** 
     * 返回此元素定義的型別 
     * 例如,對於一般類元素 Clazz<P extends People>,返回引數化型別 Clazz<P> 
     */  
    TypeMirror asType();  
  
    /** 
     * 返回此元素的種類:包、類、介面、方法、欄位...,如下列舉值 
     * PACKAGE, ENUM, CLASS, ANNOTATION_TYPE, INTERFACE, ENUM_CONSTANT, FIELD, PARAMETER, LOCAL_VARIABLE, EXCEPTION_PARAMETER, 
     * METHOD, CONSTRUCTOR, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER, OTHER, RESOURCE_VARIABLE; 
     */  
    ElementKind getKind();  
  
    /** 
     * 返回此元素的修飾符,如下列舉值 
     * PUBLIC, PROTECTED, PRIVATE, ABSTRACT, DEFAULT, STATIC, FINAL, 
     * TRANSIENT, VOLATILE, SYNCHRONIZED, NATIVE, STRICTFP; 
     */  
    Set<Modifier> getModifiers();  
  
    /** 
     * 返回此元素的簡單名稱,例如 
     * 型別元素 java.util.Set<E> 的簡單名稱是 "Set"; 
     * 如果此元素表示一個未指定的包,則返回一個空名稱; 
     * 如果它表示一個構造方法,則返回名稱 "<init>"; 
     * 如果它表示一個靜態初始化程式,則返回名稱 "<clinit>"; 
     * 如果它表示一個匿名類或者例項初始化程式,則返回一個空名稱 
     */  
    Name getSimpleName();  
  
    /** 
     * 返回封裝此元素的最裡層元素。 
     * 如果此元素的宣告在詞法上直接封裝在另一個元素的宣告中,則返回那個封裝元素; 
     * 如果此元素是頂層型別,則返回它的包; 
     * 如果此元素是一個包,則返回 null; 
     * 如果此元素是一個泛型引數,則返回 null. 
     */  
    Element getEnclosingElement();  
  
    /** 
     * 返回此元素直接封裝的子元素 
     */  
    List<? extends Element> getEnclosedElements();  
    
    boolean equals(Object var1);

    int hashCode();
  
    /** 
     * 返回直接存在於此元素上的註解 
     * 要獲得繼承的註解,可使用 getAllAnnotationMirrors 
     */  
    List<? extends AnnotationMirror> getAnnotationMirrors();  
  
    /** 
     * 返回此元素針對指定型別的註解(如果存在這樣的註解),否則返回 null。註解可以是繼承的,也可以是直接存在於此元素上的 
     */   
    <A extends Annotation> A getAnnotation(Class<A> annotationType); 
     
    <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}
複製程式碼

“詩人”的大腦-APT(Annotation Processor Tool)註解處理器

APT,說其是詩人的大腦,是因為我們整個程式碼生成任務的核心,需要用到註解的處理器提供的方法和入口。在整個流程的核心部分,都是由APT來實現和控制的。APT是一種處理註解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理註解,一個註解的註解處理器,以java程式碼(或者編譯過的位元組碼)作為輸入,生成.java檔案作為輸出,核心是交給自己定義的處理器去處理。實際上,APT在編譯期留出了一個供我們程式設計的一套模板介面。我們通過實現處理器中的方法,就可以編寫自己的註解處理流程了。

APT核心-AbstractProcessor

每個自定義的註解處理器,都要繼承虛處理器AbstractProcessor,來實現其幾個關鍵方法。

虛處理器AbstractProcessor的幾個關鍵方法:

public class MyProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    @Override
    public SourceVersion getSupportedSourceVersion() { }
}
複製程式碼

當我們實現自定義的註解處理器時,上述的這幾個方法,是必須要實現的。下面重點介紹一下這四個方法:

  • init(ProcessingEnvironment env):每一個註解處理器類都必須有一個空的建構函式。然而,這裡有一個特殊的init()方法,它會被註解處理工具呼叫,並輸入ProcessingEnviroment引數。ProcessingEnviroment提供很多有用的工具類,如Elements, Types和Filer。

  • process(Set<? extends TypeElement> annotations, RoundEnvironment env):這相當於每個處理器的主函式main()。你在這裡寫你的掃描、評估和處理註解的程式碼,以及生成Java檔案。輸入引數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素。這是一個布林值,表明註解是否已經被處理器處理完成,官方原文whether or not the set of annotations are claimed by this processor,通常在處理出現異常直接返回false、處理完成返回true。

  • getSupportedAnnotationTypes():必須要實現;用來表示這個註解處理器是註冊給哪個註解的。返回值是一個字串的集合,包含本處理器想要處理的註解型別的合法全稱。

  • getSupportedSourceVersion():用來指定你使用的Java版本。通常這裡返回SourceVersion.latestSupported(),你也可以使用SourceVersion_RELEASE_6、7、8註冊處理器版本。

由於註解處理器是javac的工具,因此我們必須將自定義的處理器註冊到javac中,方法是我們需要提供一個.jar檔案,打包你的註解處理器到此檔案中,並且在jar中,需要打包一個特定的檔案 javax.annotation.processing.Processor到META-INF/services路徑下 。而這一切都是極為繁瑣的。幸好谷歌給我們開發了AutoService註解,你只需要引入這個依賴,然後在你的處理器類上加上註解:

@AutoService(Processor.class)
複製程式碼

然後我們就可以自動生成檔案,並打包進jar中。省去了很多麻煩事兒。

那麼上面我們介紹完處理器相關內容,下面我們再來看一看APT還為我們提供了哪些其它工具。

APT提供的四個輔助工具

這四個工具,我們通過在AbstractProcessor的實現類中,通過ProcessingEnvironment即可獲得:

    private Filer mFiler;
    private Elements mElementUtils;
    private Messager mMessager;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        mFiler = processingEnv.getFiler();
    }
複製程式碼

Filer

從名字看得出來,與檔案相關的操作會用到。一般配合JavaPoet來生成Java檔案

Messager

它提供給註解處理器一個報告錯誤、警告資訊的途徑。當我們自定義的註解處理器執行時報錯時,那麼執行註解處理器的JVM也會崩潰,列印出一些不容易被應用開發者讀懂的日誌資訊。這時,我們可以藉助Messager輸出一些除錯資訊,以更直觀的方式提示程式執行的錯誤。

Types

Types是一個用來操作TypeMirror的工具。TypeMirror是Element中通過adType()方法得到的一個物件。它儲存了元素的具體資訊,比如Element是一個類,那麼其成員詳細資訊就儲存在TypeMirror中。

Elements

Elements是一個用來處理Element的工具。這裡不詳細展開了。用到的時候會提到。

“詩人”的工具箱

JavaPoet為我們提供了編譯期通過操作Java檔案結構元素,依據註解生成Java檔案的便捷方法。那麼如何來生成呢?我們先有必要來了解一下JavaPoet為我們提供了哪些工具。

JavaPoet為我們提供的四個表達Java檔案元素的常用類

這些用來表達Java檔案元素的類,其實和上面說的Element有異曲同工之妙。現在沒法具體理解沒關係,後面有例子。

類名 含義
MethodSpec 代表一個建構函式或方法宣告
TypeSpec 代表一個類,介面,或者列舉宣告
FieldSpec 代表一個成員變數,一個欄位宣告
ParameterSpec 代表一個引數,可用來生成引數
AnnotationSpec 代表一個註解
JavaFile 包含一個頂級類的Java檔案

“詩人”實戰

我們先來整體看一個簡單的例子,然後再擴充到各種情況。

一個例子

假如我們定義瞭如下一個註解,運用上一篇我們學過的知識:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Xnpe {
    String value();
}
複製程式碼

接下來實現註解處理器:

@AutoService(Processor.class)
public class XnpeProcess extends AbstractProcessor {

    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement element : annotations) {
            if (element.getQualifiedName().toString().equals(Xnpe.class.getCanonicalName())) {
                MethodSpec main = MethodSpec.methodBuilder("main")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(void.class)
                        .addParameter(String[].class, "args")
                        .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                        .build();
                TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addMethod(main)
                        .build();

                try {
                    JavaFile javaFile = JavaFile.builder("com.xm", helloWorld)
                            .addFileComment(" This codes are generated automatically. Do not modify!")
                            .build();
                    javaFile.writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Xnpe.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
複製程式碼

這裡需要一點耐心了,乍看起來有點多,但實際上比較簡單。這裡我們總結出實現自定義註解處理器的幾個關鍵步驟:

  1. 為註解處理器增加@AutoService註解。即@AutoService(Processor.java)
  2. 實現上文說的自定義註解常用到的四個方法,即init()、process()、getSupportedAnnotationTypes和getSupportedSourceVersion。
  3. 編寫處理註解的邏輯。

本例中,我們先來重點看第二條,即四個大方法的實現。重點在處理方法上,即process()方法。我們拿出其中的核心部分做一個講解。

MethodSpec main = MethodSpec.methodBuilder("main") //MethodSpec當然是methodBuilder,即建立方法。
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//增加限定符
    .returns(void.class)                           //指定返回值型別
    .addParameter(String[].class, "args")          //指定方法的引數
    .addStatement("$T.out.println($S)", System.class, "Hello, I am Poet!")//新增邏輯程式碼。
    .build();                                      //建立
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") //TypeSpec構建Class
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL) //增加限定符
    .addMethod(main)                               //將剛才建立的main方法新增進類中。
    .build();                                      //建立
複製程式碼

是不是流程上很容易理解。MethodSpec是用來生成方法的,詳細解釋可參加程式碼上的註釋。

細心的你也許注意到了,程式碼中有些$T等字樣的東東,這個又是什麼呢?下面我們通過幾個小例子,一方面來了解一下Poet中的一些佔位符,另一方面也熟悉一下常用的方法。

常用方法

addCodeaddStatement用來增加程式碼
MethodSpec main = MethodSpec.methodBuilder("main")
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n")
    .build();
複製程式碼

生成的是

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}
複製程式碼
  • addCode用於增加極簡程式碼。即程式碼中僅包含純Java基礎型別和運算。
  • addStatement用於增加一些需要import方法的程式碼。如上面的.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") 就需要使用.addStatement來宣告。
beginControlFlow和endControlFlow,流控方法

流控方法主要用來實現一些流控程式碼的新增,比上面的add方法看著美觀一點。比如上面的程式碼,可以改寫為:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("int total = 0")
    .beginControlFlow("for (int i = 0; i < 10; i++)")
    .addStatement("total += i")
    .endControlFlow()
    .build();
複製程式碼

佔位符

$L字面量(Literals)

private MethodSpec computeRange(String name, int from, int to, String op) {
  return MethodSpec.methodBuilder(name)
      .returns(int.class)
      .addStatement("int result = 0")
      .beginControlFlow("for (int i = $L; i < $L; i++)", from, to)
      .addStatement("result = result $L i", op)
      .endControlFlow()
      .addStatement("return result")
      .build();
}
複製程式碼

當我們傳參呼叫時,coputeRange("test", 0, 10, "+")它能生成的程式碼如下:

int test(){
    int result = 0;
    for(int i = 0; i < 10; i++) {
        result = result + i;
    }
    return result;
}
複製程式碼

$S 字串常量(String)

這個比較容易理解,這裡就不贅述了。

$T 型別(Types)

MethodSpec today = MethodSpec.methodBuilder("today")
    .returns(Date.class)
    .addStatement("return new $T()", Date.class)
    .build(); //建立today方法
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(today)
    .build(); //建立HelloWorld類
JavaFile javaFile = JavaFile.builder("com.xm.helloworld", helloWorld).build();
javaFile.writeTo(System.out);//寫java檔案
複製程式碼

生成的程式碼如下,我們看到,它會自動匯入所需的包。這也是我們使用佔位符的好處,也是使用JavaPoet的一大好處。

package com.xm.helloworld;

import java.util.Date;

public final class HelloWorld {
  Date today() {
    return new Date();
  }
}
複製程式碼

如果我們想要匯入自己寫的類怎麼辦?上面的例子是傳入系統的class,這裡也提供一種方式,通過ClassName.get(”類的路徑”,”類名“),結合$T可以生成

ClassName testClass = ClassName.get("com.xm", "TestClass");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOftestClasses = ParameterizedTypeName.get(list, testClass);

MethodSpec xNpe = MethodSpec.methodBuilder("xNpe")
    .returns(listOftestClasses)
    .addStatement("$T result = new $T<>()", listOftestClasses, arrayList)
    .addStatement("result.add(new $T())", testClass)
    .addStatement("result.add(new $T())", testClass)
    .addStatement("result.add(new $T())", testClass)
    .addStatement("return result")
    .build();
複製程式碼

生成的程式碼如下:

package com.xm.helloworld;

import com.xm.TestClass;
import java.util.ArrayList;
import java.util.List;

public final class HelloWorld {
  List<TestClass> xNpe() {
    List<TestClass> result = new ArrayList<>();
    result.add(new TestClass());
    result.add(new TestClass());
    result.add(new TestClass());
    return result;
  }
}
複製程式碼

Javapoet 同樣支援import static,通過addStaticImport來新增:

JavaFile.builder("com.xm.helloworld", hello)
    .addStaticImport(TestClass, "START")
    .addStaticImport(TestClass2, "*")
    .addStaticImport(Collections.class, "*")
    .build();
複製程式碼

$N 命名(Names)

通常指我們自己生成的方法名或者變數名等等。比如這樣的程式碼:

public String byteToHex(int b) {
  char[] result = new char[2];
  result[0] = hexDigit((b >>> 4) & 0xf);
  result[1] = hexDigit(b & 0xf);
  return new String(result);
}

public char hexDigit(int i) {
  return (char) (i < 10 ? i + '0' : i - 10 + 'a');
}
複製程式碼

這個例子中,我們在byteToHex中需要呼叫到hexDigit方法,我們就可以用$N來表示這種引用。然後通過傳遞方法名,達到這種呼叫語句的生成。

MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit")
    .addParameter(int.class, "i")
    .returns(char.class)
    .addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')")
    .build();

MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex")
    .addParameter(int.class, "b")
    .returns(String.class)
    .addStatement("char[] result = new char[2]")
    .addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit)
    .addStatement("result[1] = $N(b & 0xf)", hexDigit)
    .addStatement("return new String(result)")
    .build();
複製程式碼

自頂向下,構建Java類的元素

普通方法

我們在定義方法時,也要對方法增加一些修飾符,如Modifier.ABSTRACT。可以通過addModifiers()方法:

MethodSpec test = MethodSpec.methodBuilder("test")
    .addModifiers(Modifier.ABSTRACT, Modifier.PROTECTED)
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addMethod(test)
    .build();
複製程式碼

將會生成如下程式碼:

public abstract class HelloWorld {
  protected abstract void test();
}
複製程式碼

構造器

構造器只不過是一個特殊的方法,因此可以使用MethodSpec來生成構造器方法。使用constrctorBuilder來生成:

MethodSpec flux = MethodSpec.constructorBuilder()
    .addModifiers(Modifier.PUBLIC)
    .addParameter(String.class, "greeting")
    .addStatement("this.$N = $N", "greeting", "greeting")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL)
    .addMethod(flux)
    .build();
複製程式碼

將會生成程式碼:

public class HelloWorld {
  private final String greeting;

  public HelloWorld(String greeting) {
    this.greeting = greeting;
  }
}
複製程式碼

各種引數

引數也有自己的一個專用類ParameterSpec,我們可以使用ParameterSpec.builder()來生成引數,然後MethodSpec的addParameter去使用,這樣更加優雅。

ParameterSpec android = ParameterSpec.builder(String.class, "android")
    .addModifiers(Modifier.FINAL)
    .build();

MethodSpec welcomeOverlords = MethodSpec.methodBuilder("test")
    .addParameter(android)
    .addParameter(String.class, "robot", Modifier.FINAL)
    .build();
複製程式碼

生成的程式碼

void test(final String android, final String robot) {
}
複製程式碼

欄位,成員變數

可以使用FieldSpec去宣告欄位,然後加到類中:

FieldSpec android = FieldSpec.builder(String.class, "android")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(android)
    .addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL)
    .build();
複製程式碼

生成程式碼:

public class HelloWorld {
  private final String android;
  private final String robot;
}
複製程式碼

通常Builder可以更加詳細的建立欄位的內容,比如javadoc、annotations或者初始化欄位引數等,如:

FieldSpec android = FieldSpec.builder(String.class, "android")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .initializer("$S + $L", "Pie v.", 9.0)//初始化賦值
    .build();
複製程式碼

對應生成的程式碼:

private final String android = "Pie v." + 9.0;
複製程式碼

介面

介面方法必須是PUBLIC ABSTRACT並且介面欄位必須是PUBLIC STATIC FINAL ,使用TypeSpec.interfaceBuilder如下:

TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(FieldSpec.builder(String.class, "KEY_START")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
        .initializer("$S", "start")
        .build())
    .addMethod(MethodSpec.methodBuilder("beep")
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .build())
    .build();
複製程式碼

生成的程式碼如下:

public interface HelloWorld {
  String KEY_START = "start";
  void beep();
}
複製程式碼

列舉型別

使用TypeSpec.enumBuilder來建立,使用addEnumConstant來新增列舉值:

TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
    .addModifiers(Modifier.PUBLIC)
    .addEnumConstant("ROCK")
    .addEnumConstant("SCISSORS")
    .addEnumConstant("PAPER")
    .build();
複製程式碼

生成的程式碼

public enum Roshambo {
  ROCK,
  SCISSORS,
  PAPER
}
複製程式碼

匿名內部類

需要使用Type.anonymousInnerClass(""),通常可以使用$L佔位符來指代:

TypeSpec comparator = TypeSpec.anonymousClassBuilder("")
    .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class))
    .addMethod(MethodSpec.methodBuilder("compare")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .addParameter(String.class, "a")
        .addParameter(String.class, "b")
        .returns(int.class)
        .addStatement("return $N.length() - $N.length()", "a", "b")
        .build())
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addMethod(MethodSpec.methodBuilder("sortByLength")
        .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings")
        .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator)
        .build())
    .build();
複製程式碼

生成程式碼:

void sortByLength(List<String> strings) {
  Collections.sort(strings, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
      return a.length() - b.length();
    }
  });
}
複製程式碼

定義匿名內部類的一個特別棘手的問題是引數的構造。在上面的程式碼中我們傳遞了不帶引數的空字串。TypeSpec.anonymousClassBuilder("")

註解

註解使用起來比較簡單,通過addAnnotation就可以新增:

MethodSpec toString = MethodSpec.methodBuilder("toString")
    .addAnnotation(Override.class)
    .returns(String.class)
    .addModifiers(Modifier.PUBLIC)
    .addStatement("return $S", "Hello XiaoMing")
    .build();
複製程式碼

生成程式碼

@Override
public String toString() {
  return "Hello XiaoMing";
}
複製程式碼

通過AnnotationSpec.builder() 可以對註解設定屬性:

MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addAnnotation(AnnotationSpec.builder(Headers.class)
        .addMember("accept", "$S", "application/json; charset=utf-8")
        .addMember("userAgent", "$S", "Square Cash")
        .build())
    .addParameter(LogRecord.class, "logRecord")
    .returns(LogReceipt.class)
    .build();
複製程式碼

程式碼生成如下

@Headers(
    accept = "application/json; charset=utf-8",
    userAgent = "Square Cash"
)
LogReceipt recordEvent(LogRecord logRecord);
複製程式碼
MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addAnnotation(AnnotationSpec.builder(HeaderList.class)
        .addMember("value", "$L", AnnotationSpec.builder(Header.class)
            .addMember("name", "$S", "Accept")
            .addMember("value", "$S", "application/json; charset=utf-8")
            .build())
        .addMember("value", "$L", AnnotationSpec.builder(Header.class)
            .addMember("name", "$S", "User-Agent")
            .addMember("value", "$S", "Square Cash")
            .build())
        .build())
    .addParameter(LogRecord.class, "logRecord")
    .returns(LogReceipt.class
    .build
複製程式碼

生成Java檔案

生成Java檔案,我們需要用到上文提到的FilerElements。注意下面這段程式碼,重要的是包名,類名的指定。這裡生成的檔名,一般會遵循某個約定,以便事先寫好反射程式碼。

//獲取待生成檔案的包名
public String getPackageName(TypeElement type) {
    return mElementUtils.getPackageOf(type).getQualifiedName().toString();
}

//獲取待生成檔案的類名
private static String getClassName(TypeElement type, String packageName) {
    int packageLen = packageName.length() + 1;
    return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}

//生成檔案
private void writeJavaFile() {
    String packageName = getPackageName(mClassElement);
    String className = getClassName(mClassElement, packageName);
    ClassName bindClassName = ClassName.get(packageName, className);
    TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
        .addModifiers(Modifier.PUBLIC)
        .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR,
                                                     TypeName.get(mClassElement.asType())))
        .addMethod(methodBuilder.build())
        .build();
    //使用JavaFile的builder來生成java檔案
    JavaFile.builder(packageName, finderClass).build().writeTo(mFiler);
}
複製程式碼

總結

通過兩篇的學習,我們熟悉了Java註解的用途,寫法,以及如何用它為我們的編碼或程式服務。本篇羅列了很多具體的例子,希望能覆蓋到日常大家使用的方方面面,大家也可以收藏本文,在使用JavaPoet的時候進行參照。

小銘出品,必屬精品

歡迎關注xNPE技術論壇,更多原創乾貨每日推送。

Android Annotation-讓你的程式碼更加優雅(二)做一個Java詩人(JavaPoet)

相關文章