前言
相信大家在平常的開發中,依賴注入這個詞沒少聽說過吧,比如做安卓開發的,使用的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是怎麼實現的了,不過我們這個只是一個簡單的功能,不得不感嘆要生成那麼多的代碼,這得是一個多麼心細和考驗耐心的活啊!