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完成了。