AOP

1、閒談AOP

你們都知道OOP,即ObjectOriented Programming,面向對象編程。而本文要介紹的是AOP。AOP是Aspect Oriented Programming的縮寫,中譯文爲面向切向編程。OOP和AOP是什麼關係呢?首先:html

 

  • OOP和AOP都是方法論。我記得在剛學習C++的時候,最難學的並非C++的語法,而是C++所表明的那種看問題的方法,即OOP。一樣,今天在AOP中,我發現其難度並不在利用AOP幹活,而是從AOP的角度來看待問題,設計解決方法。這就是爲何我特地強調AOP是一種方法論的緣由!
  • l 在OOP的世界中,問題或者功能都被劃分到一個一個的模塊裏邊。每一個模塊專心幹本身的事情,模塊之間經過設計好的接口交互。從圖示來看,OOP世界中,最多見的表示好比:

圖1中所示爲AndroidFramework中的模塊。OOP世界中,你們畫的模塊圖基本上是這樣的,每一個功能都放在一個模塊裏。很是好理解,並且確實簡化了咱們所處理問題的難度。java

OOP的精髓是把功能或問題模塊化,每一個模塊處理本身的家務事。但在現實世界中,並非全部問題都能完美得劃分到模塊中。舉個最簡單而又常見的例子:如今想爲每一個模塊加上日誌功能,要求模塊運行時候能輸出日誌。在不知道AOP的狀況下,通常的處理都是:先設計一個日誌輸出模塊,這個模塊提供日誌輸出API,好比Android中的Log類。而後,其餘模塊須要輸出日誌的時候調用Log類的幾個函數,好比e(TAG,...),w(TAG,...),d(TAG,...),i(TAG,...)等。android

在沒有接觸AOP以前,包括我在內,想到的解決方案就是上面這樣的。可是,從OOP角度看,除了日誌模塊自己,其餘模塊的家務事絕大部分狀況下應該都不會包含日誌輸出功能。什麼意思?以ActivityManagerService爲例,你能說它的家務事裏包含日誌輸出嗎?顯然,ActivityManagerService的功能點中不包含輸出日誌這一項。但實際上,軟件中的衆多模塊確實又須要打印日誌。這個日誌輸出功能,從總體來看,都是一個上的。而這個面的範圍,就不侷限在單個模塊裏了,而是橫跨多個模塊。git

  • l 在沒有AOP以前,各個模塊要打印日誌,就是本身處理。反正日誌模塊的那幾個API都已經寫好了,你在其餘模塊的任何地方,任什麼時候候均可以調用。功能是獲得了知足,可是好像沒有Oriented的感受了。是的,隨意加日誌輸出功能,使得其餘模塊的代碼和日誌模塊耦合很是緊密。並且,未來要是日誌模塊修改了API,則使用它們的地方都得改。這種搞法,一點也不酷。

 

AOP的目標就是解決上面提到的不cool的問題。在AOP中:sql

  • l 第一,咱們要認識到OOP世界中,有些功能是橫跨並嵌入衆多模塊裏的,好比打印日誌,好比統計某個模塊中某些函數的執行時間等。這些功能在各個模塊裏分散得很厲害,可能處處都能見到。
  • l 第二,AOP的目標是把這些功能集中起來,放到一個統一的地方來控制和管理。若是說,OOP若是是把問題劃分到單個模塊的話,那麼AOP就是把涉及到衆多模塊的某一類問題進行統一管理。好比咱們能夠設計兩個Aspects,一個是管理某個軟件中全部模塊的日誌輸出的功能,另一個是管理該軟件中一些特殊函數調用的權限檢查。

講了這麼多,仍是先來看個例子。在這個例子中,咱們要:編程

  • l Activity的生命週期的幾個函數運行時,要輸出日誌。
  • l 幾個重要函數調用的時候,要檢查有沒有權限。

注意,本文的例子代碼在https://code.csdn.net/Innost/androidaopdemo上。ubuntu

2、沒有AOP的例子

先來看沒有AOP的狀況下,代碼怎麼寫。主要代碼都在AopDemoActivity中api

[-->AopDemoActivity.java]安全

public class AopDemoActivity extends Activity {  
   private static final String TAG = "AopDemoActivity";  
Œ  onCreate,onStart,onRestart,onPause,onResume,onStop,onDestory返回前,都輸出一行日誌  
   protected void onCreate(Bundle savedInstanceState) {  
       super.onCreate(savedInstanceState);  
       setContentView(R.layout.layout_main);  
       Log.e(TAG,"onCreate");  
    }  
   protected void onStart() {  
       super.onStart();  
        Log.e(TAG, "onStart");  
    }  
   protected void onRestart() {  
       super.onRestart();  
        Log.e(TAG, "onRestart");  
    }  
    protectedvoid onResume() {  
       super.onResume();  
        Log.e(TAG, "onResume");  
   checkPhoneState會檢查app是否申明瞭android.permission.READ_PHONE_STATE權限  
        checkPhoneState();  
    }  
   protected void onPause() {  
       super.onPause();  
        Log.e(TAG, "onPause");  
    }  
   protected void onStop() {  
       super.onStop();  
        Log.e(TAG, "onStop");  
    }  
   protected void onDestroy() {  
       super.onDestroy();  
        Log.e(TAG, "onDestroy");  
    }  
   private void checkPhoneState(){  
       if(checkPermission("android.permission.READ_PHONE_STATE")== false){  
           Log.e(TAG,"have no permission to read phone state");  
           return;  
        }  
       Log.e(TAG,"Read Phone State succeed");  
       return;  
    }  
   private boolean checkPermission(String permissionName){  
       try{  
           PackageManager pm = getPackageManager();  
          //調用PackageMangaer的checkPermission函數,檢查本身是否申明使用某權限  
           int nret = pm.checkPermission(permissionName,getPackageName());  
           return nret == PackageManager.PERMISSION_GRANTED;  
        }......  
    }  
}

代碼很簡單。可是從這個小例子中,你也會發現要是這個程序比較複雜的話,處處都加Log,或者在某些特殊函數加權限檢查的代碼,真的是一件挺繁瑣的事情。數據結構

