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

關(guān)聯(lián)文章:Android自定義注解

新手村

先來(lái)說(shuō)說(shuō)注解處理器(AbstractProcessor)是干嘛的,它主要是用來(lái)處理注解的一些內(nèi)部邏輯,拿butterknife舉例,我聲明了一個(gè)bindView注解,那肯定是要寫(xiě)一些邏輯才能找到控件的id對(duì)吧,AbstractProcessor就是注解處理的邏輯入口,出于性能考慮,肯定是不能使用反射來(lái)處理找id這個(gè)邏輯的,這時(shí),JavaPoet就派上用場(chǎng)了,它的作用是根據(jù)特定的規(guī)則生成java代碼文件,這樣,我通過(guò)注解來(lái)拿到需要的參數(shù),通過(guò)JavaPoet來(lái)生成模板代碼,對(duì)性能沒(méi)有任何的影響,由于ServiceLoader加載Processor需要手動(dòng)注冊(cè)配置,框架AutoService就是用來(lái)自動(dòng)注冊(cè)ServiceLoader的,省去了AbstractProcessor繁瑣的配置。理解了這三者的關(guān)系,下面開(kāi)始真正的學(xué)習(xí)吧

副本之JavaPoet的使用

項(xiàng)目地址:https://github.com/square/javapoet
javapoet的api非常的通俗易懂,我用主頁(yè)的使用示例來(lái)說(shuō)明一下
例如我們要生成一個(gè)這樣的代碼:

package com.example.helloworld;

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

對(duì)應(yīng)的代碼為:

MethodSpec main = MethodSpec.methodBuilder("main") //方法名
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC) //修飾符
    .returns(void.class)//返回值
    .addParameter(String[].class, "args")//參數(shù)
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//內(nèi)容
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") //類(lèi)名
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL) //修飾符
    .addMethod(main) //方法
    .build();

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

javaFile.writeTo(System.out);

MethodSpec類(lèi)是用來(lái)配置方法的,一個(gè)方法包括方法名,修飾符,返回值,參數(shù),內(nèi)容,配置對(duì)應(yīng)的方法已在注釋中標(biāo)出。
TypeSpec為類(lèi)的配置,類(lèi)包括類(lèi)名,修飾符,方法,字段等
JavaFile用于指定輸出位置,生成類(lèi),我們傳入包名,和類(lèi),最后通過(guò)writeTo指定輸出到控制臺(tái)。
可以看出,復(fù)雜的地方就是在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;
  }
}

可以看到里面的分號(hào)和換行符混在一起看起來(lái)眼花繚亂,丟失一個(gè)還不會(huì)報(bào)錯(cuò),讓人很抓狂,因此JavaPoet很貼心的準(zhǔn)備了換行符分號(hào)和起始結(jié)束括號(hào)的api:

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

此外還有一個(gè)nextControlFlow是前后都加括號(hào),通常用于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");
  }
}

可以看到上面有幾個(gè)不明覺(jué)厲的符號(hào),我們稱之為占位符,占位符常用的有以下幾種:

  • $T 類(lèi)占位符,用于替換代碼中的類(lèi)
  • $L 姑且叫它變量占位符吧,用法和String.format中的%s差不多,按照順序依次替換里面的變量值
  • $S 字符串占位符,當(dāng)我們需要在代碼中使用字符串時(shí),用這個(gè)替換
  • $N 名稱占位符,比方說(shuō)需要在一個(gè)方法里使用另一個(gè)方法,可以用這個(gè)替換

$L演示示例,后面的變量按照順序?qū)μ?hào)入座:

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

其余兩個(gè)前面的示例中已經(jīng)使用過(guò)了,T傳入類(lèi),S傳入字符串,注意順序:

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

類(lèi)的獲取與使用
java中的源碼類(lèi)我們?cè)谑褂玫臅r(shí)候都會(huì)自動(dòng)導(dǎo)入,但是我們自定義的類(lèi)是不會(huì)的,所以我們需要使用ClassName來(lái)獲取我們想要的類(lèi)

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

