新四季網

android系統開發的基礎知識(Android基礎系列篇一)

2023-07-27 00:50:37 1

前言

本系列文章主要是匯總了一下大佬們的技術文章,屬於Android基礎部分,作為一名合格的安卓開發工程師,咱們肯定要熟練掌握java和android,本期就來說說這些~

[非商業用途,如有侵權,請告知我,我會刪除]

DD一下: 開發文檔跟之前仍舊一樣,需要的跟作者直接要。

註解

Annotation 中文譯過來就是註解、標釋的意思,在 Java 中註解是一個很重要的知識點,但經常還是有點讓新手不容易理解。

我個人認為,比較糟糕的技術文檔主要特徵之一就是:用專業名詞來介紹專業名詞。 比如:

Java 註解用於為 Java 代碼提供元數據。作為元數據,註解不直接影響你的代碼執行,但也有一些類型的註解實際上可以用於這一目的。Java 註解是從 Java5 開始添加到 Java 的。 這是大多數網站上對於 Java 註解,解釋確實正確,但是說實在話,我第一次學習的時候,頭腦一片空白。這什麼跟什麼啊?聽了像沒有聽一樣。因為概念太過於抽象,所以初學者實在是比較吃力才能夠理解,然後隨著自己開發過程中不斷地強化練習,才會慢慢對它形成正確的認識。

我在寫這篇文章的時候,我就在思考。如何讓自己或者讓讀者能夠比較直觀地認識註解這個概念?是要去官方文檔上翻譯說明嗎?我馬上否定了這個答案。

後來,我想到了一樣東西————墨水,墨水可以揮發、可以有不同的顏色,用來解釋註解正好。

不過,我繼續發散思維後,想到了一樣東西能夠更好地代替墨水,那就是印章。印章可以沾上不同的墨水或者印泥,可以定製印章的文字或者圖案,如果願意它也可以被戳到你任何想戳的物體表面。

但是,我再繼續發散思維後,又想到一樣東西能夠更好地代替印章,那就是標籤。標籤是一張便利紙,標籤上的內容可以自由定義。常見的如貨架上的商品價格標籤、圖書館中的書本編碼標籤、實驗室中化學材料的名稱類別標籤等等。

並且,往抽象地說,標籤並不一定是一張紙,它可以是對人和事物的屬性評價。也就是說,標籤具備對於抽象事物的解釋。

所以,基於如此,我完成了自我的知識認知升級,我決定用標籤來解釋註解。

1.註解的定義

註解通過 @interface 關鍵字進行定義。

public @interface TestAnnotation {}

它的形式跟接口很類似,不過前面多了一個 @ 符號。上面的代碼就創建了一個名字為 TestAnnotaion 的註解。

你可以簡單理解為創建了一張名字為 TestAnnotation 的標籤。

1.1註解的屬性

註解的屬性也叫做成員變量。註解只有成員變量,沒有方法。註解的成員變量在註解的定義中以「無形參的方法」形式來聲明,其方法名定義了該成員變量的名字,其返回值定義了該成員變量的類型。

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface TestAnnotation { int id; String msg;}

上面代碼定義了 TestAnnotation 這個註解中擁有 id 和 msg 兩個屬性。在使用的時候,我們應該給它們進行賦值。

賦值的方式是在註解的括號內以 value=」」 形式,多個屬性之前用 ,隔開。

@TestAnnotation(id=3,msg="hello annotation")public class Test {}

需要注意的是,在註解中定義屬性時它的類型必須是 8 種基本數據類型外加 類、接口、註解及它們的數組。

註解中屬性可以有默認值,默認值需要用 default 關鍵值指定。比如:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface TestAnnotation { public int id default -1; public String msg default "Hi";}

TestAnnotation 中 id 屬性默認值為 -1,msg 屬性默認值為 Hi。 它可以這樣應用。

@TestAnnotationpublic class Test {}

因為有默認值,所以無需要再在 @TestAnnotation 後面的括號裡面進行賦值了,這一步可以省略。

另外,還有一種情況。如果一個註解內僅僅只有一個名字為 value 的屬性時,應用這個註解時可以直接接屬性值填寫到括號內。

public @interface Check { String value;}

上面代碼中,Check 這個註解只有 value 這個屬性。所以可以這樣應用。

@Check("hi")int a;

這和下面的效果是一樣的

@Check(value="hi")int a;

最後,還需要注意的一種情況是一個註解沒有任何屬性。比如

public @interface Perform {}

那麼在應用這個註解的時候,括號都可以省略。

@Performpublic void testMethod{}

2.自定義註解2.1註解如同標籤

之前某新聞客戶端的評論有蓋樓的習慣,於是 「賈伯斯重新定義了手機、羅永浩重新定義了傻 X」 就經常極為工整地出現在了評論樓層中,並且廣大網友在相當長的一段時間內對於這種行為樂此不疲。這其實就是等同於貼標籤的行為。 在某些網友眼中,羅永浩就成了傻 X的代名詞。

廣大網友給羅永浩貼了一個名為「傻x」的標籤,他們並不真正了解羅永浩,不知道他當教師、砸冰箱、辦博客的壯舉,但是因為「傻 x」這樣的標籤存在,這有助於他們直接快速地對羅永浩這個人做出評價,然後基於此,羅永浩就可以成為茶餘飯後的談資,這就是標籤的力量。

而在網絡的另一邊,老羅靠他的人格魅力自然收穫一大批忠實的擁泵,他們對於老羅貼的又是另一種標籤。

老羅還是老羅,但是由於人們對於它貼上的標籤不同,所以造成對於他的看法大相逕庭,不喜歡他的人整天在網絡上評論抨擊嘲諷,而崇拜欣賞他的人則會願意掙錢購買錘子手機的發布會門票。

我無意於評價這兩種行為,我再引個例子。

《奇葩說》是近年網絡上非常火熱的辯論節目,其中辯手陳銘被另外一個辯手馬薇薇攻擊說是————「站在宇宙中心呼喚愛」,然後貼上了一個大大的標籤————「雞湯男」,自此以後,觀眾再看到陳銘的時候,首先映入腦海中便是「雞湯男」三個大字,其實本身而言陳銘非常優秀,為人師表、作風正派、談吐舉止得體,但是在網絡中,因為娛樂至上的環境所致,人們更願意以娛樂的心態來認知一切,於是「雞湯男」就如陳銘自己所說成了一個撕不了的標籤。

我們可以抽象概括一下,標籤是對事物行為的某些角度的評價與解釋。

到這裡,終於可以引出本文的主角註解了。

初學者可以這樣理解註解:想像代碼具有生命,註解就是對於代碼中某些鮮活個體的貼上去的一張標籤。簡化來講,註解如同一張標籤。

在未開始學習任何註解具體語法而言,你可以把註解看成一張標籤。這有助於你快速地理解它的大致作用。如果初學者在學習過程有大腦放空的時候,請不要慌張,對自己說:

註解,標籤。註解,標籤。

2.2註解語法

因為平常開發少見,相信有不少的人員會認為註解的地位不高。其實同 classs 和 interface 一樣,註解也屬於一種類型。它是在 Java SE 5.0 版本中開始引入的概念。