3、AspectJ介紹

3.1  AspectJ極簡介

AOP雖然是方法論,但就好像OOP中的Java同樣,一些先行者也開發了一套語言來支持AOP。目前用得比較火的就是AspectJ了,它是一種幾乎和Java徹底同樣的語言,並且徹底兼容Java(AspectJ應該就是一種擴展Java,但它不是像Groovy[1]那樣的拓展。)。固然,除了使用AspectJ特殊的語言外,AspectJ還支持原生的Java,只要加上對應的AspectJ註解就好。因此,使用AspectJ有兩種方法:

l 徹底使用AspectJ的語言。這語言一點也不難,和Java幾乎同樣,也能在AspectJ中調用Java的任何類庫。AspectJ只是多了一些關鍵詞罷了。

l 或者使用純Java語言開發,而後使用AspectJ註解,簡稱@AspectJ

Anyway,不論哪一種方法,最後都須要AspectJ的編譯工具ajc來編譯。因爲AspectJ實際上脫胎於Java,因此ajc工具也能編譯java源碼。

AspectJ如今託管於Eclipse項目中,官方網站是:

 

  • l http://www.eclipse.org/aspectj/   <=AspectJ官方網站
  • http://www.eclipse.org/aspectj/doc/released/runtime-api/index.html  <=AspectJ類庫參考文檔,內容很是少
  • l http://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/index.html  <=@AspectJ文檔,之後咱們用Annotation的方式最多。

3.2  AspectJ語法

題外話:AspectJ東西比較多,可是AOP作爲方法論,它的學習和體會是須要一步一步,而且必定要結合實際來的。若是一會兒講太多,反而會疲倦。更可怕的是,有些膽肥的同窗要是一股腦把全部高級玩法全弄上去,反而得不償失。這就是是方法論學習和其餘知識學習不同的地方。請你們切記。

3.2.1  Join Points介紹

Join Points(之後簡稱JPoints)是AspectJ中最關鍵的一個概念。什麼是JPoints呢?JPoints就是程序運行時的一些執行點。那麼,一個程序中,哪些執行點是JPoints呢?好比:

  • l 一個函數的調用能夠是一個JPoint。好比Log.e()這個函數。e的執行能夠是一個JPoint,而調用e的函數也能夠認爲是一個JPoint。
  • l 設置一個變量,或者讀取一個變量,也能夠是一個JPoint。好比Demo類中有一個debug的boolean變量。設置它的地方或者讀取它的地方均可以看作是JPoints。
  • l for循環能夠看作是JPoint。

理論上說,一個程序中不少地方均可以被看作是JPoint,可是AspectJ中,只有如表1所示的幾種執行點被認爲是JPoints:

表1  AspectJ中的Join Point

Join Points

說明

 

示例

method call

函數調用

好比調用Log.e(),這是一處JPoint

method execution

函數執行

好比Log.e()的執行內部,是一處JPoint。注意它和method call的區別。method call是調用某個函數的地方。而execution是某個函數執行的內部。

constructor call

構造函數調用

和method call相似

constructor execution

構造函數執行

和method execution相似

field get

獲取某個變量

好比讀取DemoActivity.debug成員

field set

設置某個變量

好比設置DemoActivity.debug成員

pre-initialization

Object在構造函數中作得一些工做。

不多使用,詳情見下面的例子

initialization

Object在構造函數中作得工做

詳情見下面的例子

static initialization

類初始化

好比類的static{}

handler

異常處理

好比try catch(xxx)中,對應catch內的執行

advice execution

這個是AspectJ的內容,稍後再說

 

表1列出了AspectJ所承認的JoinPoints的類型。下面咱們來看個例子以直觀體會一把。

圖2  示例代碼

圖2是一個Java示例代碼,下面咱們將打印出其中全部的join points。圖3所示爲打印出來的join points:

圖3  全部的join points

圖3中的輸出爲從左到右,咱們來解釋紅框中的內容。先來看左圖的第一個紅框:

  • staticinitialization(test.Test.<clinit>):表示當前是哪一種類型的JPoint,括號中表明目標對象是誰(此處是指Test class的類初始化)。因爲Test類沒有指定static block,因此後面的at:Test.java:0 表示代碼在第0行(其實就是沒有找到源代碼的意思)。
  • l Test類初始化完後,就該執行main函數了。因此,下一個JPoint就是execution(voidtest.Test.main(String[]))。括號中表示此JPoint對應的是test.Test.main函數。at:Test.java:30表示這個JPoint在源代碼的第30行。你們能夠對比圖2的源碼,很準確!
  • l main函數裏首先是執行System.out.println。而這一行代碼實際包括兩個JPoint。一個是get(PrintStream java.lang.System.out),get表示Field get,它表示從System中獲取out對象。另一個是call(void java.io.PrintStream.println(String)),這是一個call類型的JPoint,表示執行out.println函數。

 

再來看左圖第二個紅框,它表示TestBase的類的初始化,因爲源碼中爲TestBase定義了static塊,因此這個JPoint清晰指出了源碼的位置是at:Test.java:5

接着看左圖第三個紅框,它和對象的初始化有關。在源碼中,咱們只是構造了一個TestDerived對象。它會先觸發TestDerived Preinitialization JPoint,而後觸發基類TestBase的PreInitialization JPoint。注意紅框中的beforeafter 。在TestDerived和TestBase所對應的PreInitialization before和after中都沒有包含其餘JPoint。因此,Pre-Initialization應該是構造函數中一個比較基礎的Phase。這個階段不包括類中成員變量定義時就賦值的操做,也不包括構造函數中對某些成員變量進行的賦值操做。

而成員變量的初始化(包括成員變量定義時就賦值的操做,好比源碼中的int base = 0,以及在構造函數中所作的賦值操做,好比源碼中的this.derived = 1000)都被囊括到initialization階段。請讀者對應圖三第二個紅框到第三個紅框(包括第3個紅框的內容)看看是否是這樣的。