傳入包名前半段和后半段的類(lèi)名就能獲取到我們想要的類(lèi)了,但是參數(shù)化類(lèi)型我們要怎么定義呢,比如List<Hoverboard>,這時(shí)TypeName就上場(chǎng)了,TypeName有多個(gè)子類(lèi),包括上面的ClassName也是它的子類(lèi),每個(gè)子類(lèi)都承擔(dān)著不同的職責(zé):

  • ArrayTypeName 用于生成數(shù)組類(lèi),例如Hoverboard []
  • ClassName 獲取普通的類(lèi),例如Hoverboard
  • Parameterized 獲取參數(shù)化類(lèi),例如List<Hoverboard>
  • TypeVariableName 獲取類(lèi)型變量,例如泛型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),參數(shù)(ParameterSpec)等api用起來(lái)都大同小異,由于篇幅有限,javaPoet的介紹就講到這里,如果還有不太明白的地方或有想進(jìn)一步了解的可以參考這個(gè)比較全面的介紹:JavaPoet使用詳解

副本之AutoService的使用

項(xiàng)目地址:https://github.com/google/auto
使用非常的簡(jiǎn)單:

package foo.bar;

import javax.annotation.processing.Processor;

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

編譯后,則會(huì)在META-INF文件夾下生成Processor配置信息文件,而當(dāng)外部程序裝配這個(gè)模塊的時(shí)候,
就能通過(guò)該jar包META-INF/services/里的配置文件找到具體的實(shí)現(xiàn)類(lèi)名,并裝載實(shí)例化,完成模塊的注入。

副本之AbstractProcessor的使用

AbstractProcessor繼承自Processor,是一個(gè)抽象處理器,它的作用是在編譯時(shí)掃描注解并處理一些邏輯,例如生成代碼等,一般我們繼承它需要實(shí)現(xiàn)4個(gè)方法:

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

    //文件相關(guān)輔助類(lèi)
    private Filer mFiler;
    //元素
    private Elements elements;
    //日志信息
    private Messager messager;

    /**
     * 入口,相當(dāng)于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();
    }
}

注解類(lèi):

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

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

參數(shù)Set<? extends TypeElement> set是請(qǐng)求處理的類(lèi)型的集合,RoundEnvironment 是當(dāng)前或之前的請(qǐng)求處理類(lèi)型的環(huán)境,可以通過(guò)它獲取當(dāng)前需要處理請(qǐng)求的元素,例如我需要獲取BindView注解的元素的類(lèi)并獲取其中的內(nèi)容可以這樣寫(xiě):

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

這樣我們就獲取到了注解中的值,思考下butterknife中bindView注解中的那個(gè)id的獲取,是不是有點(diǎn)豁然開(kāi)朗了呢。
我們獲取信息都是基于Element這個(gè)類(lèi)展開(kāi)來(lái),所以了解下這個(gè)類(lèi)很有必要,Element表示一個(gè)程序元素,比如包、類(lèi)或者方法,主要包括以下幾種方法:

  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
    獲取元素的類(lèi)型信息,包括包名,類(lèi)名等,配合javapoet的ClassName可以直接獲取到該TypeName
    TypeName typeName = ClassName.get(element.asType());
  • getKind 用于判斷是哪種element
  • getModifiers 用于獲取元素的關(guān)鍵字public static等
  • getEnclosingElement 返回包含該element的父element
  • getAnnotation 獲取元素上的注解
  • accept是一個(gè)判斷方法,用于判斷如果是某一個(gè)元素就執(zhí)行某一個(gè)方法,用的很少,不細(xì)講了

可能會(huì)遇到的問(wèn)題

加入AutoService發(fā)現(xiàn)配置都正確,但就是不能生成代碼,原因可能是你的Gradle過(guò)高,把版本降到4.10.1或以下就可以了,原因不詳,如果有知道原因的朋友可以在留言區(qū)說(shuō)一下
另外一個(gè)就是你的注解器的lib包需要使用annotationProcessor來(lái)使用,而不是implementation

文章到這就要到我們的實(shí)戰(zhàn)環(huán)節(jié)了,下一篇我將帶領(lǐng)大家仿butterknife簡(jiǎn)單實(shí)現(xiàn)一個(gè)findviewbyid的功能,幫助大家更好的運(yùn)用和消化這些知識(shí),加油。關(guān)注我不迷茫,支持我的就點(diǎn)個(gè)贊唄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容