安卓使用註解處理器自動生成代碼操作詳解(AutoService,JavaPoet,AbstractProcessor)

關聯文章:Android自定義註解

新手村

先來說說註解處理器(AbstractProcessor)是幹嘛的,它主要是用來處理註解的一些內部邏輯,拿butterknife舉例,我聲明瞭一個bindView註解,那肯定是要寫一些邏輯才能找到控件的id對吧,AbstractProcessor就是註解處理的邏輯入口,出於性能考慮,肯定是不能使用反射來處理找id這個邏輯的,這時,JavaPoet就派上用場了,它的作用是根據特定的規則生成java代碼文件,這樣,我通過註解來拿到需要的參數,通過JavaPoet來生成模板代碼,對性能沒有任何的影響,由於ServiceLoader加載Processor需要手動註冊配置,框架AutoService就是用來自動註冊ServiceLoader的,省去了AbstractProcessor繁瑣的配置。理解了這三者的關係,下面開始真正的學習吧

副本之JavaPoet的使用

項目地址:https://github.com/square/javapoet
javapoet的api非常的通俗易懂,我用主頁的使用示例來說明一下
例如我們要生成一個這樣的代碼:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

對應的代碼爲:

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();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

MethodSpec類是用來配置方法的,一個方法包括方法名,修飾符,返回值,參數,內容,配置對應的方法已在註釋中標出。
TypeSpec爲類的配置,類包括類名,修飾符,方法,字段等
JavaFile用於指定輸出位置,生成類,我們傳入包名,和類,最後通過writeTo指定輸出到控制檯。
可以看出,複雜的地方就是在MethodSpec的配置,下面着重介紹MethodSpec的一些常用用法

基本用法

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;
  }
}

可以看到裏面的分號和換行符混在一起看起來眼花繚亂,丟失一個還不會報錯,讓人很抓狂,因此JavaPoet很貼心的準備了換行符分號和起始結束括號的api:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("int total = 0") //這段代碼之後會添加一個分號和換行符
    .beginControlFlow("for (int i = 0; i < 10; i++)")//這段代碼之後會添加一個起始的括號
    .addStatement("total += i")
    .endControlFlow()//括號結束
    .build();

此外還有一個nextControlFlow是前後都加括號,通常用於if else的邏輯判斷中,示例:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("long now = $T.currentTimeMillis()", System.class)
    .beginControlFlow("if ($T.currentTimeMillis() < now)", System.class)
    .addStatement("$T.out.println($S)", System.class, "Time travelling, woo hoo!")
    .nextControlFlow("else if ($T.currentTimeMillis() == now)", System.class)
    .addStatement("$T.out.println($S)", System.class, "Time stood still!")
    .nextControlFlow("else")
    .addStatement("$T.out.println($S)", System.class, "Ok, time still moving forward")
    .endControlFlow()
    .build();

輸出:

void main() {
  long now = System.currentTimeMillis();
  if (System.currentTimeMillis() < now)  {
    System.out.println("Time travelling, woo hoo!");
  } else if (System.currentTimeMillis() == now) {
    System.out.println("Time stood still!");
  } else {
    System.out.println("Ok, time still moving forward");
  }
}

可以看到上面有幾個不明覺厲的符號,我們稱之爲佔位符,佔位符常用的有以下幾種:

  • $T 類佔位符,用於替換代碼中的類
  • $L 姑且叫它變量佔位符吧,用法和String.format中的%s差不多,按照順序依次替換裏面的變量值
  • $S 字符串佔位符,當我們需要在代碼中使用字符串時,用這個替換
  • $N 名稱佔位符,比方說需要在一個方法裏使用另一個方法,可以用這個替換

$L演示示例,後面的變量按照順序對號入座:

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();
}

$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();

輸出:
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');
}

其餘兩個前面的示例中已經使用過了,T傳入類,S傳入字符串,注意順序:

 addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")

類的獲取與使用
java中的源碼類我們在使用的時候都會自動導入,但是我們自定義的類是不會的,所以我們需要使用ClassName來獲取我們想要的類

ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
MethodSpec today = MethodSpec.methodBuilder("tomorrow")
    .returns(hoverboard)
    .addStatement("return new $T()", hoverboard)
    .build();

輸出:
package com.example.helloworld;
import com.mattel.Hoverboard;

public final class HelloWorld {
  Hoverboard tomorrow() {
    return new Hoverboard();
  }
}