最後來看第5個紅框。它包括三個JPoint:

(1) 本身動手試

圖2的示例代碼我也放到了https://code.csdn.net/Innost/androidaopdemo上。請小夥伴們本身下載玩耍。具體的操做方法是:

 

  • l 下載獲得androidaopdemo中,有一個aspectj-test目錄。
  • l aspectj-test目錄中有一個libs目錄,裏邊有一個文件aspectj-1.8.7.jar文件。執行這個文件(java -jar aspectj-1.8.7.jar,安裝aspectj)。安裝完後,按照圖示要求將aspectj的安裝路徑加到PATH中,而後export。這樣,就能夠在命令行中執行aspectj的命令了。好比編譯工具ajc
  • l 另外,libs下還有一個aspectjrt.jar,這個是aspectjt運行時的依賴庫。使用AspectJ的程序都要包含該jar包。
  • l 執行create-jar.sh。它會編譯幾個文件,而後生成test.jar。
  • l 執行test.jar(java -jar test.jar),就會打印出圖3的log。

 

我已經編譯並提交了一個test.jar到git上,小夥伴們能夠執行一把玩玩!

3.2.2  Pointcuts介紹

pointcuts這個單詞很差翻譯,此處直接用英文好了。那麼,Pointcuts是什麼呢?前面介紹的內容可知,一個程序會有不少的JPoints,即便是同一個函數(好比testMethod這個函數),還分爲call類型和execution類型的JPoint。顯然,不是全部的JPoint,也不是全部類型的JPoint都是咱們關注的。再次以AopDemo爲例,咱們只要求在Activity的幾個生命週期函數中打印日誌,只有這幾個生命週期函數纔是咱們業務須要的JPoint,而其餘的什麼JPoint我不須要關注。

怎麼從一堆一堆的JPoints中選擇本身想要的JPoints呢?恩,這就是Pointcuts的功能。一句話,Pointcuts的目標是提供一種方法使得開發者可以選擇本身感興趣的JoinPoints。

在圖2的例子中,怎麼把Test.java中全部的Joinpoint選擇出來呢?用到的pointcut格式爲:

pointcuttestAll():within(Test)

AspectJ中,pointcut有一套標準語法,涉及的東西不少,還有一些比較高級的玩法。我本身琢磨了半天,需不須要把這些內容一股腦都搬到此文呢?回想我本身學習AOP的經歷,好像看了幾本書,記得比較清楚的都是簡單的case,而那些複雜的case則是到實踐中,確實有需求了,纔回過頭來,從新查閱文檔來實施的。恩,那就一步一步來吧。

  • l testMethod的call類型JPoint
  • l testMethod的execution類型JPonint
  • l 以及對異常捕獲的Handler類型JPoint

 

好了。JPoint的介紹就先到此。如今你們對JoinPoint應該有了一個很直觀的體會,簡單直白粗暴點說,JoinPoint就是一個程序中的關鍵函數(包括構造函數)和代碼段(staticblock)。

爲何AspectJ首先要定義好JoinPoint呢?你們仔細想一想就能明白,以打印log的AopDemo爲例,log在哪裏打印?天然是在一些關鍵點去打印。而誰是關鍵點?AspectJ定義的這些類型的JPoint就能知足咱們絕大部分需求。

注意,要是想在一個for循環中打印一些日誌,而AspectJ沒有這樣的JPoint,因此這個需求咱們是沒法利用AspectJ來實現了。另外,不一樣的軟件框架對錶1中的JPoint類型支持也不一樣。好比Spring中,不是全部AspectJ支持的JPoint都有。

(1) 一個Pointcuts例子

直接來看一個例子,如今我想把圖2中的示例代碼中,那些調用println的地方找到,該怎麼弄?代碼該這麼寫:

public pointcut  testAll(): call(public  *  *.println(..)) && !within(TestAspect) ;  

注意,aspectj的語法和Java同樣,只不過多了一些關鍵詞

咱們來看看上述代碼

Œ  第一個public:表示這個pointcut是public訪問。這主要和aspect的繼承關係有關,屬於AspectJ的高級玩法,本文不考慮。

  pointcut:關鍵詞,表示這裏定義的是一個pointcut。pointcut定義有點像函數定義。總之,在AspectJ中,你得定義一個pointcut。

Ž  testAll():pointcut的名字。在AspectJ中,定義Pointcut可分爲有名和匿名兩種辦法。我的建議使用named方法。由於在後面,咱們要使用一個pointcut的話,就能夠直接使用它的名字就好。

  testAll後面有一個冒號,這是pointcut定義名字後,必須加上。冒號後面是這個pointcut怎麼選擇Joinpoint的條件。

  本例中,call(public  *  *.println(..))是一種選擇條件。call表示咱們選擇的Joinpoint類型爲call類型。

‘  public  **.println(..):這小行代碼使用了通配符。因爲咱們這裏選擇的JoinPoint類型爲call類型,它對應的目標JPoint必定是某個函數。因此咱們要找到這個/些函數。public  表示目標JPoint的訪問類型(public/private/protect)。第一個*表示返回值的類型是任意類型。第二個*用來指明包名。此處不限定包名。緊接其後的println是函數名。這代表咱們選擇的函數是任何包中定義的名字叫println的函數。固然,惟一肯定一個函數除了包名外,還有它的參數。在(..)中,就指明瞭目標函數的參數應該是什麼樣子的。好比這裏使用了通配符..,表明任意個數的參數,任意類型的參數。

’  再來看call後面的&&:AspectJ能夠把幾個條件組合起來,目前支持 &&,||,以及!這三個條件。這三個條件的意思不用我說了吧?和Java中的是同樣的。

“  來看最後一個!within(TestAspectJ):前面的!表示不知足某個條件。within是另一種類型選擇方法,特別注意,這種類型和前面講到的joinpoint的那幾種類型不一樣。within的類型是數據類型,而joinpoint的類型更像是動態的,執行時的類型。

上例中的pointcut合起來就是:

  • l 選擇那些調用println(並且不考慮println函數的參數是什麼)的Joinpoint。
  • l 另外,調用者的類型不要是TestAspect的。

 

