27.APT技術與JavaPoet

上一篇文章完成了組件化工程的搭建(https://www.jianshu.com/p/ed2c9f677e0e),這篇文章來說下APT和JavaPoet
下一篇我們會說下ARouter是如何實現頁面跳轉的(https://www.jianshu.com/p/0fa14358a765

APT

APT是英文Annotation Processing Tool的簡寫,是一種在編譯期間通過對註解的處理,來實現自動生成輔助代碼的技術。生成代碼的方式有直接拼接字符串這種比較原始的方式,還有一種面向對象編程的方式-JavaPoet。使用APT技術的框架,通常都是屬於編譯期框架,如組件化框架ARouter,通信框架EventBus,以及使用非常頻繁的ButterKnife,他們都會使用到APT並藉助它來實現部分代碼的生成,就等於藉助編譯器幫我們寫代碼一樣。

JavaPoet

JavaPoet (https://github.com/square/javapoet)簡單理解就是一個用來生成 .java源文件的Java API。我們通常寫代碼都是自己創建一個類,然後自己編寫代碼的實現,但是JavaPoet卻是一個可以幫我們寫java文件的庫,由square開源。早期的APT技術生成輔助類的方式可以參考下EventBus中的實現,是通過拼接大量的字符串來生成一個文件,非常的繁瑣,並且容易出錯,JavaPoet的出現解決了這個問題,讓我們以一種面向對象的方式生成java文件。實現方式可以參考ARouter源碼。

如何使用

那麼在Android開發中如何去使用APT技術,並結合JavaPoet去生成輔助類呢,其實很簡單,今天的主要內容就是通過JavaPoet生成一個HelloWorld.java類

1.創建工程

我們創建一個Android項目,然後創建一個java library, apt技術需要在java library中創建相關類。然後讓app模塊依賴這個library,暫且命名爲hello-world-compiler

然後引入需要的一些依賴,在java library模塊的build.gradle文件的根目錄配置如下

dependencies {
    //auto-service是Google開源的一個庫,可以方便快捷的幫助我們進行組件化開發
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
    //引入javapoet
    implementation "com.squareup:javapoet:1.9.0"
}

既然是APT技術,那麼必然少不了註解的,我們仿照開源框架的方式,再創建一個java library,命名爲hello-world-annotion,然後在裏邊創建一個註解,就叫HelloWorld,然後在上邊的hello-world-compiler中依賴它,這樣,我們的註解處理器就可以找到這個註解了

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE})
public @interface HelloWorld {
    //這裏也可以設置一些東西
}
dependencies {
    //auto-service是Google開源的一個庫,可以方便快捷的幫助我們進行組件化開發
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
    //引入javapoet
    implementation "com.squareup:javapoet:1.9.0"
    implementation project(":hello-world-annotion")
}

那麼當前三個module的依賴關係如下:
app

implementation project(":hello-world-annotation")
annotationProcessor project(":hello-world-compiler")

hello-world-compiler:

implementation project(":hello-world-annotation")

2.編寫註解處理器

現在開始寫註解處理器,創建一個類MyCompiler,MyCompiler繼承AbstractProcessor,實現它的process方法

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

我們上邊知道,app以annotationProcessor的方式依賴了hello-world-compiler,所以當app模塊編譯的時候,process方法會被執行。或者如果有很多的module,每個module都依賴了註解處理器hello-world-compiler,那麼每個module編譯的時候,都會單獨走一次process方法

除了這個必須實現的process方法之外,我們再重寫AbstractProcessor的幾個方法,他們的含義見註釋,如下

@AutoService(Processor.class)
public class HelloWorldProcessor extends AbstractProcessor {
    // type(類信息)的工具類,包含用於操作TypeMirror的工具方法
    private Types typeUtils;
    // Message用來打印 日誌相關信息
    private Messager messager;
    // 文件生成器, 類 資源 等,就是最終要生成的文件 是需要Filer來完成的
    private Filer filer;
    // 操作Element的工具類(類,函數,屬性,其實都是Element)
    private Elements elementUtils;
    //從每個module接收到的數據
    private String moduleName;

    //此方法用於指定可以從module中接收的數據
    @Override
    public Set<String> getSupportedOptions() {
        return super.getSupportedOptions();
    }
    //此方法指明註解處理器支持的java sdk版本,建議和項目保持一致,否則編譯時會有提示
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }
    //指明註解處理器可以處理哪些註解,如我們本次只有HelloWorld這個註解、
    //所以把它添加到set中並返回,表示我們只處理HelloWorld
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new HashSet<>();
        set.add(HelloWorld.class.getName());
        return set;
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        typeUtils = processingEnvironment.getTypeUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        elementUtils = processingEnvironment.getElementUtils();
        //如我們從app的build.gradle中配置了一個key爲moduleName的值
        //在這裏可以接收到
        moduleName = processingEnvironment.getOptions().get(“moduleName”);
    }
    ....
}

向註解處理器傳遞參數的配置方式

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                // project.getName() == app
                arguments = [moduleName: project.getName()]
            }
        }
    }
    ...
}

3.JavaPoet api

我們知道JavaPoet是用來寫java文件的,一個java文件通常對應一個java類,那麼java類中的元素在JavaPoet中都有所對應
方法 ---- MethodSpec
class ---- TypeSpec
java文件 ---- JavaFile

那麼我們最終要生成一個這樣的文件,代碼該怎麼寫?

package com.example.helloworld;

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

直接貼上process方法的實現

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) {
            messager.printMessage(Diagnostic.Kind.NOTE, "並沒有發現 被@HelloWorld註解的地方呀");
            return false; // 沒有機會處理
        }

        // 獲取所有被 @ HelloWorld 註解的 元素集合
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(HelloWorld.class);
        // 遍歷所有的類節點
        for (Element element : elements) {
            // 獲取簡單類名,例如:MainActivity
            String className = element.getSimpleName().toString();
            messager.printMessage(Diagnostic.Kind.NOTE, "被@ HelloWorld註解的類有:" + className); // 打印出 就證明APT沒有問題
            // 1.方法
            MethodSpec mainMethod = 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();
            // 2.類
            TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(mainMethod)
                    .build();
            // 3.包
            JavaFile packagef = JavaFile.builder("com.renzhenming", helloWorld).build();
            // 去生成
            try {
                packagef.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
                messager.printMessage(Diagnostic.Kind.NOTE, "生成失敗,請檢查代碼...");
            }  
        }
        return true; 表示處理註解完成
    }

這樣一來,在app模塊中使用HelloWorld註解某一個類,編譯後就會在代碼中生成一個HelloWorld的類了

參考代碼:https://github.com/renzhenming/Modularization

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