13.3.1 AOP
1. 簡介
OOP(Object Oriented Programming)面向對象編程。在OOP的世界中,問題或者功能都被劃分到一個一個的模塊里邊。每個模塊專心干自己的事情,模塊之間通過設計好的接口交互。如下圖就是Android Framework中的模塊:
OOP的精髓是把++功能或問題模塊化,每個模塊處理自己的家務事++。但在現實世界中,并不是所有問題都能完美得劃分到模塊中。舉個最簡單而又常見的例子:現在想為每個模塊加上日志功能,要求模塊運行時候能輸出日志。在不知道AOP的情況下,一般的處理都是:先設計一個日志輸出模塊,這個模塊提供日志輸出API,比如Android中的Log類。然后,其他模塊需要輸出日志的時候調用Log類的幾個方法。這種方式功能是得到了滿足,但是好像沒有Oriented的感覺了。是的,隨意加日志輸出功能,使得其他模塊的代碼和日志模塊耦合非常緊密。而且,將來要是日志模塊修改了API,則使用它們的地方都得改。
AOP(Aspect Oriented Programming)面向切向編程。AOP的目標==是把這些分散在各個模塊中的功能集中起來,放到一個統一的地方來控制和管理==。如果說,OOP如果是把問題劃分到單個模塊的話,那么AOP就是把涉及到眾多模塊的某一類問題進行統一管理。比如我們可以設計兩個Aspects,一個是管理某個軟件中所有模塊的日志輸出的功能,另外一個是管理該軟件中一些特殊函數調用的權限檢查。
2. AspectJ
AOP是一種思想,就好像OOP中的Java一樣,一些先行者也開發了一套語言來支持AOP。目前用得比較火的就是AspectJ了,它是一種幾乎和Java完全一樣的==語言==,而且完全兼容Java。當然,除了使用AspectJ特殊的語言外,AspectJ還支持原生的Java,只要加上對應的AspectJ注解就好。所以,使用AspectJ有兩種方法:
- 完全使用AspectJ的語言。這語言一點也不難,和Java幾乎一樣,也能在AspectJ中調用Java的任何類庫。AspectJ只是多了一些關鍵詞罷了。
- 或者使用純Java語言開發,然后使用AspectJ注解,簡稱@AspectJ。(推薦)
不論哪種方法,最后都需要AspectJ的編譯工具ajc來編譯。由于AspectJ實際上脫胎于Java,所以ajc工具也能編譯java源碼。接下來介紹下幾個概念。
(1) Join Points
Join Points(以后簡稱==JPoints==)是AspectJ中最關鍵的一個概念,JPoints就是==程序運行時的一些執行點==。那么,一個程序中,哪些執行點是JPoints呢?比如:
- 一個函數的調用可以是一個JPoint。比如Log.e()這個函數。e的執行可以是一個JPoint,而調用e的函數也可以認為是一個JPoint。
- 設置一個變量,或者讀取一個變量,也可以是一個JPoint。比如Demo類中有一個disable的boolean變量。設置它的地方或者讀取它的地方都可以看做是JPoints。
- for循環可以看做是JPoint。
理論上說,一個程序中很多地方都可以被看做是JPoint,但是AspectJ中,只有如表1所示的幾種執行點被認為是JPoints:
Join Points | 說明 | 示例 |
---|---|---|
method call | 函數調用 | 比如調用Log.e(),這是一處JPoint |
method execution | 函數執行 | 比如Log.e()的執行內部,是一處JPoint |
constructor call | 構造函數調用 | 和method call類似 |
constructor execution | 構造函數執行 | 和method execution類似 |
field get | 獲取變量 | 比如讀取DemoActivity.debug成員 |
field set | 設置變量 | 比如設置DemoActivity.debug成員 |
static initialization | 類初始化 | 比如類的static{} |
handler | 異常處理 | 比如try catch(xxx)中,對應catch內的執行 |
(2) Pointcuts
一個程序會有很多的JPoints,即使是同一個函數(比如testMethod這個函數),還分為==call類型和execution類型==的JPoint。顯然,不是所有的JPoint,也不是所有類型的JPoint都是我們關注的。比如我們只要求在Activity的幾個生命周期函數中打印日志,只有這幾個生命周期函數才是我們業務需要的JPoint,而其他的什么JPoint我不需要關注。
怎么從一堆一堆的JPoints中選擇自己想要的JPoints呢?恩,這就是Pointcuts的功能。一句話,==Pointcuts的目標是提供一種方法使得開發者能夠選擇自己感興趣的JoinPoints==。
(i) 一個Pointcuts例子
@Pointcut("call(public * *.println(..)) && !within(TestAspect)")
- call(public * *.println(..))是一種選擇條件。call表示我們選擇的Joinpoint類型為call類型。
- public * *.println(..):這小行代碼使用了通配符。由于我們這里選擇的JoinPoint類型為call類型,它對應的目標JPoint一定是某個函數。所以我們要找到這個/些函數。public 表示目標JPoint的訪問類型(public/private/protect)。第一個*表示返回值的類型是任意類型。第二個*用來指明包名。此處不限定包名。緊接其后的println是函數名。這表明我們選擇的函數是任何包中定義的名字叫println的函數。當然,唯一確定一個函數除了包名外,還有它的參數。在(..)中,就指明了目標函數的參數應該是什么樣子的。比如這里使用了通配符..,代表任意個數的參數,任意類型的參數。
- call后面的&&:AspectJ可以把幾個條件組合起來,目前支持 &&,||,以及!這三個條件。
- !within(TestAspectJ):前面的!表示不滿足某個條件。within是另外一種類型選擇方法,特別注意,這種類型和前面講到的joinpoint的那幾種類型不同。within的類型是數據類型,而joinpoint的類型更像是動態的,執行時的類型。
上例中的pointcut合起來就是:
- 選擇那些調用println(而且不考慮println函數的參數是什么)的Joinpoint。
- 調用者的類型不要是TestAspect的。
(ii) 直接針對JoinPoint的選擇
pointcuts中最常用的選擇條件和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中代表不定參數的意思
(iii) 間接針對JPoint的選擇
除了根據前面提到的Signature信息來匹配JPoint外,AspectJ還提供其他一些選擇方法來選擇JPoint。比如某個類中的所有JPoint,每一個函數執行流程中所包含的JPoint。
下表列出了一些常用的非JPoint選擇方法:
關鍵詞 | 說明 | 示例 |
---|---|---|
within(TypePattern) | TypePattern標示package或者類,可以使用通配符 | 表示某個Package或者類中的所有JPoint。比如within(Test):Test類中(包括內部類)所有JPoint。圖2所示的例子就是用這個方法。 |
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。 |
(3) advice
現在,我們知道如何通過pointcuts來選擇合適的JPoint。那么,下一步工作就是選擇這些JPoint后,需要干一些事情的。比如前面例子中的輸出都有before,after之類的。這其實JPoint在執行前,執行后,都執行了一些我們設置的代碼。在AspectJ中,這段代碼叫advice。簡單點說,advice就是一種Hook。
關鍵詞 | 說明 |
---|---|
before() | 表示在JPoint執行之前,需要干的事情 |
after() | 表示JPoint自己執行完了后,需要干的事情 |
返回值類型 around() | ==替代了原JPoint==,如果要執行原JPoint的話,需要調用proceed |
(4) 例子
(1) 示例一
@Aspect //必須使用@AspectJ標注
public class DemoAspect {
static final 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。Before后面跟的是pointcut名字,然后其代碼塊由一個函數來實現。比如此處的log。
*/
@Before("logForActivity()")
public void log(JoinPoint joinPoint){
//對于使用Annotation的AspectJ而言,JoinPoint就不能直接在代碼里得到多了,而需要通過
//參數傳遞進來。
Log.e(TAG, joinPoint.toShortString());
}
}
(2) 示例二
//第一個@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;
}
我們來看看如何在AspectJ中,充分利用這注解信息來幫助我們檢查權限。
/*
來看這個Pointcut,首先,它在選擇Jpoint的時候,把@SecurityCheckAnnotation使用上了,
這表明所有那些public的,并且攜帶有這個注解的API都是目標JPoint。如果是帶參數的pointcut,
則把參數類型和名字放到代表pointcut名字的checkPermssion中,然后在@Pointcut注解中使用參數名。
接著,由于我們希望在函數中獲取注解的信息,所以這里的poincut函數有一個參數,參數類型是
SecurityCheckAnnotation,參數名為ann。
這個參數我們需要在后面的advice里用上,所以pointcut還使用了@annotation(ann)這種方法來告訴
AspectJ,這個ann是一個注解
*/
@Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)")
public void checkPermssion(SecurityCheckAnnotation ann){};
/*
接下來是advice,advice的真正功能由check函數來實現,這個check函數第二個參數就是我們想要
的注解。在實際運行過程中,AspectJ會把這個信息從JPoint中提出出來并傳遞給check函數。
*/
@Before("checkPermssion(securityCheckAnnotation)")
public void check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){
//從注解信息中獲取聲明的權限。
String neededPermission = securityCheckAnnotation.declaredPermission();
Log.e(TAG, joinPoint.toShortString());
Log.e(TAG, "\t needed permission is " + neededPermission);
return;
}
(3) 示例三
@Aspect
public class FragmentAspectj {
private final static String TAG = FragmentAspectj.class.getCanonicalName();
@Around("execution(* android.support.v4.app.Fragment.onCreateView(..))")
public Object fragmentOnCreateViewMethod(ProceedingJoinPoint joinPoint) throws Throwable {
return trackFragmentView(joinPoint);
}
@Around("execution(* android.app.Fragment.onCreateView(..))")
public Object fragmentOnCreateViewMethod2(ProceedingJoinPoint joinPoint) throws Throwable {
return trackFragmentView(joinPoint);
}
private Object trackFragmentView(final ProceedingJoinPoint joinPoint) throws Throwable {
// 被注解的方法在這一行代碼被執行
Object result = joinPoint.proceed();
AopUtil.sendTrackEventToSDK3(joinPoint, "trackFragmentView", result);
return result;
}
@After("execution(* android.support.v4.app.Fragment.onHiddenChanged(boolean))")
public void onHiddenChangedMethod(JoinPoint joinPoint) throws Throwable {
AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentHiddenChangedMethod");
}
@After("execution(* android.support.v4.app.Fragment.setUserVisibleHint(boolean))")
public void setUserVisibleHintMethod(JoinPoint joinPoint) throws Throwable {
AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentSetUserVisibleHintMethod");
}
@After("execution(* android.support.v4.app.Fragment.onResume())")
public void onResumeMethod(JoinPoint joinPoint) throws Throwable {
AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentOnResumeMethod");
}
}