圖4展現了執行結果:

 

圖4  新pointcut執行結果

我在圖2所示的源碼中,爲Test類定義了一個public static void println()函數,因此圖4的執行結果就把這個println給匹配上了。

看完例子,咱們來說點理論。

(2) 直接針對JoinPoint的選擇

pointcuts中最經常使用的選擇條件和Joinpoint的類型密切相關,好比圖5:

圖5  不一樣類型的JPoint對應的pointcuts查詢方法

以圖5爲例,若是咱們想選擇類型爲methodexecution的JPoint,那麼pointcuts的寫法就得包括execution(XXX)來限定。

除了指定JPoint類型外,咱們還要更進一步選擇目標函數。選擇的根據就是圖5中列出的什麼MethodSignature,ConstructorSignature,TypeSinature,FieldSignature等。名字聽起來陌生得很,其實就是指定JPoint對應的函數(包括構造函數),Static block的信息。好比圖4中的那個println例子,首先它的JPoint類型是call,因此它的查詢條件是根據MethodSignature來表達。一個Method Signature的完整表達式爲:

@註解 訪問權限 返回值的類型 包名.函數名(參數)
Œ  @註解和訪問權限(public/private/protect,以及static/final)屬於可選項。若是不設置它們,則默認都會選擇。以訪問權限爲例,若是沒有設置訪問權限做爲條件,那麼public,private,protect及static、final的函數都會進行搜索。
  返回值類型就是普通的函數的返回值類型。若是不限定類型的話,就用*通配符表示
Ž  包名.函數名用於查找匹配的函數。可使用通配符,包括*和..以及+號。其中*號用於匹配除.號以外的任意字符,而..則表示任意子package,+號表示子類。
     好比:
     java.*.Date:能夠表示java.sql.Date,也能夠表示java.util.Date
     Test*:能夠表示TestBase,也能夠表示TestDervied
     java..*:表示java任意子類
     java..*Model+:表示Java任意package中名字以Model結尾的子類,好比TabelModel,TreeModel
     等
  最後來看函數的參數。參數匹配比較簡單,主要是參數類型,好比:
     (int, char):表示參數只有兩個,而且第一個參數類型是int,第二個參數類型是char
     (String, ..):表示至少有一個參數。而且第一個參數類型是String,後面參數類型不限。在參數匹配中,
     ..表明任意參數個數和類型
     (Object ...):表示不定個數的參數,且類型都是Object,這裏的...不是通配符,而是Java中表明不定參數的意思

是否是很簡單呢?

Constructorsignature和Method Signature相似,只不過構造函數沒有返回值,並且函數名必須叫new。好比:
public *..TestDerived.new(..):
Œ  public:選擇public訪問權限
  *..表明任意包名
Ž  TestDerived.new:表明TestDerived的構造函數
  (..):表明參數個數和類型都是任意
再來看Field Signature和Type Signature,用它們的地方見圖5。下面直接上幾個例子:
Field Signature標準格式:
@註解 訪問權限 類型 類名.成員變量名
Œ  其中,@註解和訪問權限是可選的
  類型:成員變量類型,*表明任意類型
Ž  類名.成員變量名:成員變量名能夠是*,表明任意成員變量
好比,
set(inttest..TestBase.base):表示設置TestBase.base變量時的JPoint
Type Signature:直接上例子
staticinitialization(test..TestBase):表示TestBase類的static block
handler(NullPointerException):表示catch到NullPointerException的JPoint。注意,圖2的源碼第23行截獲的實際上是Exception,其真實類型是NullPointerException。可是因爲JPointer的查詢匹配是靜態的,即編譯過程當中進行的匹配,因此handler(NullPointerException)在運行時並不能真正被截獲。只有改爲handler(Exception),或者把源碼第23行改爲NullPointerException才行。

以上例子,讀者均可以在aspectj-test例子中本身都試試。

(3) 間接針對JPoint的選擇

除了根據前面提到的Signature信息來匹配JPoint外,AspectJ還提供其餘一些選擇方法來選擇JPoint。好比某個類中的全部JPoint,每個函數執行流程中所包含的JPoint。

特別強調,不論什麼選擇方法,最終都是爲了找到目標的JPoint。

表2列出了一些經常使用的非JPoint選擇方法:

表2  其它經常使用選擇方法

關鍵詞

說明

 

示例

within(TypePattern)

TypePattern標示package或者類。TypePatter可使用通配符

表示某個Package或者類中的全部JPoint。好比

within(Test):Test類中(包括內部類)全部JPoint。圖2所示的例子就是用這個方法。

withincode(Constructor Signature|Method Signature)

表示某個構造函數或其餘函數執行過程當中涉及到的JPoint

好比

withinCode(* TestDerived.testMethod(..))

表示testMethod涉及的JPoint

withinCode( *.Test.new(..))

表示Test構造函數涉及的JPoint

cflow(pointcuts)

cflow是call flow的意思

cflow的條件是一個pointcut

好比

cflow(call TestDerived.testMethod):表示調用TestDerived.testMethod函數時所包含的JPoint,包括testMethod的call這個JPoint自己

cflowbelow(pointcuts)

cflow是call flow的意思。

好比

cflowblow(call TestDerived.testMethod):表示調用TestDerived.testMethod函數時所包含的JPoint,包括testMethod的call這個JPoint自己

this(Type)

JPoint的this對象是Type類型。

(其實就是判斷Type是否是某種類型,便是否知足instanceof Type的條件)

JPoint是代碼段(不管是函數,異常處理,static block),從語法上說,它都屬於一個類。若是這個類的類型是Type標示的類型,則和它相關的JPoint將所有被選中。

圖2示例的testMethod是TestDerived類。因此

this(TestDerived)將會選中這個testMethod JPoint

target(Type)

JPoint的target對象是Type類型

和this相對的是target。不過target通常用在call的狀況。call一個函數,這個函數可能定義在其餘類。好比testMethod是TestDerived類定義的。那麼