2.3元註解

元註解是什麼意思呢?

元註解是可以註解到註解上的註解,或者說元註解是一種基本註解,但是它能夠應用到其它的註解上面。

如果難於理解的話,你可以這樣理解。元註解也是一張標籤,但是它是一張特殊的標籤,它的作用和目的就是給其他普通的標籤進行解釋說明的。

元標籤有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 種。

@Retention

Retention 的英文意為保留期的意思。當 @Retention 應用到一個註解上的時候,它解釋說明了這個註解的的存活時間。

它的取值如下:

RetentionPolicy.SOURCE 註解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被加載到 JVM 中。RetentionPolicy.RUNTIME 註解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。

我們可以這樣的方式來加深理解,@Retention 去給一張標籤解釋的時候,它指定了這張標籤張貼的時間。@Retention 相當於給一張標籤上面蓋了一張時間戳,時間戳指明了標籤張貼的時間周期。

@Retention(RetentionPolicy.RUNTIME)public @interface TestAnnotation {}

上面的代碼中,我們指定 TestAnnotation 可以在程序運行周期被獲取到,因此它的生命周期非常的長。

@Documented

顧名思義,這個元註解肯定是和文檔有關。它的作用是能夠將註解中的元素包含到 Javadoc 中去。

@Target

Target 是目標的意思,@Target 指定了註解運用的地方。

你可以這樣理解,當一個註解被 @Target 註解時,這個註解就被限定了運用的場景。

類比到標籤,原本標籤是你想張貼到哪個地方就到哪個地方,但是因為 @Target 的存在,它張貼的地方就非常具體了,比如只能張貼到方法上、類上、方法參數上等等。@Target 有下面的取值

ElementType.ANNOTATION_TYPE 可以給一個註解進行註解ElementType.CONSTRUCTOR 可以給構造方法進行註解ElementType.FIELD 可以給屬性進行註解ElementType.LOCAL_VARIABLE 可以給局部變量進行註解ElementType.METHOD 可以給方法進行註解ElementType.PACKAGE 可以給一個包進行註解ElementType.PARAMETER 可以給一個方法內的參數進行註解ElementType.TYPE 可以給一個類型進行註解,比如類、接口、枚舉@Inherited

Inherited 是繼承的意思,但是它並不是說註解本身可以繼承,而是說如果一個超類被 @Inherited 註解過的註解進行註解的話,那麼如果它的子類沒有被任何註解應用的話,那麼這個子類就繼承了超類的註解。 說的比較抽象。代碼來解釋。

@Inherited@Retention(RetentionPolicy.RUNTIME)@interface Test {}@Testpublic class A {}public class B extends A {}

註解 Test 被 @Inherited 修飾,之後類 A 被 Test 註解,類 B 繼承 A,類 B 也擁有 Test 這個註解。

可以這樣理解:

老子非常有錢,所以人們給他貼了一張標籤叫做富豪。

老子的兒子長大後,只要沒有和老子斷絕父子關係,雖然別人沒有給他貼標籤,但是他自然也是富豪。

老子的孫子長大了,自然也是富豪。

這就是人們口中戲稱的富一代,富二代,富三代。雖然叫法不同,好像好多個標籤,但其實事情的本質也就是他們有一張共同的標籤,也就是老子身上的那張富豪的標籤。

@Repeatable

Repeatable 自然是可重複的意思。@Repeatable 是 Java 1.8 才加進來的,所以算是一個新的特性。

什麼樣的註解會多次應用呢?通常是註解的值可以同時取多個。

舉個例子,一個人他既是程式設計師又是產品經理,同時他還是個畫家。

@interface Persons { Person[] value;}@Repeatable(Persons.class)@interface Person{ String role default "";}@Person(role="artist")@Person(role="coder")@Person(role="PM")public class SuperMan{}

注意上面的代碼,@Repeatable 註解了 Person。而 @Repeatable 後面括號中的類相當於一個容器註解。

什麼是容器註解呢?就是用來存放其它註解的地方。它本身也是一個註解。

我們再看看代碼中的相關容器註解。

@interface Persons { Person[] value;}

按照規定,它裡面必須要有一個 value 的屬性,屬性類型是一個被 @Repeatable 註解過的註解數組,注意它是數組。

如果不好理解的話,可以這樣理解。Persons 是一張總的標籤,上面貼滿了 Person 這種同類型但內容不一樣的標籤。把 Persons 給一個 SuperMan 貼上,相當於同時給他貼了程式設計師、產品經理、畫家的標籤。

我們可能對於 @Person(role=」PM」) 括號裡面的內容感興趣,它其實就是給 Person 這個註解的 role 屬性賦值為 PM ,大家不明白正常,馬上就講到註解的屬性這一塊。

2.4Java 預置的註解

Java 語言本身已經提供了幾個現成的註解。

@Deprecated

這個元素是用來標記過時的元素,想必大家在日常開發中經常碰到。編譯器在編譯階段遇到這個註解時會發出提醒警告,告訴開發者正在調用一個過時的元素比如過時的方法、過時的類、過時的成員變量。

public class Hero { @Deprecated public void say{ System.out.println("Noting has to say!"); } public void speak{ System.out.println("I have a dream!"); }}

定義了一個 Hero 類,它有兩個方法 say 和 speak ,其中 say 被 @Deprecated 註解。然後我們在 IDE 中分別調用它們。

可以看到,say 方法上面被一條直線劃了一條,這其實就是編譯器識別後的提醒效果。

@Override

這個大家應該很熟悉了,提示子類要複寫父類中被 @Override 修飾的方法

@SuppressWarnings

阻止警告的意思。之前說過調用被 @Deprecated 註解的方法後,編譯器會警告提醒,而有時候開發者會忽略這種警告,他們可以在調用的地方通過 @SuppressWarnings 達到目的。

@SuppressWarnings("deprecation")public void test1{ Hero hero = new Hero; hero.say; hero.speak;}

@SafeVarargs

參數安全類型註解。它的目的是提醒開發者不要用參數做一些不安全的操作,它的存在會阻止編譯器產生 unchecked 這樣的警告。它是在 Java 1.7 的版本中加入的。

@SafeVarargs // Not actually safe! static void m(List... stringLists) { Object[] array = stringLists; List tmpList = Arrays.asList(42); array[0] = tmpList; // Semantically invalid, but compiles without warnings String s = stringLists[0].get(0); // Oh no, ClassCastException at runtime!}

上面的代碼中,編譯階段不會報錯,但是運行時會拋出 ClassCastException 這個異常,所以它雖然告訴開發者要妥善處理,但是開發者自己還是搞砸了。

Java 官方文檔說,未來的版本會授權編譯器對這種不安全的操作產生錯誤警告。

@FunctionalInterface

函數式接口註解,這個是 Java 1.8 版本引入的新特性。函數式編程很火,所以 Java 8 也及時添加了這個特性。

函數式接口 (Functional Interface) 就是一個具有一個方法的普通接口。 比如

@FunctionalInterfacepublic interface Runnable { /** * When an object implementing interface Runnable is used * to create a thread, starting the thread causes the object's * run method to be called in that separately executing * thread. * * The general contract of the method run is that it may * take any action whatsoever. * * @see java.lang.Thread#run */ public abstract void run;}

