Android(Java)代碼生成技術--JavaPoet初體驗之手動實現依賴注入

前言

相信大家在平常的開發中,依賴注入這個詞沒少聽說過吧,比如做安卓開發的,使用的Butterknife、Greendao等等第三方庫,都是使用的一種叫做編譯期代碼即時生成的技術,然後我們可以利用編譯生成的類來輔助我們的開發,減少我們的工作量,這個技術聽上去感覺挺高大上的,編譯期間代碼生成,這該怎麼做到啊,好像從來沒有從哪聽說編譯還能生成代碼的,下面讓我們來看看這門神奇的技術!

編譯期代碼生成原理

首先在這之前,我們可能或多或少瞭解到一個叫JavaPoet的技術,首先我們從它開始,我們來到它的源碼,你會發現它好像並沒有做什麼高深的事情,總共才十幾個類,而且大多數只是做了一些類的封裝,提供一些接口方便使用,然後還提供了一個工具類供我們使用,如圖,它的源碼僅僅只有下面幾個類而已
這裏寫圖片描述
這時候就迷了,難道編譯期生成代碼不是這個庫幫我們完成的嗎?
準確的說確實不是的,那它的功能是什麼呢?它其實只是完成了我們所說的一半的功能,就是代碼生成,而且是簡單易懂的代碼生成,因爲它做了封裝,對一些常用的代碼生成需求基本都提供了相應的方法,大大減少了代碼複雜度。那和我們想要的需求來比,少了一個編譯期間生成,那這個怎麼做呢?

其實要實現編譯期間做一些事情,這個工作Java已經幫我們做好了,我們先來看一個類AbstractProcessor,這個類可能平常不會用到,但是也沒關係,我們簡單瞭解一下它,首先你可以把它理解爲一個抽象的註解處理器,它位於javax.annotation.processing.AbstractProcessor;下面,是用於在編譯時掃描和處理註解的類,我們要實現類似ButterKnife這樣的功能,首先定義一個自己的註解處理器,然後繼承它即可,如下

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

可以看到我們需要實現一個叫process的抽象方法,這個方法裏的內容就會在編譯期間執行。
然後,怎麼讓jvm在編譯期間調用我們自己寫的這個註解處理器呢,有一個快捷辦法就是使用谷歌的開源庫auto,然後使用它提供的AutoService註解來實現,另外一種辦法就是自己手動去創建指定的文件夾,然後配置我們的註解處理器的路徑。

做完上述工作後,在編譯時,jvm就會掃描到所有的AbstractProcessor 的實現類,這裏也就是MyProcessor ,然後調用實現實現類的process方法,執行相應的操作,然後我們生成代碼的工作就可以寫在process這裏,然後具體的生成代碼的方法再借助JavaPoet工具來簡化操作。
因爲我們生成代碼的工具是在編譯期間執行的,最後生成的java代碼會和普通的java代碼一起編譯,當做普通的類來使用,所以不會影響到最後程序運行時的性能,最多隻是編譯速度慢了點,因爲要生成額外的類和代碼。

至此我們知道了怎麼在編譯期間生成代碼,基本的實現思路也有了,不過還有一個問題存在。

怎麼使用註解處理器來處理一個註解,從而實現類似@BindView的效果呢?首先我們當然要自定義一個註解,具體自定義註解不瞭解的可以去學下,挺簡單的,內容不多,由於不是本章重點,所以不作過多說明,當然也可以就在文末的demo鏈接裏下載源碼學習,在定義了一個註解之後,我們先看到我們的MyProcessor 註解處理器的 process() 方法的annotations 參數,這個參數就是在編譯的時候,用來存儲掃描到的所有的非元註解(非元註解也就是自定義註解,元註解一共四個,除了元註解,剩下的就是自定義註解),然後我們遍歷這個集合,取到我們自定義註解時,再執行相應的邏輯。具體的代碼如下,其中XXXAnnotation就是你的自定義註解的名稱

for (TypeElement element : annotations) {
    if (element.getQualifiedName().toString().equals(XXXAnnotation.class.getCanonicalName())) {
        //執行你的邏輯
    }
}

然後我們還需要重寫AbstractProcessor 類的getSupportedAnnotationTypes() 方法和getSupportedSourceVersion() 方法,getSupportedAnnotationTypes() 方法用來指定該註解處理器是用來處理哪個註解的,getSupportedSourceVersion() 方法用來指定java版本,一般給值爲SourceVersion.latestSupported()

完成以上工作後,對於自定義註解作用的對象,編譯期間就會自動執行相應process() 裏的邏輯,比如生成輔助類,這個輔助類其實就可以理解爲依賴類,這個編譯期間通過註解生成輔助類的過程,就相當於實現了注入。
到這裏,一整個依賴注入的思路和實現方法已經全部打通。
下面我們來動手實現一個小例子吧!

動手實現

首先我們新建一個Android工程,按照默認的配置就好,新建完畢後,會有個默認的app的module,我們暫且不管它,然後直接新建一個java module,新建方式爲file -> New -> New Module
然後選擇Java Libary,這裏給libary取名爲javapoet
這裏寫圖片描述
然後在module對應的build.gradle下,加入下面兩個依賴

implementation 'com.squareup:javapoet:1.11.1'
implementation 'com.google.auto.service:auto-service:1.0-rc4'