target(TestDerived)就會搜索到調用testMethod的地方。可是不包括testMethod的execution JPoint

args(TypeSignature)

用來對JPoint的參數進行條件搜索的

好比args(int,..),表示第一個參數是int,後面參數個數和類型不限的JPoint。

 

上面這些東西,建議讀者:

  • l 進入androidaopdemo/aspectj-test目錄。
  • l 修改test/TestAspect.aj文件。主要是其中的pointcuts:testAll()這一行。按照圖2中的解釋說明,隨便改改試試。
  • l 執行./create-jar.sh,獲得一個test.jar包,而後java -jar test.jar獲得執行結果

 

注意:this()和target()匹配的時候不能使用通配符。

圖6給出了修改示例和輸出:

圖6  示例代碼和輸出結果

注意,不是全部的AOP實現都支持本節所說的查詢條件。好比Spring就不支持withincode查詢條件。

3.2.3  advice和aspect介紹

恭喜,看到這個地方來,AspectJ的核心部分就掌握一大部分了。如今,咱們知道如何經過pointcuts來選擇合適的JPoint。那麼,下一步工做就很明確了,選擇這些JPoint後,咱們確定是須要幹一些事情的。好比前面例子中的輸出都有before,after之類的。這其實JPoint在執行前,執行後,都執行了一些咱們設置的代碼。在AspectJ中,這段代碼叫advice。簡單點說,advice就是一種Hook

ASpectJ中有好幾個Hook,主要是根據JPoint執行時機的不一樣而不一樣,好比下面的:

before():testAll(){
   System.out.println("before calling: " + thisJoinPoint);//打印這個JPoint的信息
  System.out.println("      at:" + thisJoinPoint.getSourceLocation());//打印這個JPoint對應的源代碼位置
}

testAll()是前面定義的pointcuts,而before()定義了在這個pointcuts選中的JPoint執行前咱們要乾的事情。

表3列出了AspectJ所支持的Advice的類型:

表3  advice的類型

關鍵詞

說明

 

示例

before()

before advice

表示在JPoint執行以前,須要乾的事情

after()

after advice

表示JPoint本身執行完了後,須要乾的事情。

after():returning(返回值類型)

after():throwing(異常類型)

returning和throwing後面均可以指定具體的類型,若是不指定的話則匹配的時候不限定類型

假設JPoint是一個函數調用的話,那麼函數調用執行完有兩種方式退出,一個是正常的return,另一個是拋異常。

注意,after()默認包括returning和throwing兩種狀況

返回值類型 around()

before和around是指JPoint執行前或執行後備觸發,而around就替代了原JPoint

around是替代了原JPoint,若是要執行原JPoint的話,須要調用proceed

注意,after和before沒有返回值,可是around的目標是替代原JPoint的,因此它通常會有返回值,並且返回值的類型須要匹配被選中的JPoint。咱們來看個例子,見圖7。

圖7  advice示例和結果

圖7中:

 

  • l 第一個紅框是修改後的testMethod,在這個testMethod中,確定會拋出一個空指針異常。
  • l 第二個紅框是咱們配置的advice,除了before之外,還加了一個around。咱們重點來看around,它的返回值是Object。雖然匹配的JPoint是testMethod,其定義的返回值是void。可是AspectJ考慮的很周到。在around裏,能夠設置返回值類型爲Object來表示返回任意類型的返回值。AspectJ在真正返回參數的時候,會自動進行轉換。好比,假設inttestMethod定義了int做爲返回值類型,咱們在around裏能夠返回一個Integer,AspectJ會自動轉換成int做爲返回值。
  • l 再看around中的//proceed()這句話。這表明調用真正的JPoint函數,即testMethod。因爲這裏咱們屏蔽了proceed,因此testMethod真正的內容並未執行,故運行的時候空指針異常就不會拋出來。也就是說,咱們徹底截獲了testMethod的運行,甚至能夠任意修改它,讓它執行別的函數都沒有問題。

 

注意:從技術上說,around是徹底能夠替代before和after的。圖7中第二個紅框還把after給註釋掉了。若是不註釋掉,編譯時候報錯,[error]circular advice precedence: can't determine precedence between two or morepieces of advice that apply to the same join point: method-execution(voidtest.Test$TestDerived.testMethod())(你們能夠本身試試)。我猜想其中的緣由是around和after衝突了。around本質上表明瞭目標JPoint,好比此處的testMethod。而after是testMethod以後執行。那麼這個testMethod究竟是around仍是原testMethod呢?真是傻傻分不清楚!

(我以爲再加一些限制條件給after是能夠避免這個問題的,可是沒搞成功...)

advice講完了。如今回顧下3.2節從開始到如今咱們學到了哪些內容:

 

  • l AspectJ中各類類型的JoinPoint,JPoint是一個程序的關鍵執行點,也是咱們關注的重點。
  • l pointcuts:提供了一種方法來選擇目標JPoint。程序有不少JPoint,可是須要一種方法來讓咱們選擇咱們關注的JPoint。這個方法就是利用pointcuts來完成的。
  • l 經過pointcuts選擇了目標JPoint後,咱們總得乾點什麼吧?這就用上了advice。advice包括好幾種類型,通常狀況下都夠咱們用了。

 

上面這些東西都有點像函數定義,在Java中,這些東西都是要放到一個class裏的。在AspectJ中,也有相似的數據結構,叫aspect。

public aspect 名字 {//aspect關鍵字和class的功能同樣,文件名以.aj結尾
 pointcuts定義...
 advice定義...
}

你看,經過這種方式,定義一個aspect類,就把相關的JPoint和advice包含起來,是否是造成了一個「關注面」?好比:

 

  • l 咱們定義一個LogAspect,在LogAspect中,咱們在關鍵JPoint上設置advice,這些advice就是打印日誌
  • l 再定義一個SecurityCheckAspect,在這個Aspect中,咱們在關鍵JPoint上設置advice,這些advice將檢查調用app是否有權限。

 