我們進行線程開發中常用的 Runnable 就是一個典型的函數式接口,上面源碼可以看到它就被 @FunctionalInterface 註解。

可能有人會疑惑,函數式接口標記有什麼用,這個原因是函數式接口可以很容易轉換為 Lambda 表達式。這是另外的主題了,有興趣的同學請自己搜索相關知識點學習。

3.註解的使用3.1註解的應用

上面創建了一個註解,那麼註解的的使用方法是什麼呢。

@TestAnnotationpublic class Test {}

創建一個類 Test,然後在類定義的地方加上 @TestAnnotation 就可以用 TestAnnotation 註解這個類了。

你可以簡單理解為將 TestAnnotation 這張標籤貼到 Test 這個類上面。

3.2註解的提取

前面的部分講了註解的基本語法,現在是時候檢測我們所學的內容了。

我通過用標籤來比作註解,前面的內容是講怎麼寫註解,然後貼到哪個地方去,而現在我們要做的工作就是檢閱這些標籤內容。 形象的比喻就是你把這些註解標籤在合適的時候撕下來,然後檢閱上面的內容信息。

要想正確檢閱註解,離不開一個手段,那就是反射。

3.3註解與反射

註解通過反射獲取。首先可以通過 Class 對象的 isAnnotationPresent 方法判斷它是否應用了某個註解

public boolean isAnnotationPresent(Class annotationClass) {}

然後通過 getAnnotation 方法來獲取 Annotation 對象。

public A getAnnotation(Class annotationClass) {}

或者是 getAnnotations 方法。

public Annotation[] getAnnotations {}

前一種方法返回指定類型的註解,後一種方法返回註解到這個元素上的所有註解。

如果獲取到的 Annotation 如果不為 null,則就可以調用它們的屬性方法了。比如

@TestAnnotationpublic class Test { public static void main(String[] args) { boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class); if ( hasAnnotation ) { TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class); System.out.println("id:" testAnnotation.id); System.out.println("msg:" testAnnotation.msg); } }}

程序的運行結果是:

id:-1msg:

這個正是 TestAnnotation 中 id 和 msg 的默認值。

上面的例子中,只是檢閱出了註解在類上的註解,其實屬性、方法上的註解照樣是可以的。同樣還是要假手於反射。

@TestAnnotation(msg="hello")public class Test { ) int a; @Perform public void testMethod{} @SuppressWarnings("deprecation") public void test1{ Hero hero = new Hero; hero.say; hero.speak; } public static void main(String[] args) { boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class); if ( hasAnnotation ) { TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class); //獲取類的註解 System.out.println("id:" testAnnotation.id); System.out.println("msg:" testAnnotation.msg); } try { Field a = Test.class.getDeclaredField("a"); a.setAccessible(true); //獲取一個成員變量上的註解 Check check = a.getAnnotation(Check.class); if ( check != null ) { System.out.println("check value:" check.value); } Method testMethod = Test.class.getDeclaredMethod("testMethod"); if ( testMethod != null ) { // 獲取方法中的註解 Annotation[] ans = testMethod.getAnnotations; for( int i = 0;i < ans.length;i ) { System.out.println("method testMethod annotation:" ans[i].annotationType.getSimpleName); } } } catch (NoSuchFieldException e) { // TODO Auto-generated catch block e.printStackTrace; System.out.println(e.getMessage); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace; System.out.println(e.getMessage); } catch (NoSuchMethodException e) { // TODO Auto-generated catch block e.printStackTrace; System.out.println(e.getMessage); } }}

它們的結果如下:

id:-1msg:hellocheck value:himethod testMethod annotation:Perform

需要注意的是,如果一個註解要在運行時被成功提取,那麼 @Retention(RetentionPolicy.RUNTIME) 是必須的。

3.4註解的使用場景

我相信博文講到這裡大家都很熟悉了註解,但是有不少同學肯定會問,註解到底有什麼用呢?

對啊註解到底有什麼用?

我們不妨將目光放到 Java 官方文檔上來。

文章開始的時候,我用標籤來類比註解。但標籤比喻只是我的手段,而不是目的。為的是讓大家在初次學習註解時能夠不被那些抽象的新概念搞懵。既然現在,我們已經對註解有所了解,我們不妨再仔細閱讀官方最嚴謹的文檔。

註解是一系列元數據,它提供數據用來解釋程序代碼,但是註解並非是所解釋的代碼本身的一部分。註解對於代碼的運行效果沒有直接影響。 註解有許多用處,主要如下:

提供信息給編譯器: 編譯器可以利用註解來探測錯誤和警告信息編譯階段時的處理: 軟體工具可以用來利用註解信息來生成代碼、Html文檔或者做其它相應處理。運行時的處理: 某些註解可以在程序運行的時候接受代碼的提取 值得注意的是,註解不是代碼本身的一部分。

如果難於理解,可以這樣看。羅永浩還是羅永浩,不會因為某些人對於他「傻x」的評價而改變,標籤只是某些人對於其他事物的評價,但是標籤不會改變事物本身,標籤只是特定人群的手段。所以,註解同樣無法改變代碼本身,註解只是某些工具的的工具。

還是回到官方文檔的解釋上,註解主要針對的是編譯器和其它工具軟體(SoftWare tool)。

當開發者使用了Annotation 修飾了類、方法、Field 等成員之後,這些 Annotation 不會自己生效,必須由開發者提供相應的代碼來提取並處理 Annotation 信息。這些處理提取和處理 Annotation 的代碼統稱為 APT(Annotation Processing Tool)。

現在,我們可以給自己答案了,註解有什麼用?給誰用?給 編譯器或者 APT 用的。

如果,你還是沒有搞清楚的話,我親自寫一個好了。

3.5親手自定義註解完成項目

我要寫一個測試框架,測試程式設計師的代碼有無明顯的異常。

—— 程式設計師 A : 我寫了一個類,它的名字叫做 NoBug,因為它所有的方法都沒有錯誤。 —— 我:自信是好事,不過為了防止意外,讓我測試一下如何? —— 程式設計師 A: 怎麼測試? —— 我:把你寫的代碼的方法都加上 @Jiecha 這個註解就好了。 —— 程式設計師 A: 好的。

package ceshi;import ceshi.Jiecha;public class NoBug { @Jiecha public void suanShu{ System.out.println("1234567890"); } @Jiecha public void jiafa{ System.out.println("1 1=" 1 1); } @Jiecha public void jiefa{ System.out.println("1-1=" (1-1)); } @Jiecha public void chengfa{ System.out.println("3 x 5=" 3*5); } @Jiecha public void chufa{ System.out.println("6 / 0=" 6 / 0); } public void ziwojieshao{ System.out.println("我寫的程序沒有 bug!"); }}

上面的代碼,有些方法上面運用了 @Jiecha 註解。

這個註解是我寫的測試軟體框架中定義的註解。

package ceshi;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;@Retention(RetentionPolicy.RUNTIME)public @interface Jiecha {}

然後,我再編寫一個測試類 TestTool 就可以測試 NoBug 相應的方法了。