第一個依賴是javaPoet的依賴,第二個是Google開源庫auto的依賴
上面提到讓jvm加載我們自己的註解處理器有兩種方式,這裏我們先試一下用谷歌的這個開源庫

這個module用來編寫我們的註解處理器的實現類,先放着,待會再寫。
繼續新建一個java module,我這裏取名爲libannotation,然後在裏面新建一個自定義註解,內容如下

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface HelloAnnotation {
}

@Retention(RetentionPolicy.CLASS)表示我們定義的這個註解會留存在class字節碼文件中 ,但是在運行時是沒有的,@Target(ElementType.TYPE) 表示我們的這個註解作用對象是類,然後我們看下整個目錄的樣子,
這裏寫圖片描述

TestGenerator是一個我寫的測試JavaPoet的測試類,可以忽略

現在我們開始寫代碼,首先在javapoet的build.gradle中加入libannotation module的依賴,如下

implementation project(path: ':libannotation')

然後在javapoet module中新建HelloProcessor類,用AutoService標註它,然後繼承AbstractProcessor方法,重寫相應的方法,具體怎麼寫,原理裏解釋的比較詳細,然後註釋裏我作了詳細說明,如下

import com.aiiage.libannotation.HelloAnnotation;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.Collections;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;

/**
 * Created By HuangQing on 2018/7/20 15:38
 **/
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler(); // 獲得filer對象,用來創建文件
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement element : annotations) {//遍歷掃描到的註解集合
            if (element.getQualifiedName().toString().equals(HelloAnnotation.class.getCanonicalName())) {
                // 當前註解是我們自定義的註解,也就是HelloAnnotation時,執行下列代碼
                TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")//聲明類名爲HelloWorld
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)//聲明類的修飾符爲 public final
                        .addMethod(getMethodSpec("hello1", "Hello"))//爲HelloWorld類添加名爲hello1的方法,返回值爲Hello
                        .addMethod(getMethodSpec("hello2", "Java"))//同上
                        .addMethod(getMethodSpec("hello3", "Poet!"))//同上
                        .build();

                try {
                    // 建立 com.aiiage.testjavapoet.HelloWorld.java 對象
                    JavaFile javaFile = JavaFile.builder("com.aiiage.testjavapoet", helloWorld)
                            .addFileComment(" This codes are generated automatically. Do not modify!")
                            .build();
                    // 寫入文件
                    javaFile.writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return true;
    }


    /**
     * @param methodStr  方法名
     * @param returnStr  返回值
     * @return
     */
    private static MethodSpec getMethodSpec(String methodStr, String returnStr) {
        return MethodSpec.methodBuilder(methodStr)
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//指定方法修飾符爲 public static
                .returns(String.class) //指定返回值爲String類型
                .addStatement("return $S", returnStr) //拼接返回值語句
                .build();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(HelloAnnotation.class.getCanonicalName());
    }

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

上述代碼中,一定不要忘了加AutoService的註解,通過這個註解,我們的HelloProcessor註解處理器相當於執行了一個註冊的過程,這樣纔會被jvm在編譯時加載,代碼生成部分最終生成的文件代碼長下面這個樣子,可以結合註釋對着體會下,還是很方便的,有了javapoet之後

import java.lang.String;

public final class HelloWorld {
  public static String hello1() {
    return "Hello";
  }

  public static String hello2() {
    return "Java";
  }

  public static String hello3() {
    return "Poet!";
  }
}

ok,代碼方面準備完畢。
接下來我們爲app module添加依賴,如下

//必須使用annotationProcessor,而不是implementation
//annotationProcessor修飾的,最終不會打包到apk中,可理解爲在編譯時執行的
annotationProcessor project(':javapoet')
implementation project(':libannotation')

然後我們手動編譯一下項目,build -> make project,然後我們來到熟悉的MainActivity,首先使用我們的自定義註解標註MainActivity,要標在class上面,因爲我們自定義的註解作用範圍是Type,然後用一個TextView簡單顯示一下注入的HelloWorld類的幾個靜態方法,如下

@HelloAnnotation
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv=findViewById(R.id.tv);
        String str1="";
        str1+=HelloWorld.hello1()+" ";
        str1+=HelloWorld.hello2()+" ";
        str1+=HelloWorld.hello3();
        tv.setText(str1);
    }
}

編譯運行,看到Hello Java Poet字樣即表示成功!

然後我們上面提到,除了使用Google開源庫auto實現註冊註解處理器外,還可以使用手動配置的方式,手動配置的方式如下

在自定義註解處理器的module中,這裏也就是javapoet的module中,在main目錄下創建出resources目錄,然後在resources目錄下創建出META-INF目錄,然後在META-INF目錄下創建出services目錄,然後在services目錄下創建一個名爲javax.annotation.processing.Processor 的文件,在該文件中聲明我們的註解處理器:

com.aiiage.javapoet.HelloProcessor;

結語

這樣我們就完成了一個最簡單的依賴注入的實現,掌握其原理後,我們就不難明白類似ButterKnife、GreenDao是怎麼實現的了,不過我們這個只是一個簡單的功能,不得不感嘆要生成那麼多的代碼,這得是一個多麼心細和考驗耐心的活啊!

源碼下載

源碼下載

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