經過這種方式,咱們在原來的JPoint中,就不須要寫log打印的代碼,也不須要寫權限檢查的代碼了。全部這些關注點都挪到對應的Aspectj文件中來控制。恩,這就是AOP的精髓。

注意,讀者在把玩代碼時候,必定會碰到AspectJ語法不熟悉的問題。因此請讀者記得隨時參考官網的文檔。這裏有一個官方的語法大全:

http://www.eclipse.org/aspectj/doc/released/quick5.pdf 或者官方的另一個文檔也能夠:

http://www.eclipse.org/aspectj/doc/released/progguide/semantics.html

3.2.4  參數傳遞和JPoint信息

(1) 參數傳遞

到此,AspectJ最基本的東西其實講差很少了,可是在實際使用AspectJ的時候,你會發現前面的內容還欠缺一點,尤爲是advice的地方:

l 前面介紹的advice都是沒有參數信息的,而JPoint確定是或多或少有參數的。並且advice既然是對JPoint的截獲或者hook也好,確定須要利用傳入給JPoint的參數乾點什麼事情。比方所around advice,我能夠對傳入的參數進行檢查,若是參數不合法,我就直接返回,根本就不須要調用proceed作處理。

往advice傳參數比較簡單,就是利用前面提到的this(),target(),args()等方法。另外,整個pointcuts和advice編寫的語法也有一些區別。具體方法以下:

Œ  先在pointcuts定義時候指定參數類型和名字

pointcut testAll(Test.TestDerived derived,int x):call(*Test.TestDerived.testMethod(..))
             && target(derived)&& args(x)

  注意上述pointcuts的寫法,首先在testAll中定義參數類型和參數名。這一點和定義一個函數徹底同樣

Ž  接着看target和args。此處的target和args括號中用得是參數名。而參數名則是在前面pointcuts中定義好的。這屬於target和args的另一種用法。

  注意,增長參數並不會影響pointcuts對JPoint的匹配,上面的pointcuts選擇和

pointcut testAll():call(*Test.TestDerived.testMethod(..)) && target(Test.TestDerived) &&args(int)是同樣的

只不過咱們須要把參數傳入advice,才須要改造

接下來是修改advice:

Object around(Test.TestDerived derived,int x):testAll(derived,x){
     System.out.println("     arg1=" + derived);
     System.out.println("     arg2=" + x);
      return proceed(derived,x); //注意,proceed就必須把全部參數傳進去。
}

advice的定義如今也和函數定義同樣,把參數類型和參數名傳進來。

‘  接着把參數名傳給pointcuts,此處是testAll。注意,advice必須和使用的pointcuts在參數類型和名字上保持一致。

’  而後在advice的代碼中,你就能夠引用參數了,好比derived和x,均可以打印出來。

總結,參數傳遞其實並不複雜,關鍵是得記住語法:

 

  • l pointcuts修改:像定義函數同樣定義pointcuts,而後在this,target或args中綁定參數名(注意,再也不是參數類型,而是參數名)。
  • l advice修改:也像定義函數同樣定義advice,而後在冒號後面的pointcuts中綁定參數名(注意是參數名)
  • l 在advice的代碼中使用參數名。

 

(2) JoinPoint信息收集

咱們前面示例中都打印出了JPoint的信息,好比當前調用的是哪一個函數,JPoint位於哪一行代碼。這些都屬於JPoint的信息。AspectJ爲咱們提供以下信息:

 

  • l thisJoinpoint對象:在advice代碼中可直接使用。表明JPoint每次被觸發時的一些動態信息,好比參數啊之類的、
  • l thisJoinpointStatic對象:在advice代碼中可直接使用,表明JPoint中那些不變的東西。好比這個JPoint的類型,JPoint所處的代碼位置等。
  • l thisEnclosingJoinPointStaticPart對象:在advice代碼中可直接使用。也表明JPoint中不可變的部分,可是它包含的東西和JPoint的類型有關,好比對一個call類型JPoint而言,thisEnclosingJoinPointStaticPart表明包含調用這個JPoint的函數的信息。對一個handler類型的JPoint而言,它表明包含這個try/catch的函數的信息。

 

關於thisJoinpoint,建議你們直接查看API文檔,很是簡單。其地址位於http://www.eclipse.org/aspectj/doc/released/runtime-api/index.html。

 

4、使用AOP的例子

如今正式回到咱們的AndroidAopDemo這個例子來。咱們的目標是爲AopDemoActivity的幾個Activity生命週期函數加上log,另外爲checkPhoneState加上權限檢查。一切都用AOP來集中控制。

前面提到說AspectJ須要編寫aj文件,而後把AOP代碼放到aj文件中。可是在Android開發中,我建議不要使用aj文件。由於aj文件只有AspectJ編譯器才認識,而Android編譯器不認識這種文件。因此當更新了aj文件後,編譯器認爲源碼沒有發生變化,因此不會編譯它。

固然,這種問題在其餘不認識aj文件的java編譯環境中也存在。因此,AspectJ提供了一種基於註解的方法來把AOP實現到一個普通的Java文件中。這樣咱們就把AOP當作一個普通的Java文件來編寫、編譯就好。

4.1  打印Log

立刻來看AopDemoActivity對應的DemoAspect.java文件吧。先看輸出日誌初版本:

[-->初版本]

package com.androidaop.demo;
import android.util.Log;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.JoinPoint;
 
@Aspect   //必須使用@AspectJ標註,這樣class DemoAspect就等同於 aspect DemoAspect了
public class DemoAspect {
    staticfinal String TAG = "DemoAspect";
/*
@Pointcut:pointcut也變成了一個註解,這個註解是針對一個函數的,好比此處的logForActivity()
其實它表明了這個pointcut的名字。若是是帶參數的pointcut,則把參數類型和名字放到
表明pointcut名字的logForActivity中,而後在@Pointcut註解中使用參數名。
基本和之前同樣,只是寫起來比較奇特一點。後面咱們會介紹帶參數的例子
*/
@Pointcut("execution(* com.androidaop.demo.AopDemoActivity.onCreate(..)) ||"
        +"execution(* com.androidaop.demo.AopDemoActivity.onStart(..))")
public void logForActivity(){};  //注意,這個函數必需要有實現,不然Java編譯器會報錯
 
/*
@Before:這就是Before的advice,對於after,after -returning,和after-throwing。對於的註解格式爲
@After,@AfterReturning,@AfterThrowing。Before後面跟的是pointcut名字,而後其代碼塊由一個函數來實現。好比此處的log。
*/
    @Before("logForActivity()")
    public void log(JoinPoint joinPoint){
       //對於使用Annotation的AspectJ而言,JoinPoint就不能直接在代碼裏獲得多了,而須要經過
      //參數傳遞進來。
       Log.e(TAG, joinPoint.toShortString());
    }
}
 

