1. 程式人生 > >android面向切面AOP程式設計精華總結

android面向切面AOP程式設計精華總結

AspectJ語法

1.Join Points

JPoints就是程式執行時的一些執行點。
這裡寫圖片描述
最經常使用的是call和execution的執行點。

2.Pointcuts

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

一個Pointcuts例子:

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

第一個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的。

3.Jpoints和pointcut之間的聯絡

這裡寫圖片描述

①直接針對JoinPoint的選擇:

一個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的完整表示式為:

Constructorsignature和Method Signature類似,只不過建構函式沒有返回值,而且函式名必須叫new。比如:
public *..TestDerived.new(..):
Œ public:選擇public訪問許可權
 *..代表任意包名
Ž TestDerived.new:代表TestDerived的建構函式
 (..):代表引數個數和型別都是任意
再來看Field Signature和Type Signature
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才行。

②間接針對JoinPoint的選擇:

這裡寫圖片描述

注意:this()和target()匹配的時候不能使用萬用字元。不是所有的AOP實現都支援本節所說的查詢條件。比如Spring就不支援withincode查詢條件。

4.advice和aspect

①advice

簡單點說,advice就是一種Hook。
ASpectJ中有好幾個Hook,主要是根據JPoint執行時機的不同而不同,比如下面的:
這裡寫圖片描述

注意,after和before沒有返回值,但是around的目標是替代原JPoint的,所以它一般會有返回值,而且返回值的型別需要匹配被選中的JPoint。

(1)往advice傳遞引數:
作用:比方所around advice,我可以對傳入的引數進行檢查,如果引數不合法,我就直接返回,根本就不需要呼叫proceed做處理。
往advice傳引數比較簡單,就是利用前面提到的this(),target(),args()等方法。具體方法如下:
Œ 先在pointcuts定義時候指定引數型別和引數名:

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

此處的target和args括號中用得是引數名。而引數名則是在前面pointcuts中定義好的。
或者:

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

(2)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就必須把所有引數傳進去。
}

這裡寫圖片描述

②aspect

上面這些東西都有點像函式定義,在Java中,這些東西都是要放到一個class裡的。在AspectJ中,也有類似的資料結構,叫aspect。
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的精髓。

綜合前面4點,可以得知,總體的流程是這樣的:
這裡寫圖片描述

5.使用aop的例子

1.列印Log


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(){};  

2.檢查許可權

(1)自定義註解
如果我有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用於定義一個註解。  
    public String 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;  
    }  

(2)檢查許可權


/*
來看這個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;
    }

這裡寫圖片描述
恩,這其實是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;  
  }  

6.AspectJ在Android studio中的配置

在Android裡邊,我們用得是第二種方法,即對class檔案進行處理。
配置總結:
①在原工程下新建一個android library型別的moudle,然後新增AspectJ依賴至module中
compile 'org.aspectj:aspectjrt:1.8.9'
②編寫moudle的gradle指令碼
A。這幾個配置要和原有的app下的配置一致:

 compile 'com.android.support:appcompat-v7:22.2.1'
 compileSdkVersion 22
 buildToolsVersion '23.0.1'

B。在build的過程中,會下載jar包,要開啟vpn進行下載,下載過程沒有進度條提示,只有一個沒有標示進度的滾動橫條,如果順利的話會在10分鐘之內下載build好這個moudle,接下的app的gradle就好辦了。
③編寫app的gradle指令碼
A。特別要注意這個,裡面的名字要和你在①建立的moudle的名字一致

compile project(':aspectjlibrary')

B。這幾個配置還是要和原有的app的gradle保持一致:

 compileSdkVersion 21
 buildToolsVersion '22.0.1'
 minSdkVersion 15
 targetSdkVersion 21

然後直接build app下的這個gradle就可以了,這個就很快了,20秒之內就可以build完成了。