編譯時註解愈來愈多的出如今各大開源框架使用中,好比javascript
JakeWharton/butterknife viewhtml
greenrobot/EventBus 事件java
square/dagger 依賴注入android
相似這樣的庫在開發和工做中已經愈來愈多,它們旨在幫助咱們在效率爲前提的狀況下幫助開發者快速開發,節約時間成本。而它們都使用了編譯時註解的思想。git
正由於如此火熱,因此有必要好好學習其中的實現原理,方便解決由於編譯時註解致使的問題,同時可將此技術運用到本身的開源庫中github
編譯時註解框架在編寫時有相對固定的格式,分包爲例api
格式相對固定,可是也能夠靈活變更,好比講api
和annotations
結合在一個moudel
裏app
moudel中
的依賴關係也很是的固定框架
processors
依賴包有api
-annotations
ide
app
依賴包有api
-annotations
-processors
其中除了app
是android moudel
之外,其餘所有均是java moudel
annotations
註解在講解annotations
註解以前,須要對java和android註解有大體的瞭解,能夠參考我以前的博客
先初始一個HelloWordAtion註解標註Target爲ElementType.TYPE
修飾類對象
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface HelloWordAtion {
String value();
}複製代碼
通常一個註解須要對應一個註解處理器,註解處理器在processors
處理
processors
註解處理器對應註解的處理器須要繼承AbstractProcessor
類,須要複寫如下4個方法:
init
init(ProcessingEnvironment processingEnv)
會被註解處理工具調用
process
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
這至關於每一個處理器的主函數main(),你在這裏寫你的掃描、評估和處理註解的代碼,以及生成Java文件。
getSupportedAnnotationTypes
etSupportedAnnotationTypes()
這裏必須指定,這個註解處理器是註冊給哪一個註解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的註解類型的合法全稱
@return
註解器所支持的註解類型集合,若是沒有這樣的類型,則返回一個空集合
getSupportedSourceVersion
指定使用的Java
版本,一般這裏返回SourceVersion.latestSupported(),默認返回
SourceVersion.RELEASE_6 `
@return
使用的Java
版本
對AbstractProcessor
有了深刻的瞭解,知道核心的初始編譯時編寫代碼的方法及時process
,在process
中咱們經過獲得傳遞過來的數據,寫入代碼,這裏先採用打印的方式,簡單輸出信息,後續會詳細講解如何本身實現 butterknife
功能
public class HelloWordProcessor extends AbstractProcessor {
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// Filer是個接口,支持經過註解處理器建立新文件
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
String clsNmae = typeElement.getSimpleName().toString();
String msg = typeElement.getAnnotation(HelloWordAtion.class).value();
System.out.println("clsName--->"+clsNmae+" msg->"+msg);
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(HelloWordAtion.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}複製代碼
到這一步HelloWordAtion
對應的註解處理器已經編寫完成,這裏簡單的打印了HelloWordAtion
註解的class
和註解指定的value
信息
準備工做完成之後,app
觸發調用
@HelloWordAtion("hello")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}複製代碼
這裏註解註釋的類MainActivity
而且指定value
爲hello
,到此準備工做就算完成了,這時若是你直接編譯或者運行工程的話,是看不到任何輸出信息的,這裏還要作的一步操做是指定註解處理器的所在,須要作以下操做:
一、在 processors 庫的 main 目錄下新建 resources 資源文件夾;
二、在 resources文件夾下創建 META-INF/services 目錄文件夾;
三、在 META-INF/services 目錄文件夾下建立 javax.annotation.process.Processors 文件;
四、在 javax.annotation.process.Processors 文件寫入註解處理器的全稱,包括包路徑;
經歷了以上步驟之後方可成功運行,可是實在是太複雜了,博主爲了配置這一步也是搞了很久,因此這裏推薦使用開源框架AutoService
AutoService
直接在Processors
中依賴
compile 'com.google.auto.service:auto-service:1.0-rc2'複製代碼
使用
@AutoService(Processor.class)
public class HelloWordProcessor extends AbstractProcessor {
xxxxxxx
}複製代碼
到這裏運行程序即可以成功看到後臺的輸出信息
須要切換到右下角的Gradle Console
窗口,若是變異不成功能夠clean
工程之後從新運行
獲得須要的數據,下一步固然是將數據寫入到java class
中,也就是題目所言的編譯時註解,如何才能寫入,這裏須要藉助Filer
類
Filer
在AbstractProcessor
的init
方法中初始Filer
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}複製代碼
到此咱們已經有了寫入的類的幫助類,還差代碼生成邏輯,這裏介紹使用javapoet
javapoet
JavaPoet一個是建立 .java 源文件的輔助庫,它能夠很方便地幫助咱們生成須要的.java 源文件,GitHub
上面有很是詳細的用法,建議好好閱讀相關的使用
processors
依賴:
compile 'com.squareup:javapoet:1.8.0'複製代碼
綜合上述的技術,仿照javapoet
的第一個Example
生成以下代碼
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
String clsNmae = typeElement.getSimpleName().toString();
String msg = typeElement.getAnnotation(HelloWordAtion.class).value();
System.out.println("clsName--->"+clsNmae+" msg->"+msg);
// 建立main方法
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, clsNmae+"-"+msg)
.build();
// 建立HelloWorld類
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
try {
// 生成 com.wzgiceman.viewinjector.HelloWorld.java
JavaFile javaFile = JavaFile.builder("com.wzgiceman.viewinjector", helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 生成文件
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}複製代碼
這裏重點講解process
方法,也就是寫入代碼的方法體,咱們在javapoet
的Example
基礎上將輸出信息改成HelloWordAtion
註解獲取的信息,處處便徹底搞定編譯時註解的整個流程,clean
之後運行工程,在以下路徑下即可看到自動編譯生成的HelloWorld
類
到此簡單的編譯時註解就搞定了,可是編譯時註解的自動寫入也會致使代碼混亂,可能在屢次build
編譯過程當中出現文件衝突的狀況,因此這裏須要引入android-apt
android-apt
android-apt
能在編譯時期去依賴註解處理器並進行工做,但在生成 APK 時不會包含任何遺留無用的文件,輔助 Android Studio
項目的對應目錄中存放註解處理器在編譯期間生成的文件
依賴使用:
根目錄build.gradle
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'複製代碼
app
中
apply plugin: 'com.neenbedankt.android-apt'
apt project(':processors')複製代碼
這裏是apt
替換compile
依賴processors
到此簡單的編譯時註解就搞定了,可是api
模塊尚未涉及,彆着急接下來的博客中繼續擴展,運用掌握的編譯時註解和時下主流的butterknife
框架,實現一套本身的自定義注入框架中會詳細講解api
模塊的使用,你會發現原來butterknife
很簡單,固然能夠自由發散,擴展回到本身的任何開源項目中,替換掉反射提升效率。火燒眉毛的小夥伴能夠去GitHub
下載源碼先自行研究。