提示:若是開發者已經切到AndroidStudio的話,AspectJ註解是能夠被識別並能自動補齊。

上面的例子僅僅是列出了onCreate和onStart兩個函數的日誌,若是想在全部的onXXX這樣的函數里加上log,該怎麼改呢?

@Pointcut("execution(* *..AopDemoActivity.on*(..))")
public void logForActivity(){};

圖8給出這個例子的執行結果:

圖8  AopDemoActivity執行結果

4.2  檢查權限

4.2.1  使用註解

檢查權限這個功能的實現也能夠採用剛纔打印log那樣,可是這樣就沒有太多意思了。咱們玩點高級的。不過這個高級的玩法也是來源於現實需求:

 

  • l 權限檢查通常是針對API的,好比調用者是否有權限調用某個函數。
  • l API每每是經過SDK發佈的。通常而言,咱們會在這個函數的註釋裏說明須要調用者聲明哪些權限。
  • l 而後咱們在API檢查調用者是否是申明瞭文檔中列出的權限。

 

若是我有10個API,10個不一樣的權限,那麼在10個函數的註釋裏都要寫,太麻煩了。怎麼辦?這個時候我想到了註解。註解的本質是源代碼的描述。權限聲明,從語義上來講,實際上是屬於API定義的一部分,兩者是一個統一體,而不是分離的。

Java提供了一些默認的註解,不過此處咱們要使用本身定義的註解:

package com.androidaop.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
//第一個@Target表示這個註解只能給函數使用
//第二個@Retention表示註解內容須要包含的Class字節碼裏,屬於運行時須要的。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityCheckAnnotation {//@interface用於定義一個註解。
    publicString declaredPermission();  //declarePermssion是一個函數,其實表明了註解裏的參數
}
怎麼使用註解呢?接着看代碼:
//爲checkPhoneState使用SecurityCheckAnnotation註解,並指明調用該函數的人須要聲明的權限
   @SecurityCheckAnnotation(declaredPermission="android.permission.READ_PHONE_STATE")
   private void checkPhoneState(){
        //若是不使用AOP,就得本身來檢查權限
       if(checkPermission("android.permission.READ_PHONE_STATE") ==false){
           Log.e(TAG,"have no permission to read phone state");
           return;
        }
       Log.e(TAG,"Read Phone State succeed");
       return;
    }

4.2.2  檢查權限

下面,咱們來看看如何在AspectJ中,充分利用這注解信息來幫助咱們檢查權限。

/*
來看這個Pointcut,首先,它在選擇Jpoint的時候,把@SecurityCheckAnnotation使用上了,這代表全部那些public的,而且攜帶有這個註解的API都是目標JPoint
接着,因爲咱們但願在函數中獲取註解的信息,全部這裏的poincut函數有一個參數,參數類型是
SecurityCheckAnnotation,參數名爲ann
這個參數咱們須要在後面的advice裏用上,因此pointcut還使用了@annotation(ann)這種方法來告訴
AspectJ,這個ann是一個註解
*/
  @Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)")
  publicvoid checkPermssion(SecurityCheckAnnotationann){};
 
/*
接下來是advice,advice的真正功能由check函數來實現,這個check函數第二個參數就是咱們想要
的註解。在實際運行過程當中,AspectJ會把這個信息從JPoint中提出出來並傳遞給check函數。
*/
   @Before("checkPermssion(securityCheckAnnotation)")
    publicvoid check(JoinPoint joinPoint,SecurityCheckAnnotationsecurityCheckAnnotation){
        //從註解信息中獲取聲明的權限。
       String neededPermission = securityCheckAnnotation.declaredPermission();
       Log.e(TAG, joinPoint.toShortString());
       Log.e(TAG, "\tneeded permission is " + neededPermission);
       return;
    }

如此這般,咱們在API源碼中使用的註解信息,如今就能夠在AspectJ中使用了。這樣,咱們在源碼中定義註釋,而後利用AspectJ來檢查。圖9展現了執行的結果

圖9  權限檢查的例子

4.2.3  和其餘模塊交互

事情這樣就完了?很明顯沒有。爲何?剛纔權限檢查只是簡單得打出了日誌,可是並無真正去作權限檢查。如何處理?這就涉及到AOP如何與一個程序中其餘模塊交互的問題了。初看起來容易,其實有難度。

好比,DemoAspect雖然是一個類,可是沒有構造函數。並且,咱們也沒有在代碼中主動去構造它。根據AsepctJ的說明,DemoAspect不須要咱們本身去構造,AspectJ在編譯的時候會把構造函數給你自動加上。具體在程序什麼位置加上,實際上是有規律的,可是咱們並不知道,也不要去知道。

這樣的話,DemoAspect豈不是除了打打log就沒什麼做用了?非也!以此例的權限檢查爲例,咱們須要:

 

  • l 把真正進行權限檢查的地方封裝到一個模塊裏,好比SecurityCheck中。
  • l SecurityCheck每每在一個程序中只會有一個實例。因此能夠爲它提供一個函數,好比getInstance以獲取SecurityCheck實例對象。
  • l 咱們就能夠在DemoAspect中獲取這個對象,而後調用它的check函數,把最終的工做由SecurityCheck來檢查了。

 

恩,這實際上是Aspect的真正做用,它負責收集Jpoint,設置advice。一些簡單的功能可在Aspect中來完成,而一些複雜的功能,則只是有Aspect來統一收集信息,並交給專業模塊來處理。