傳入包名前半段和後半段的類名就能獲取到我們想要的類了,但是參數化類型我們要怎麼定義呢,比如List<Hoverboard>,這時TypeName就上場了,TypeName有多個子類,包括上面的ClassName也是它的子類,每個子類都承擔着不同的職責:

  • ArrayTypeName 用於生成數組類,例如Hoverboard []
  • ClassName 獲取普通的類,例如Hoverboard
  • Parameterized 獲取參數化類,例如List<Hoverboard>
  • TypeVariableName 獲取類型變量,例如泛型T
  • WildcardTypeName 獲取通配符,例如? extends Hoverboard
    它們的用法差不多,以Parameterized舉例:
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);

MethodSpec beyond = MethodSpec.methodBuilder("beyond")
    .returns(listOfHoverboards)
    .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("return result")
    .build();

輸出:
package com.example.helloworld;

import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;

public final class HelloWorld {
  List<Hoverboard> beyond() {
    List<Hoverboard> result = new ArrayList<>();
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    return result;
  }
}

其他的例如字段(FieldSpec),註解(AnnotationSpec),參數(ParameterSpec)等api用起來都大同小異,由於篇幅有限,javaPoet的介紹就講到這裏,如果還有不太明白的地方或有想進一步瞭解的可以參考這個比較全面的介紹:JavaPoet使用詳解

副本之AutoService的使用

項目地址:https://github.com/google/auto
使用非常的簡單:

package foo.bar;

import javax.annotation.processing.Processor;

@AutoService(Processor.class)
final class MyProcessor implements Processor {
  // …
}

編譯後,則會在META-INF文件夾下生成Processor配置信息文件,而當外部程序裝配這個模塊的時候,
就能通過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。

副本之AbstractProcessor的使用

AbstractProcessor繼承自Processor,是一個抽象處理器,它的作用是在編譯時掃描註解並處理一些邏輯,例如生成代碼等,一般我們繼承它需要實現4個方法:

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

    //文件相關輔助類
    private Filer mFiler;
    //元素
    private Elements elements;
    //日誌信息
    private Messager messager;

    /**
     * 入口,相當於java的main入口
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        elements = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
    }

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


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

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

註解類:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
     int value();
}

init方法是一個入口,ProcessingEnvironment類主要提供了一些工具類給我們使用,我們可以在init方法中獲取我們需要的工具類。
getSupportedAnnotationTypes用於獲取我們自定義的註解,寫法可以固定
getSupportedSourceVersion用於獲取java版本,寫法可以固定
process方法是我們處理邏輯的核心方法,返回true,代表註解已申明,並要求Processor後期不用再處理了它們

參數Set<? extends TypeElement> set是請求處理的類型的集合,RoundEnvironment 是當前或之前的請求處理類型的環境,可以通過它獲取當前需要處理請求的元素,例如我需要獲取BindView註解的元素的類並獲取其中的內容可以這樣寫:

//先拿到所有使用了BindView註解的元素集合
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
 for (Element element:elementsAnnotatedWith){
          //從元素中拿到這個註解實例
            BindView annotation = element.getAnnotation(BindView.class);
          //從這個註解實例中獲取到註解中包含的值
            int value = annotation.value();
  }

這樣我們就獲取到了註解中的值,思考下butterknife中bindView註解中的那個id的獲取,是不是有點豁然開朗了呢。
我們獲取信息都是基於Element這個類展開來,所以瞭解下這個類很有必要,Element表示一個程序元素,比如包、類或者方法,主要包括以下幾種方法:

  public interface Element extends AnnotatedConstruct {
    TypeMirror asType();

    ElementKind getKind();

    Set<Modifier> getModifiers();

    Name getSimpleName();

    Element getEnclosingElement();

    List<? extends Element> getEnclosedElements();

    boolean equals(Object var1);

    int hashCode();

    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}
public interface AnnotatedConstruct {
    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <A extends Annotation> A[] getAnnotationsByType(Class<A> var1);
}
  • asType
    獲取元素的類型信息,包括包名,類名等,配合javapoet的ClassName可以直接獲取到該TypeName
    TypeName typeName = ClassName.get(element.asType());
  • getKind 用於判斷是哪種element
  • getModifiers 用於獲取元素的關鍵字public static等
  • getEnclosingElement 返回包含該element的父element
  • getAnnotation 獲取元素上的註解
  • accept是一個判斷方法,用於判斷如果是某一個元素就執行某一個方法,用的很少,不細講了

可能會遇到的問題

加入AutoService發現配置都正確,但就是不能生成代碼,原因可能是你的Gradle過高,把版本降到4.10.1或以下就可以了,原因不詳,如果有知道原因的朋友可以在留言區說一下
另外一個就是你的註解器的lib包需要使用annotationProcessor來使用,而不是implementation

文章到這就要到我們的實戰環節了,下一篇我將帶領大家仿butterknife簡單實現一個findviewbyid的功能,幫助大家更好的運用和消化這些知識,加油。關注我不迷茫,支持我的就點個讚唄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章