package ceshi;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class TestTool { public static void main(String[] args) { // TODO Auto-generated method stub NoBug testobj = new NoBug; Class clazz = testobj.getClass; Method[] method = clazz.getDeclaredMethods; //用來記錄測試產生的 log 信息 StringBuilder log = new StringBuilder; // 記錄異常的次數 int errornum = 0; for ( Method m: method ) { // 只有被 @Jiecha 標註過的方法才進行測試 if ( m.isAnnotationPresent( Jiecha.class )) { try { m.setAccessible(true); m.invoke(testobj, null); } catch (Exception e) { // TODO Auto-generated catch block //e.printStackTrace; errornum ; log.append(m.getName); log.append(" "); log.append("has error:"); log.append("\n\r caused by "); //記錄測試過程中,發生的異常的名稱 log.append(e.getCause.getClass.getSimpleName); log.append("\n\r"); //記錄測試過程中,發生的異常的具體信息 log.append(e.getCause.getMessage); log.append("\n\r"); } } } log.append(clazz.getSimpleName); log.append(" has "); log.append(errornum); log.append(" error."); // 生成測試報告 System.out.println(log.toString); }}

測試的結果是:

12345678901 1=111-1=03 x 5=15chufa has error: caused by ArithmeticException/ by zeroNoBug has 1 error.

提示 NoBug 類中的 chufa 這個方法有異常,這個異常名稱叫做 ArithmeticException,原因是運算過程中進行了除 0 的操作。

所以,NoBug 這個類有 Bug。

這樣,通過註解我完成了我自己的目的,那就是對別人的代碼進行測試。

所以,再問我註解什麼時候用?我只能告訴你,這取決於你想利用它幹什麼用。

3.6註解應用實例

註解運用的地方太多了,如: JUnit 這個是一個測試框架,典型使用方法如下:

public class ExampleUnitTest { @Test public void addition_isCorrect throws Exception { assertEquals(4, 2 2); }}

@Test 標記了要進行測試的方法 addition_isCorrect.

還有例如ssm框架等運用了大量的註解。

註解部分總結如果註解難於理解,你就把它類同於標籤,標籤為了解釋事物,註解為了解釋代碼。註解的基本語法,創建如同接口,但是多了個 @ 符號。註解的元註解。註解的屬性。註解主要給編譯器及工具類型的軟體用的。註解的提取需要藉助於 Java 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。4.APT實現原理4.1 SPI機制

SPI是jdk內置的服務發現機制, 全稱叫Service Provider Interface.

SPI的工作原理, 就是ClassPath路徑下的META-INF/services文件夾中, 以接口的全限定名來命名文件名, 文件裡面寫該接口的實現。 然後再資源加載的方式,讀取文件的內容(接口實現的全限定名), 然後再去加載類。

SPI可以很靈活的讓接口和實現分離, 讓api提供者只提供接口, 第三方來實現。

這一機制為很多框架的擴展提供了可能,比如在 Dubbo、JDBC、SpringBoot 中都使用到了SPI機制。雖然他們之間的實現方式不同,但原理都差不多。今天我們就來看看,SPI到底是何方神聖,在眾多開源框架中又扮演了什麼角色。

4.1.1 JDK中的SPI

我們先從JDK開始,通過一個很簡單的例子來看下它是怎麼用的。

4.1.1.1、小慄子

首先,我們需要定義一個接口,SpiService

package com.dxz.jdk.spi;public interface SpiService { void println;}

然後,定義一個實現類,沒別的意思,只做列印。

package com.dxz.jdk.spi;​public class SpiServiceImpl implements SpiService { @Override public void println { System.out.println("------SPI DEMO-------"); }}

最後呢,要在resources路徑下配置添加一個文件。文件名字是接口的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。

文件內容就是實現類的全限定類名:

4.1.1.2、測試

然後我們就可以通過 ServiceLoader.load 方法拿到實現類的實例,並調用它的方法。

public static void main(String[] args){ ServiceLoader load = ServiceLoader.load(SpiService.class); Iterator iterator = load.iterator; while (iterator.hasNext){ SpiService service = iterator.next; service.println; }}

4.1.1.3、源碼分析

首先,我們先來了解下 ServiceLoader,看看它的類結構。

public final class ServiceLoader implements Iterable{ //配置文件的路徑 private static final String PREFIX = "META-INF/services/"; //加載的服務類或接口 private final Class service; //已加載的服務類集合 private LinkedHashMap providers = new LinkedHashMap; //類加載器 private final ClassLoader loader; //內部類,真正加載服務類 private LazyIterator lookupIterator;}

當我們調用 load 方法時,並沒有真正的去加載和查找服務類。而是調用了 ServiceLoader 的構造方法,在這裡最重要的是實例化了內部類 LazyIterator ,它才是接下來的主角。

private ServiceLoader(Class svc, ClassLoader cl) { //要加載的接口 service = Objects.requireNonNull(svc, "Service interface cannot be null"); //類加載器 loader = (cl == null) ? ClassLoader.getSystemClassLoader : cl; //訪問控制器 acc = (System.getSecurityManager != null) ? AccessController.getContext : null; //先清空 providers.clear; //實例化內部類 LazyIterator lookupIterator = new LazyIterator(service, loader);}

查找實現類和創建實現類的過程,都在 LazyIterator 完成。當我們調用 iterator.hasNext和iterator.next 方法的時候,實際上調用的都是 LazyIterator 的相應方法。

public Iterator iterator {​ return new Iterator { public boolean hasNext { return lookupIterator.hasNext; } public S next { return lookupIterator.next; } ....... };}

所以,我們重點關注 lookupIterator.hasNext 方法,它最終會調用到 hasNextServicez ,在這裡返回實現類名稱。

private class LazyIterator implements Iterator{ Class service; ClassLoader loader; Enumeration configs = null; Iterator pending = null; String nextName = null; private boolean hasNextService { //第二次調用的時候,已經解析完成了,直接返回 if (nextName != null) { return true; } if (configs == null) { //META-INF/services/ 加上接口的全限定類名,就是文件服務類的文件 //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService String fullName = PREFIX service.getName; //將文件路徑轉成URL對象 configs = loader.getResources(fullName); } while ((pending == null) || !pending.hasNext) { //解析URL文件對象,讀取內容,最後返回 pending = parse(service, configs.nextElement); } //拿到第一個實現類的類名 nextName = pending.next; return true; }}

然後當我們調用 next 方法的時候,調用到 lookupIterator.nextService 。它通過反射的方式,創建實現類的實例並返回。

private S nextService { //全限定類名 String cn = nextName; nextName = null; //創建類的Class對象 Class c = Class.forName(cn, false, loader); //通過newInstance實例化 S p = service.cast(c.newInstance); //放入集合,返回實例 providers.put(cn, p); return p; }

到這為止,已經獲取到了類的實例。

4.1.2 JDBC中的應用

我們開頭說,SPI機制為很多框架的擴展提供了可能,其實JDBC就應用到了這一機制。

在以前,需要先設置資料庫驅動的連接,再通過 DriverManager.getConnection 獲取一個 Connection 。

String url = "jdbc:mysql:///consult?serverTimezone=UTC";String user = "root";String passwor d = "r oot";​Class.forName("com.mysql.jdbc.Driver");Connection connection = DriverManager.getConnection(url, user, password);

而現在,設置資料庫驅動連接,這一步驟就不再需要,那麼它是怎麼分辨是哪種資料庫的呢?答案就在SPI。

4.1.2.1 加載

下圖mysql Driver的實例。 com.mysql.cj.jdbc.Driver就是Driver的實現。

mysql驅動為例

mysql Driver實現類

Driver接口上的一段注釋。

DriverManager將嘗試加載儘可能多的驅動程序。

我們把目光回到 DriverManager 類,它在靜態代碼塊裡面做了一件比較重要的事。很明顯,它已經通過SPI機制, 把資料庫驅動連接初始化了。

public class DriverManager { static { loadInitialDrivers; println("JDBC DriverManager initialized"); }}

接下來我們去DriverManger上看看是如何加載Driver接口的實現類的。

public class DriverManager {​ /** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers; println("JDBC DriverManager initialized"); }​ private static void loadInitialDrivers { AccessController.doPrivileged(new PrivilegedAction { public Void run { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator; try{ while(driversIterator.hasNext) { driversIterator.next; } } catch(Throwable t) { } return null; } });}

在DriverManger類初始化的時候, 調用loadInitialDrivers方法。

具體過程還得看 loadInitialDrivers ,它在裡面查找的是Driver接口的服務類,所以它的文件路徑就是:

META-INF/services/java.sql.Driver

在loadInitialDrivers方法中,

private static void loadInitialDrivers { AccessController.doPrivileged(new PrivilegedAction { public Void run { //很明顯,它要加載Driver接口的服務類,Driver接口的包為:java.sql.Driver //所以它要找的就是META-INF/services/java.sql.Driver文件 ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator; try{ //查到之後創建對象 while(driversIterator.hasNext) { driversIterator.next;//當調用next方法時,就會創建這個類的實例。它就完成了一件事,向 DriverManager 註冊自身的實例。 } } catch(Throwable t) { // Do nothing } return null; } });}

這段代碼是實現SPI的關鍵, 真是這個ServiceLoader類去實現SPI的。 那麼下面就分析分析ServiceLoader的代碼, 看看是如何實現SPI的。

package java.util;​public final class ServiceLoader implements Iterable { public static ServiceLoader load(Class service) { ClassLoader cl = Thread.currentThread.getContextClassLoader; return ServiceLoader.load(service, cl); }​ //其中service就是要加載實現類對應的接口, loader就是用該加載器去加載對應的實現類 public static ServiceLoader load(Class service, ClassLoader loader){ return new ServiceLoader(service, loader); }}

先調用ServiceLoader類的靜態方法load, 然後根據當前線程的上下文類加載器,創建一個ServiceLoader實例。

private static final String PREFIX = "META-INF/services/";​public void reload { providers.clear; lookupIterator = new LazyIterator(service, loader); }private ServiceLoader(Class svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader : cl; acc = (System.getSecurityManager != null) ? AccessController.getContext : null; reload; }

創建ServiceLoader實例的時候,接著創建一個Iterator實現類。 接下來這個Iterator分析的重點。基本所有的加載類的實現邏輯都在裡面。

其中ServiceLoader類中一個常量的定義是關鍵的。 前面說過,我們service的實現類在放在哪, 就是這裡寫死的常量路徑。

//這裡先介紹Iterator的變量,先大概有個印象。private class LazyIterator implements Iterator { //service, loader前面介紹過了。 Class service; ClassLoader loader; Enumeration configs = null; Iterator pending = null; String nextName = null;​ public boolean hasNext { //省略權限相關代碼 return hasNextService; }​ private boolean hasNextService { //一開始nextName肯定為空 if (nextName != null) { return true; } //一開始configs也肯定為空 if (configs == null) { try { //PREFIX = META-INF/services/ //以mysql為例,就是 META-INF/services/java.sql.Driver String fullName = PREFIX service.getName; if (loader == null) configs = ClassLoader.getSystemResources(fullName); //loader去加載這個classpath下文件。 //這裡很有可能返回的是多個文件的資源, //例如一個項目下既有mysql驅動, 也有sql server驅動等 //所以返回的是一個枚舉類型。 else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext) { if (!configs.hasMoreElements) { return false; } //然後根據加載出來的資源,解析一個文件中的內容。放到Iterator實現類中 pending = parse(service, configs.nextElement); } //這裡next返回的就是文件一行的內容,一般一行對應一個接口的實現類。 //一個接口放多行,就可以有多個接口實現類中。 nextName = pending.next; return true; }}

configs變量,就對應service文件。 是個枚舉, 就是說可以定義多個service文件。

pending 變量: 就對應configs中, service文件解析出來的一行有效內容,即一個實現類的全限定類名稱。

parse方法就是簡單,不是重點。這裡就略過了。就是讀取service文件中讀取,一行就是一個nextName,然後遇到「#「就跳過「#」後面的內容。所以service文件可以用「#」作為注釋。 直到遇到空行,解析結束。

LazyIterator類中的hasNext方法就分析完了。 使用classLoader.getResources方法加載service文件。我看了下getResources方法,並一定是加載classpath下的資源,得根據classLoader來解決。不過絕大多數情況下,都是classpath的資源。這裡為了好理解,就理解成classpath下的資源。

接著分析LazyIterator#next方法。

public S next { //刪除權限相關代碼 return nextService; }private S nextService { if (!hasNextService) throw new NoSuchElementException; //這個nextName前面分析過了 String cn = nextName; nextName = null; Class c = null; try { //加載類,且不初始化 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " cn " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " cn " not a subtype"); } try { //類型判斷 S p = service.cast(c.newInstance); //最後放到ServiceLoader實例變量Map中,緩存起來,下次直接使用 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " cn " could not be instantiated", x); } throw new Error; // This cannot happen }

next方法就比較簡單了,根據前面解析出來的nextName(接口實現類的全限定名稱),用Class.forName創建對應的Class對象。

4.1.2.2 創建Connection

DriverManager.getConnection 方法就是創建連接的地方,它通過循環已註冊的資料庫驅動程序,調用其connect方法,獲取連接並返回。

private static Connection getConnection(String url, Properties info, Class caller) throws SQLException { //registeredDrivers中就包含com.mysql.cj.jdbc.Driver實例 for(DriverInfo aDriver : registeredDrivers) { if(isDriverAllowed(aDriver.driver, callerCL)) { try { //調用connect方法創建連接 Connection con = aDriver.driver.connect(url, info); if (con != null) { return (con); } }catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println("skipping: " aDriver.getClass.getName); } }}

4.1.2.3 擴展

既然我們知道JDBC是這樣創建資料庫連接的,我們能不能再擴展一下呢?如果我們自己也創建一個 java.sql.Driver 文件,自定義實現類MySQLDriver,那麼,在獲取連接的前後就可以動態修改一些信息。

還是先在項目resources下創建文件,文件內容為自定義驅動類 com.jcc.java.spi.domyself.MySQLDriver

我們的 MySQLDriver 實現類,繼承自MySQL中的 NonRegisteringDriver ,還要實現 java.sql.Driver 接口。這樣,在調用connect方法的時候,就會調用到此類,但實際創建的過程還靠MySQL完成。

public class MySQLDriver extends NonRegisteringDriver implements Driver{ static { try { DriverManager.registerDriver(new MySQLDriver); } catch (SQLException e) { e.printStackTrace; } } public MySQLDriver throws SQLException {}​ @Override public Connection connect(String url, Properties info) throws SQLException { System.out.println("準備創建資料庫連接.url:" url); System.out.println("JDBC配置信息:" info); //重置配置 info.setProperty("user", "root"); Connection connection = super.connect(url, info); System.out.println("資料庫連接創建完成!" connection.toString); return connection; }}

這樣的話,當我們獲取資料庫連接的時候,就會調用到這裡。

--------------------輸出結果---------------------準備創建資料庫連接.url:jdbc:mysql:///consult?serverTimezone=UTCJDBC配置信息:{user=root, password=root}資料庫連接創建完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

4.1.3 SpringBoot中的應用

Spring Boot提供了一種快速的方式來創建可用於生產環境的基於Spring的應用程式。它基於Spring框架,更傾向於約定而不是配置,並且旨在使您儘快啟動並運行。

即便沒有任何配置文件,SpringBoot的Web應用都能正常運行。這種神奇的事情,SpringBoot正是依靠自動配置來完成。

說到這,我們必須關注一個東西: SpringFactoriesLoader,自動配置就是依靠它來加載的。

4.1.3.1 配置文件

SpringFactoriesLoader 來負責加載配置。我們打開這個類,看到它加載文件的路徑是: META-INF/spring.factories

筆者在項目中搜索這個文件,發現有4個Jar包都包含它:

List itemspring-boot-2.1.9.RELEASE.jarspring-beans-5.1.10.RELEASE.jarspring-boot-autoconfigure-2.1.9.RELEASE.jarmybatis-spring-boot-autoconfigure-2.1.0.jar

那麼它們裡面都是些啥內容呢?其實就是一個個接口和類的映射。在這裡筆者就不貼了,有興趣的小夥伴自己去看看。

比如在SpringBoot啟動的時候,要加載所有的 ApplicationContextInitializer ,那麼就可以這樣做:

SpringFactoriesLoader.loadFactoryNames(ApplicationContextInitializer.class, classLoader)

4.1.3.2 加載文件

loadSpringFactories 就負責讀取所有的 spring.factories 文件內容。

private static Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader) {​ MultiValueMap result = cache.get(classLoader); if (result != null) { return result; } try { //獲取所有spring.factories文件的路徑 Enumeration urls = lassLoader.getResources("META-INF/spring.factories"); result = new LinkedMultiValueMap; while (urls.hasMoreElements) { URL url = urls.nextElement; //加載文件並解析文件內容 UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry entry : properties.entrySet) { String factoryClassName = ((String) entry.getKey).trim; for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue)) { result.add(factoryClassName, factoryName.trim); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" FACTORIES_RESOURCE_LOCATION "]", ex); }}

可以看到,它並沒有採用JDK中的SPI機制來加載這些類,不過原理差不多。都是通過一個配置文件,加載並解析文件內容,然後通過反射創建實例。

4.1.3.3 參與其中

假如你希望參與到 SpringBoot 初始化的過程中,現在我們又多了一種方式。

我們也創建一個 spring.factories 文件,自定義一個初始化器。

org.springframework.context.ApplicationContextInitializer=com.youyouxunyin.config.context.MyContextInitializer

然後定義一個MyContextInitializer類

public class MyContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { System.out.println(configurableApplicationContext); }}

4.1.4 Dubbo中的應用

我們熟悉的Dubbo也不例外,它也是通過 SPI 機制加載所有的組件。同樣的,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模塊。基於 SPI,我們可以很容易的對 Dubbo 進行拓展。

它的使用方式同樣是在 META-INF/services 創建文件並寫入相關類名。

4.1.5 sentinel中的應用

通過SPI機制將META-INFO/servcie下配置好的默認責任鏈構造這加載出來,然後調用其builder方法進行構建調用鏈。

public final class SlotChainProvider {​ private static volatile SlotChainBuilder slotChainBuilder = null;​ /** * The load and pick process is not thread-safe, but it's okay since the method should be only invoked * via {@code lookProcessChain} in {@link com.alibaba.csp.sentinel.CtSph} under lock. * * @return new created slot chain */ public static ProcessorSlotChain newSlotChain { if (slotChainBuilder != null) { return slotChainBuilder.build; }​ // Resolve the slot chain builder SPI. slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault;​ if (slotChainBuilder == null) { // Should not go through here. RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default"); slotChainBuilder = new DefaultSlotChainBuilder; } else { RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}", slotChainBuilder.getClass.getCanonicalName); } return slotChainBuilder.build; }​ private SlotChainProvider {}}

SpiLoader.of

public static SpiLoader of(Class service) { AssertUtil.notNull(service, "SPI class cannot be null"); AssertUtil.isTrue(service.isInterface || Modifier.isAbstract(service.getModifiers), "SPI class[" service.getName "] must be interface or abstract class");​ String className = service.getName; SpiLoader spiLoader = SPI_LOADER_MAP.get(className); if (spiLoader == null) { synchronized (SpiLoader.class) { spiLoader = SPI_LOADER_MAP.get(className); if (spiLoader == null) { SPI_LOADER_MAP.putIfAbsent(className, new SpiLoader(service)); spiLoader = SPI_LOADER_MAP.get(className); } } }​ return spiLoader; }

@Spi(isDefault = true)public class DefaultSlotChainBuilder implements SlotChainBuilder {​ @Override public ProcessorSlotChain build { ProcessorSlotChain chain = new DefaultProcessorSlotChain;​ List sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted; for (ProcessorSlot slot : sortedSlotList) { if (!(slot instanceof AbstractLinkedProcessorSlot)) { RecordLog.warn("The ProcessorSlot(" slot.getClass.getCanonicalName ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain"); continue; }​ chain.addLast((AbstractLinkedProcessorSlot) slot); }​ return chain; }}

責任鏈同樣是由spi機制加載出來的,上面的加載只會在第一次使用的時候加載,然後緩存到內從後,以後直接取即可。

至此,SPI機制的實現原理就分析完了。 雖然SPI我們日常開發中用的很少,但是至少了解了解還是有必要的。 例如: 一些框架實現中一般都會用到SPI機制。

vert.x內部也是大量使用SPI

4.2 APT註解處理器4.2.1 基礎知識

註解的保留時間分為三種:

SOURCE——只在原始碼中保留,編譯器將代碼編譯成字節碼文件後就會丟掉CLASS——保留到字節碼文件中,但Java虛擬機將class文件加載到內存是不一定在內存中保留RUNTIME——一直保留到運行時

通常我們使用後兩種,因為SOURCE主要起到標記方便理解的作用,無法對代碼邏輯提供有效的信息。

時間

解析

性能影響

RUNTIME

運行時

反射

CLASS

編譯期

APT JavaPoet

如上圖,對比兩種解析方式:

運行時註解比較簡單易懂,可以運用反射技術在程序運行時獲取指定的註解信息,因為用到反射,所以性能會收到一定影響。編譯期註解可以使用APT(Annotation Processing Tool)技術,在編譯期掃描和解析註解,並結合JavaPoet技術生成新的java文件,是一種更優雅的解析註解的方式,不會對程序性能產生太大影響。

下面以BindView為例,介紹兩種方式的不同使用方法。

4.2.2 運行時註解

運行時註解主要通過反射進行解析,代碼運行過程中,通過反射我們可以知道哪些屬性、方法使用了該註解,並且可以獲取註解中的參數,做一些我們想做的事情。

首先,新建一個註解

@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface BindViewTo { int value default -1; //需要綁定的view id}

然後,新建一個註解解析工具類AnnotationTools,和一般的反射用法並無不同:

public class AnnotationTools {​ public static void bindAllAnnotationView(Activity activity) { //獲得成員變量 Field[] fields = activity.getClass.getDeclaredFields;​ for (Field field : fields) { try { if (field.getAnnotations != null) { //判斷BindViewTo註解是否存在 if (field.isAnnotationPresent(BindViewTo.class)) { //獲取訪問權限 field.setAccessible(true); BindViewTo getViewTo = field.getAnnotation(BindViewTo.class); //獲取View id int id = getViewTo.value; //通過id獲取View,並賦值該成員變量 field.set(activity, activity.findViewById(id)); } } } catch (Exception e) { } } }}

在Activity中調用

public class MainActivity extends AppCompatActivity {​ @BindViewTo(R.id.text) private TextView mText;​ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);​ //調用註解綁定,當前Activity中所有使用@BindViewTo註解的控制項將自動綁定 AnnotationTools.bindAllAnnotationView(this);​ //測試綁定是否成功 mText.setTextColor(Color.RED); }​}

測試結果毫無意外,字體變成了紅色,說明綁定成功。

4.2.3 編譯期註解(APT JavaPoet)

編譯期註解解析需要用到APT(Annotation Processing Tool)技術,APT是javac中提供的一種編譯時掃描和處理註解的工具,它會對原始碼文件進行檢查,並找出其中的註解,然後根據用戶自定義的註解處理方法進行額外的處理。APT工具不僅能解析註解,還能結合JavaPoet技術根據註解生成新的的Java源文件,最終將生成的新文件與原來的Java文件共同編譯。

APT實現流程如下:

創建一個java lib作為註解解析庫——如apt_processor在創建一個java lib作為註解聲明庫——如apt_annotation搭建兩個lib和主項目的依賴關係實現AbstractProcessor編譯和調用

整個流程是固定的,我們的主要工作是繼承AbstractProcessor,並且實現其中四個方法。下面一步一步詳細介紹:

4.2.3.1創建解析庫apt_processor

apply plugin: 'java-library'​dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) compile 'com.squareup:javapoet:1.9.0' // square開源的 Java 代碼生成框架 compile 'com.google.auto.service:auto-service:1.0-rc2' //Google開源的用於註冊自定義註解處理器的工具 implementation project(':apt_annotation') //依賴自定義註解聲明庫}sourceCompatibility = "7"targetCompatibility = "7"

4.2.3.2 創建註解庫apt_annotation

聲明一個註解BindViewTo,注意@Retention不再是RUNTIME,而是CLASS。

@Target({ElementType.FIELD})@Retention(RetentionPolicy.CLASS)public @interface BindViewTo { int value default -1;}

4.2.3.3 搭建主項目依賴關係

dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':apt_annotation') //依賴自定義註解聲明庫 annotationProcessor project(':apt_processor') //依賴自定義註解解析庫(僅編譯期)}

這裡需要解釋一下,因為註解解析庫只在程序編譯期有用,沒必要打包進APK。所以依賴解析庫使用的關鍵字是annotationProcessor,這是google為gradle插件添加的特性,表示只在編譯期依賴,不會打包進最終APK。這也是為什麼前面要把註解聲明和註解解析拆分成兩個庫的原因。因為註解聲明是一定要編譯到最終APK的,而註解解析不需要。

4.2.3.4 實現AbstractProcessor

這是最複雜的一步,也是完成我們期望工作的重點。首先,我們在apt_processor中創建一個繼承自AbstractProcessor的子類,重載其中四個方法:

init——此處初始化一個工具類getSupportedSourceVersion——聲明支持的Java版本,一般為最新版本getSupportedAnnotationTypes——聲明支持的註解列表process——編譯器回調方法,apt核心實現方法

具體代碼如下:

//@SupportedSourceVersion(SourceVersion.RELEASE_7)//@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo")@AutoService(Processor.class)public class BindViewProcessor extends AbstractProcessor {​ private Elements mElementUtils; private HashMap mCreatorMap = new HashMap;​ /** * init方法一般用於初始化一些用到的工具類,主要有 * processingEnvironment.getElementUtils; 處理Element的工具類,用於獲取程序的元素,例如包、類、方法。 * processingEnvironment.getTypeUtils; 處理TypeMirror的工具類,用於取類信息 * processingEnvironment.getFiler; 文件工具 * processingEnvironment.getMessager; 錯誤處理工具 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mElementUtils = processingEnv.getElementUtils; }​ /** * 獲取Java版本,一般用最新版本 * 也可以使用註解方式:@SupportedSourceVersion(SourceVersion.RELEASE_7) */ @Override public SourceVersion getSupportedSourceVersion { return SourceVersion.latestSupported; }​ /** * 獲取目標註解列表 * 也可以使用註解方式:@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo") */ @Override public Set getSupportedAnnotationTypes { HashSet supportTypes = new LinkedHashSet; supportTypes.add(BindViewTo.class.getCanonicalName); return supportTypes; }​ /** * 編譯期回調方法,apt核心實現方法 * 包含所有使用目標註解的元素(Element) */ @Override public boolean process(Set set, RoundEnvironment roundEnvironment) { //掃描整個工程, 找出所有使用BindViewTo註解的元素 Set elements = roundEnvironment.getElementsAnnotatedWith(BindViewTo.class); //遍曆元素, 為每一個類元素創建一個Creator for (Element element : elements) { //BindViewTo限定了只能屬性使用, 這裡強轉為變量元素VariableElement VariableElement variableElement = (VariableElement) element; //獲取封裝屬性元素的類元素TypeElement TypeElement classElement = (TypeElement) variableElement.getEnclosingElement; //獲取簡單類名 String fullClassName = classElement.getQualifiedName.toString; BinderClassCreator creator = mCreatorMap.get(fullClassName); //如果不存在, 則創建一個對應的Creator if (creator == null) { creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement), classElement); mCreatorMap.put(fullClassName, creator);​ } //將需要綁定的變量和對應的view id存儲到對應的Creator中 BindViewTo bindAnnotation = variableElement.getAnnotation(BindViewTo.class); int id = bindAnnotation.value; creator.putElement(id, variableElement); }​ //每一個類將生成一個新的java文件,其中包含綁定代碼 for (String key : mCreatorMap.keySet) { BinderClassCreator binderClassCreator = mCreatorMap.get(key); //通過javapoet構建生成Java類文件 JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName, binderClassCreator.generateJavaCode).build; try { javaFile.writeTo(processingEnv.getFiler); } catch (IOException e) { e.printStackTrace; }​ } return false; }}

其中,BinderClassCreator是代碼生成相關方法,具體代碼如下:

public class BinderClassCreator {​ public static final String ParamName = "rootView";​ private TypeElement mTypeElement; private String mPackageName; private String mBinderClassName; private Map mVariableElements = new HashMap;​ /** * @param packageElement 包元素 * @param classElement 類元素 */ public BinderClassCreator(PackageElement packageElement, TypeElement classElement) { this.mTypeElement = classElement; mPackageName = packageElement.getQualifiedName.toString; mBinderClassName = classElement.getSimpleName.toString "_ViewBinding"; }​ public void putElement(int id, VariableElement variableElement) { mVariableElements.put(id, variableElement); }​ public TypeSpec generateJavaCode { return TypeSpec.classBuilder(mBinderClassName) //public 修飾類 .addModifiers(Modifier.PUBLIC) //添加類的方法 .addMethod(generateMethod) //構建Java類 .build;​ }​ private MethodSpec generateMethod { //獲取全類名 ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName.toString); //構建方法--方法名 return MethodSpec.methodBuilder("bindView") //public方法 .addModifiers(Modifier.PUBLIC) //返回void .returns(void.class) //方法傳參(參數全類名,參數名) .addParameter(className, ParamName) //方法代碼 .addCode(generateMethodCode) .build; }​ private String generateMethodCode { StringBuilder code = new StringBuilder; for (int id : mVariableElements.keySet) { VariableElement variableElement = mVariableElements.get(id); //變量名稱 String name = variableElement.getSimpleName.toString; //變量類型 String type = variableElement.asType.toString; //rootView.name = (type)view.findViewById(id), 注意原類中變量聲明不能為private,否則這裡是獲取不到的 String findViewCode = ParamName "." name "=(" type ")" ParamName ".findViewById(" id ");\n"; code.append(findViewCode);​ } return code.toString; }​ public String getPackageName { return mPackageName; }}

4.2.3.5 編譯和調用

在MainActivity中調用,這裡需要強調的是待綁定變量不能聲明為private,原因在上面代碼注釋中已經解釋了。

public class MainActivity extends AppCompatActivity {​ @BindViewTo(R.id.text) public TextView mText;​ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);//這裡的MainActivity需要先編譯生成後才能調用 new MainActivity_ViewBinding.bindView(this); //測試綁定是否成功 mText.setTextColor(Color.RED); }}

此時,build或rebuild工程(需要先注掉MainActivity的調用),會看到在generatedJava文件夾下生成了新的Java文件。

上面的調用方式需要先編譯一次才能使用,當有多個Activity時比較繁瑣,而且無法做到統一。

我們也可以選擇另一種更簡便的方法,即反射調用。新建工具類如下:

public class MyButterKnife {​ public static void bind(Activity activity) { Class clazz = activity.getClass; try { Class bindViewClass = Class.forName(clazz.getName "_ViewBinding"); Method method = bindViewClass.getMethod("bindView", activity.getClass); method.invoke(bindViewClass.newInstance, activity); } catch (Exception e) { e.printStackTrace; } }​}

調用方式改為:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);​ //通過反射調用 MyButterKnife.bind(this);​ //測試綁定是否成功 mText.setTextColor(Color.RED); }

此方式雖然也會稍微影響性能,但依然比直接使用運行時註解高效得多。

4.2.4 APT註解處理器總結

說到底,APT是一個編譯器工具,是一個非常好的從源碼到編譯期的過渡解析工具。雖然結合JavaPoet技術被各大框架使用,但是依然存在固有的缺陷,比如變量不能私有,依然要採用反射調用等,普通開發者可斟酌使用。

個人認為APT有如下優點:

配置方式,替換文件配置方式,改為代碼內配置,提高程序內聚性代碼精簡,一勞永逸,省去繁瑣複雜的格式化代碼,適合團隊內推廣

以上優點同時也是缺點,因為很多代碼都在後臺生成,會對新同學造成理解困難,影響其對整體架構的理解,增加學習成本。

近期研究熱修復和APT,發現從我們寫完成代碼,到代碼真正執行,期間還真是有大把的「空子」可以鑽啊,借圖mark一下。

4.3 javac源碼分析4.3.1 javac概述

我們都知道 *.java 文件要首先被編譯成 *.class 文件才能被 JVM 認識,這部分的工作主要由 Javac 來完成,類似於 Javac 這樣的我們稱之為前端編譯器;

但是 *.class 文件也不是機器語言,怎麼才能讓機器識別呢?就需要 JVM 將 *.class 文件編譯成機器碼,這部分工作由JIT 編譯器完成;

除了這兩種編譯器,還有一種直接把 *.java 文件編譯成本地機器碼的編譯器,我們稱之AOT 編譯器。

4.3.2 javac 的編譯過程

首先,我們先導一份 javac 的源碼(基於 openjdk8)出來,下載地址:https://hg.openjdk.java.net/jdk8/jdk8/langtools/archive/tip.tar.gz,然後將 JDK_SRC_HOME/langtools/src/share/classes/com/sun 目錄下的源文件全部複製到工程的源碼目錄中,生成的 目錄如下:

我們執行 com.sun.tools.javac.Main 的 main 方法,就和我們在命令窗口中使用 javac 命令一樣:

從 Sun Javac 的代碼來看,編譯過程大致可以分為三個步驟:

解析和填充符號表過程插入式註解處理器的註解處理過程分析和字節碼生成過程

這三個步驟所做的工作內容大致如下:

這三個步驟之間的關係和交互順序如下圖所示,可以看到如果註解處理器在處理註解期間對語法樹進行了修改,編譯器將回到解析和填充符號表的過程進行重新處理,直到註解處理器沒有再對語法樹進行修改為止。

Javac 編譯的入口是 com.sun.tools.javac.main.JavaCompiler 類,上述三個步驟的代碼都集中在這個類的 compile 和 compile2 中:

4.3.3 javac編譯器編譯程序的步驟

詞法分析 首先是讀取原始碼,找出這些字節中哪些是我們定義的語法關鍵詞,如Java中的if、else、for等關鍵詞 語法分析的結果:從原始碼中找出一些規範化的token流 注:token是一種認證機制​語法分析 檢查關鍵詞組合在一起是不是Java語言規範,如if後面是不是緊跟著一個布爾表達式。 語法分析的結果:形成一個符合Java語言規範的抽象語法樹​語義分析 把一些難懂的、複雜的語法轉化為更加簡單的語法 語義分析的結果:完成複雜語法到簡單語法的簡化,如將foreach語句轉化成for循環結果,還有註解等。最後形成一個註解過後的抽象語法樹,這顆語法樹更接近目標語言的語法規則​生成字節碼 通過字節碼生成器生成字節碼,根據經過註解的抽象語法樹生成字節碼,也就是將一個數據結構轉化成另一個數據結構 代碼生成器的結果:生成符合Java虛擬機規範的字節碼​註:抽象語法樹 在計算機科學中,抽象語法樹是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了