最終代碼:

  @Before("checkPermssion(securityCheckAnnotation)")
    publicvoid check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){
       String neededPermission = securityCheckAnnotation.declaredPermission();
       Log.e(TAG, "\tneeded permission is " + neededPermission);
        SecurityCheckManager manager =SecurityCheckManager.getInstanc();
       if(manager.checkPermission(neededPermission) == false){
           throw new SecurityException("Need to declare permission:" + neededPermission);
        }
       return;
    }

圖10所示爲最終的執行結果。

圖10  執行真正的權限檢查

注意,

 

  • 1 全部代碼都放到https://code.csdn.net/Innost/androidaopdemo上了....
  • 2  編譯:請在ubuntu下使用gradle assemble。編譯結果放在out/apps/目錄下。關於gradle的使用,請你們參考個人另一篇重磅文章http://blog.csdn.net/innost/article/details/48228651

5、其餘、總結和參考文獻

最後咱們來說講其餘一些內容。首先是AspectJ的編譯。

5.1  AspectJ編譯

 

  • l AspectJ比較強大,除了支持對source文件(即aj文件、或@AspectJ註解的Java文件,或普通java文件)直接進行編譯外,
  • l 還能對Java字節碼(即對class文件)進行處理。有感興趣的同窗能夠對aspectj-test小例子的class文件進行反編譯,你會發現AspectJ無非是在被選中的JPoint的地方加一些hook函數。固然Before就是在調用JPoint以前加,After就是在JPoint返回以前加。
  • l 更高級的作法是當class文件被加載到虛擬機後,由虛擬機根據AOP的規則進行hook。

 

在Android裏邊,咱們用得是第二種方法,即對class文件進行處理。來看看代碼:

//AndroidAopDemo.build.gradle
//此處是編譯一個App,因此使用的applicationVariants變量,不然使用libraryVariants變量
//這是由Android插件引入的。因此,須要import com.android.build.gradle.AppPlugin;
android.applicationVariants.all { variant ->
   /*
     這段代碼之意是:
     當app編譯個每一個variant以後,在javaCompile任務的最後添加一個action。此action
     調用ajc函數,對上一步生成的class文件進行aspectj處理。
   */
    AppPluginplugin = project.plugins.getPlugin(AppPlugin)
   JavaCompile javaCompile = variant.javaCompile
   javaCompile.doLast{
       String bootclasspath =plugin.project.android.bootClasspath.join(File.pathSeparator)
       //ajc是一個函數,位於utils.gradle中
        ajc(bootclasspath,javaCompile)
    }
}

ajc函數其實和咱們手動試玩aspectj-test目標同樣,只是咱們沒有直接調用ajc命令,而是利用AspectJ提供的API作了和ajc命令同樣的事情。

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
 
def ajc(String androidbootClassFiles,JavaCompile javaCompile){
   String[] args = ["-showWeaveInfo",
                     "-1.8", //1.8是爲了兼容java 8。請根據本身java的版本合理設置它
                     "-inpath",javaCompile.destinationDir.toString(),
                     "-aspectpath",javaCompile.classpath.asPath,
                     "-d",javaCompile.destinationDir.toString(),
                     "-classpath",javaCompile.classpath.asPath,
                    "-bootclasspath", androidbootClassFiles]
    MessageHandlerhandler = new MessageHandler(true);
    new Main().run(args,handler)
 
    deflog = project.logger
    for(IMessage message : handler.getMessages(null, true)) {
     switch (message.getKind()) {
       case IMessage.ABORT:
       case IMessage.ERROR:
       case IMessage.FAIL:
         log.error message.message, message.thrown
         throw message.thrown
         break;
       case IMessage.WARNING:
       case IMessage.INFO:
         log.info message.message, message.thrown
         break;
       case IMessage.DEBUG:
         log.debug message.message, message.thrown
         break;
      }
    }
  }

主要利用了https://eclipse.org/aspectj/doc/released/devguide/ajc-ref.html中TheAspectJ compiler API一節的內容。因爲代碼已經在csdn git上,你們下載過來直接用便可。

5.2  總結

除了hook以外,AspectJ還能夠爲目標類添加變量。另外,AspectJ也有抽象,繼承等各類更高級的玩法。根據本文前面的介紹,這些高級玩法必定要靠需求來驅動。AspectJ確定對原程序是有影響的,如若貿然使用高級用法,則可能帶來一些未知的後果。關於這些內容,讀者根據狀況自行閱讀文後所列的參考文獻。

最後再來看一個圖。

圖11 未使用AOP的狀況

圖11中,左邊是一個程序的三個基於OOP而劃分的模塊(也就是concern)。安全、業務邏輯、交易管理。這三個模塊在設計圖上必定是互相獨立,互不干擾的。

可是在右圖實現的時候,這三個模塊就攪在一塊兒了。這和咱們在AndroidAopDemo中檢查權限的例子中徹底同樣。在業務邏輯的時候,須要顯示調用安全檢查模塊。

自從有了AOP,咱們就能夠去掉業務邏輯中顯示調用安全檢查的內容,使得代碼歸於乾淨,各個模塊又能各司其職。而這之中千絲萬縷的聯繫,都由AOP來鏈接和管理,豈不美哉?!

5.3  參考文獻

[1]  Manning.AspectJ.in.Action第二版

看書仍是要挑簡單易懂的,AOP概念並不複雜,而AspectJ也有不少書,可是真正寫得通俗易懂的就是這本,雖然它本意是介紹Spring中的AOP,但對AspectJ的解釋真得是很是到位,並且還有對@AspectJ註解的介紹。本文除第一個圖外,其餘參考用圖全是來自於此書。

[2]  http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/

Android中如何使用AspectJ,最重要的是它教會咱們怎麼使用aspectj編譯工具API。

 

[1] 關於Groovy更多的故事,請閱讀《深刻理解Android之Gradle》http://blog.csdn.net/innost/article/details/48228651

相關文章
相關標籤/搜索