13.3.1 AOP
1. 簡(jiǎn)介
OOP(Object Oriented Programming)面向?qū)ο缶幊唐┲亍T贠OP的世界中,問(wèn)題或者功能都被劃分到一個(gè)一個(gè)的模塊里邊卿操。每個(gè)模塊專(zhuān)心干自己的事情扁达,模塊之間通過(guò)設(shè)計(jì)好的接口交互。如下圖就是Android Framework中的模塊:
OOP的精髓是把++功能或問(wèn)題模塊化碴倾,每個(gè)模塊處理自己的家務(wù)事++。但在現(xiàn)實(shí)世界中掉丽,并不是所有問(wèn)題都能完美得劃分到模塊中跌榔。舉個(gè)最簡(jiǎn)單而又常見(jiàn)的例子:現(xiàn)在想為每個(gè)模塊加上日志功能,要求模塊運(yùn)行時(shí)候能輸出日志捶障。在不知道AOP的情況下僧须,一般的處理都是:先設(shè)計(jì)一個(gè)日志輸出模塊,這個(gè)模塊提供日志輸出API项炼,比如Android中的Log類(lèi)担平。然后,其他模塊需要輸出日志的時(shí)候調(diào)用Log類(lèi)的幾個(gè)方法锭部。這種方式功能是得到了滿足暂论,但是好像沒(méi)有Oriented的感覺(jué)了。是的拌禾,隨意加日志輸出功能取胎,使得其他模塊的代碼和日志模塊耦合非常緊密。而且蹋砚,將來(lái)要是日志模塊修改了API扼菠,則使用它們的地方都得改。
AOP(Aspect Oriented Programming)面向切向編程坝咐。AOP的目標(biāo)==是把這些分散在各個(gè)模塊中的功能集中起來(lái)循榆,放到一個(gè)統(tǒng)一的地方來(lái)控制和管理==。如果說(shuō)墨坚,OOP如果是把問(wèn)題劃分到單個(gè)模塊的話秧饮,那么AOP就是把涉及到眾多模塊的某一類(lèi)問(wèn)題進(jìn)行統(tǒng)一管理映挂。比如我們可以設(shè)計(jì)兩個(gè)Aspects,一個(gè)是管理某個(gè)軟件中所有模塊的日志輸出的功能盗尸,另外一個(gè)是管理該軟件中一些特殊函數(shù)調(diào)用的權(quán)限檢查柑船。
2. AspectJ
AOP是一種思想,就好像OOP中的Java一樣泼各,一些先行者也開(kāi)發(fā)了一套語(yǔ)言來(lái)支持AOP鞍时。目前用得比較火的就是AspectJ了,它是一種幾乎和Java完全一樣的==語(yǔ)言==扣蜻,而且完全兼容Java逆巍。當(dāng)然,除了使用AspectJ特殊的語(yǔ)言外莽使,AspectJ還支持原生的Java锐极,只要加上對(duì)應(yīng)的AspectJ注解就好。所以芳肌,使用AspectJ有兩種方法:
- 完全使用AspectJ的語(yǔ)言灵再。這語(yǔ)言一點(diǎn)也不難,和Java幾乎一樣亿笤,也能在AspectJ中調(diào)用Java的任何類(lèi)庫(kù)翎迁。AspectJ只是多了一些關(guān)鍵詞罷了。
- 或者使用純Java語(yǔ)言開(kāi)發(fā)责嚷,然后使用AspectJ注解鸳兽,簡(jiǎn)稱(chēng)@AspectJ。(推薦)
不論哪種方法罕拂,最后都需要AspectJ的編譯工具ajc來(lái)編譯。由于AspectJ實(shí)際上脫胎于Java全陨,所以ajc工具也能編譯java源碼爆班。接下來(lái)介紹下幾個(gè)概念。
(1) Join Points
Join Points(以后簡(jiǎn)稱(chēng)==JPoints==)是AspectJ中最關(guān)鍵的一個(gè)概念辱姨,JPoints就是==程序運(yùn)行時(shí)的一些執(zhí)行點(diǎn)==柿菩。那么,一個(gè)程序中雨涛,哪些執(zhí)行點(diǎn)是JPoints呢枢舶?比如:
- 一個(gè)函數(shù)的調(diào)用可以是一個(gè)JPoint。比如Log.e()這個(gè)函數(shù)替久。e的執(zhí)行可以是一個(gè)JPoint凉泄,而調(diào)用e的函數(shù)也可以認(rèn)為是一個(gè)JPoint。
- 設(shè)置一個(gè)變量蚯根,或者讀取一個(gè)變量后众,也可以是一個(gè)JPoint。比如Demo類(lèi)中有一個(gè)disable的boolean變量。設(shè)置它的地方或者讀取它的地方都可以看做是JPoints蒂誉。
- for循環(huán)可以看做是JPoint教藻。
理論上說(shuō),一個(gè)程序中很多地方都可以被看做是JPoint右锨,但是AspectJ中括堤,只有如表1所示的幾種執(zhí)行點(diǎn)被認(rèn)為是JPoints:
Join Points | 說(shuō)明 | 示例 |
---|---|---|
method call | 函數(shù)調(diào)用 | 比如調(diào)用Log.e(),這是一處JPoint |
method execution | 函數(shù)執(zhí)行 | 比如Log.e()的執(zhí)行內(nèi)部绍移,是一處JPoint |
constructor call | 構(gòu)造函數(shù)調(diào)用 | 和method call類(lèi)似 |
constructor execution | 構(gòu)造函數(shù)執(zhí)行 | 和method execution類(lèi)似 |
field get | 獲取變量 | 比如讀取DemoActivity.debug成員 |
field set | 設(shè)置變量 | 比如設(shè)置DemoActivity.debug成員 |
static initialization | 類(lèi)初始化 | 比如類(lèi)的static{} |
handler | 異常處理 | 比如try catch(xxx)中悄窃,對(duì)應(yīng)catch內(nèi)的執(zhí)行 |
(2) Pointcuts
一個(gè)程序會(huì)有很多的JPoints,即使是同一個(gè)函數(shù)(比如testMethod這個(gè)函數(shù))登夫,還分為==call類(lèi)型和execution類(lèi)型==的JPoint广匙。顯然,不是所有的JPoint恼策,也不是所有類(lèi)型的JPoint都是我們關(guān)注的鸦致。比如我們只要求在Activity的幾個(gè)生命周期函數(shù)中打印日志,只有這幾個(gè)生命周期函數(shù)才是我們業(yè)務(wù)需要的JPoint涣楷,而其他的什么JPoint我不需要關(guān)注分唾。
怎么從一堆一堆的JPoints中選擇自己想要的JPoints呢?恩狮斗,這就是Pointcuts的功能绽乔。一句話,==Pointcuts的目標(biāo)是提供一種方法使得開(kāi)發(fā)者能夠選擇自己感興趣的JoinPoints==碳褒。
(i) 一個(gè)Pointcuts例子
@Pointcut("call(public * *.println(..)) && !within(TestAspect)")
- call(public * *.println(..))是一種選擇條件折砸。call表示我們選擇的Joinpoint類(lèi)型為call類(lèi)型。
- public * *.println(..):這小行代碼使用了通配符沙峻。由于我們這里選擇的JoinPoint類(lèi)型為call類(lèi)型睦授,它對(duì)應(yīng)的目標(biāo)JPoint一定是某個(gè)函數(shù)。所以我們要找到這個(gè)/些函數(shù)摔寨。public 表示目標(biāo)JPoint的訪問(wèn)類(lèi)型(public/private/protect)去枷。第一個(gè)*表示返回值的類(lèi)型是任意類(lèi)型。第二個(gè)*用來(lái)指明包名是复。此處不限定包名删顶。緊接其后的println是函數(shù)名。這表明我們選擇的函數(shù)是任何包中定義的名字叫println的函數(shù)淑廊。當(dāng)然逗余,唯一確定一個(gè)函數(shù)除了包名外,還有它的參數(shù)蒋纬。在(..)中猎荠,就指明了目標(biāo)函數(shù)的參數(shù)應(yīng)該是什么樣子的坚弱。比如這里使用了通配符..,代表任意個(gè)數(shù)的參數(shù)关摇,任意類(lèi)型的參數(shù)荒叶。
- call后面的&&:AspectJ可以把幾個(gè)條件組合起來(lái),目前支持 &&输虱,||些楣,以及!這三個(gè)條件宪睹。
- !within(TestAspectJ):前面的!表示不滿足某個(gè)條件愁茁。within是另外一種類(lèi)型選擇方法,特別注意亭病,這種類(lèi)型和前面講到的joinpoint的那幾種類(lèi)型不同鹅很。within的類(lèi)型是數(shù)據(jù)類(lèi)型,而joinpoint的類(lèi)型更像是動(dòng)態(tài)的罪帖,執(zhí)行時(shí)的類(lèi)型促煮。
上例中的pointcut合起來(lái)就是:
- 選擇那些調(diào)用println(而且不考慮println函數(shù)的參數(shù)是什么)的Joinpoint。
- 調(diào)用者的類(lèi)型不要是TestAspect的整袁。
(ii) 直接針對(duì)JoinPoint的選擇
pointcuts中最常用的選擇條件和Joinpoint的類(lèi)型密切相關(guān):
一個(gè)==Method Signature==的完整表達(dá)式為:@注解 訪問(wèn)權(quán)限 返回值類(lèi)型 包名.函數(shù)名(參數(shù))
- @注解和訪問(wèn)權(quán)限(public/private/protect菠齿,以及static/final)屬于可選項(xiàng)。如果不設(shè)置它們那么public坐昙,private绳匀,protect及static、final的函數(shù)都會(huì)進(jìn)行搜索炸客。
- 返回值類(lèi)型就是普通的函數(shù)的返回值類(lèi)型疾棵。如果不限定類(lèi)型的話,就用*通配符表示
- 包名.函數(shù)名用于查找匹配的函數(shù)痹仙÷穑可以使用通配符,包括*和..以及+號(hào)蝶溶。其中*號(hào)用于匹配除.號(hào)之外的任意字符,而..則表示任意子package宣渗,+號(hào)表示子類(lèi)抖所。
java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase痕囱,也可以表示TestDervied
java..*:表示java任意子類(lèi)
java..*Model+:表示Java任意package中名字以Model結(jié)尾的子類(lèi)田轧,比如TabelModel,TreeModel等
- 最后來(lái)看函數(shù)的參數(shù)鞍恢。參數(shù)匹配比較簡(jiǎn)單傻粘,主要是參數(shù)類(lèi)型每窖。
(int, char):表示參數(shù)只有兩個(gè),并且第一個(gè)參數(shù)類(lèi)型是int弦悉,第二個(gè)參數(shù)類(lèi)型是char
(String, ..):表示至少有一個(gè)參數(shù)窒典。并且第一個(gè)參數(shù)類(lèi)型是String,后面參數(shù)類(lèi)型不限稽莉。在參數(shù)匹配中瀑志,
..代表任意參數(shù)個(gè)數(shù)和類(lèi)型
(Object ...):表示不定個(gè)數(shù)的參數(shù),且類(lèi)型都是Object污秆,這里的...不是通配符劈猪,而是Java中代表不定參數(shù)的意思
(iii) 間接針對(duì)JPoint的選擇
除了根據(jù)前面提到的Signature信息來(lái)匹配JPoint外,AspectJ還提供其他一些選擇方法來(lái)選擇JPoint良拼。比如某個(gè)類(lèi)中的所有JPoint战得,每一個(gè)函數(shù)執(zhí)行流程中所包含的JPoint。
下表列出了一些常用的非JPoint選擇方法:
關(guān)鍵詞 | 說(shuō)明 | 示例 |
---|---|---|
within(TypePattern) | TypePattern標(biāo)示package或者類(lèi)庸推,可以使用通配符 | 表示某個(gè)Package或者類(lèi)中的所有JPoint常侦。比如within(Test):Test類(lèi)中(包括內(nèi)部類(lèi))所有JPoint。圖2所示的例子就是用這個(gè)方法予弧。 |
this(Type) | JPoint的this對(duì)象是Type類(lèi)型刮吧。(其實(shí)就是判斷Type是不是某種類(lèi)型,即是否滿足instanceof Type的條件) | JPoint是代碼段(不論是函數(shù)掖蛤,異常處理杀捻,static block),從語(yǔ)法上說(shuō)蚓庭,它都屬于一個(gè)類(lèi)致讥。如果這個(gè)類(lèi)的類(lèi)型是Type標(biāo)示的類(lèi)型,則和它相關(guān)的JPoint將全部被選中器赞。圖2示例的testMethod是TestDerived類(lèi)垢袱。所以this(TestDerived)將會(huì)選中這個(gè)testMethod JPoint |
target(Type) | JPoint的target對(duì)象是Type類(lèi)型 | 和this相對(duì)的是target。不過(guò)target一般用在call的情況港柜。call一個(gè)函數(shù)请契,這個(gè)函數(shù)可能定義在其他類(lèi)。比如testMethod是TestDerived類(lèi)定義的夏醉。那么target(TestDerived)就會(huì)搜索到調(diào)用testMethod的地方爽锥。但是不包括testMethod的execution JPoint |
args(TypeSignature) | 用來(lái)對(duì)JPoint的參數(shù)進(jìn)行條件搜索的 | 比如args(int,..),表示第一個(gè)參數(shù)是int畔柔,后面參數(shù)個(gè)數(shù)和類(lèi)型不限的JPoint氯夷。 |
(3) advice
現(xiàn)在,我們知道如何通過(guò)pointcuts來(lái)選擇合適的JPoint靶擦。那么腮考,下一步工作就是選擇這些JPoint后雇毫,需要干一些事情的。比如前面例子中的輸出都有before踩蔚,after之類(lèi)的棚放。這其實(shí)JPoint在執(zhí)行前,執(zhí)行后寂纪,都執(zhí)行了一些我們?cè)O(shè)置的代碼席吴。在AspectJ中,這段代碼叫advice捞蛋。簡(jiǎn)單點(diǎn)說(shuō)孝冒,advice就是一種Hook。
關(guān)鍵詞 | 說(shuō)明 |
---|---|
before() | 表示在JPoint執(zhí)行之前拟杉,需要干的事情 |
after() | 表示JPoint自己執(zhí)行完了后庄涡,需要干的事情 |
返回值類(lèi)型 around() | ==替代了原JPoint==,如果要執(zhí)行原JPoint的話搬设,需要調(diào)用proceed |
(4) 例子
(1) 示例一
@Aspect //必須使用@AspectJ標(biāo)注
public class DemoAspect {
static final String TAG = "DemoAspect";
/*
@Pointcut:定義一個(gè)pointcut穴店,這個(gè)注解是針對(duì)一個(gè)函數(shù)的,比如此處的logForActivity()
其實(shí)它代表了這個(gè)pointcut的名字拿穴。如果是帶參數(shù)的pointcut泣洞,則把參數(shù)類(lèi)型和名字放到
代表pointcut名字的logForActivity中,然后在@Pointcut注解中使用參數(shù)名默色。
*/
@Pointcut("execution(* com.androidaop.demo.AopDemoActivity.onCreate(..)) ||"
+"execution(* com.androidaop.demo.AopDemoActivity.onStart(..))")
public void logForActivity(){}; //注意球凰,這個(gè)函數(shù)必須要有實(shí)現(xiàn),否則Java編譯器會(huì)報(bào)錯(cuò)
/*
@Before:這就是Before的advice腿宰。Before后面跟的是pointcut名字呕诉,然后其代碼塊由一個(gè)函數(shù)來(lái)實(shí)現(xiàn)。比如此處的log吃度。
*/
@Before("logForActivity()")
public void log(JoinPoint joinPoint){
//對(duì)于使用Annotation的AspectJ而言甩挫,JoinPoint就不能直接在代碼里得到多了,而需要通過(guò)
//參數(shù)傳遞進(jìn)來(lái)椿每。
Log.e(TAG, joinPoint.toShortString());
}
}
(2) 示例二
//第一個(gè)@Target表示這個(gè)注解只能給函數(shù)使用
//第二個(gè)@Retention表示注解內(nèi)容需要包含的Class字節(jié)碼里伊者,屬于運(yùn)行時(shí)需要的。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityCheckAnnotation {//@interface用于定義一個(gè)注解间护。
publicString declaredPermission(); //declarePermssion是一個(gè)函數(shù)删壮,其實(shí)代表了注解里的參數(shù)
}
怎么使用注解呢?接著看代碼:
//為checkPhoneState使用SecurityCheckAnnotation注解兑牡,并指明調(diào)用該函數(shù)的人需要聲明的權(quán)限
@SecurityCheckAnnotation(declaredPermission="android.permission.READ_PHONE_STATE")
private void checkPhoneState(){
//如果不使用AOP,就得自己來(lái)檢查權(quán)限
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;
}
我們來(lái)看看如何在AspectJ中税灌,充分利用這注解信息來(lái)幫助我們檢查權(quán)限均函。
/*
來(lái)看這個(gè)Pointcut亿虽,首先,它在選擇Jpoint的時(shí)候苞也,把@SecurityCheckAnnotation使用上了洛勉,
這表明所有那些public的,并且攜帶有這個(gè)注解的API都是目標(biāo)JPoint如迟。如果是帶參數(shù)的pointcut收毫,
則把參數(shù)類(lèi)型和名字放到代表pointcut名字的checkPermssion中,然后在@Pointcut注解中使用參數(shù)名殷勘。
接著此再,由于我們希望在函數(shù)中獲取注解的信息,所以這里的poincut函數(shù)有一個(gè)參數(shù)玲销,參數(shù)類(lèi)型是
SecurityCheckAnnotation输拇,參數(shù)名為ann。
這個(gè)參數(shù)我們需要在后面的advice里用上贤斜,所以pointcut還使用了@annotation(ann)這種方法來(lái)告訴
AspectJ,這個(gè)ann是一個(gè)注解
*/
@Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)")
public void checkPermssion(SecurityCheckAnnotation ann){};
/*
接下來(lái)是advice,advice的真正功能由check函數(shù)來(lái)實(shí)現(xiàn)狈蚤,這個(gè)check函數(shù)第二個(gè)參數(shù)就是我們想要
的注解贬堵。在實(shí)際運(yùn)行過(guò)程中,AspectJ會(huì)把這個(gè)信息從JPoint中提出出來(lái)并傳遞給check函數(shù)锁荔。
*/
@Before("checkPermssion(securityCheckAnnotation)")
public void check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){
//從注解信息中獲取聲明的權(quán)限蟀给。
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 {
// 被注解的方法在這一行代碼被執(zhí)行
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");
}
}