介紹
AspectJ是Java的一個簡單實用的面向方面的擴展。通過幾個新的構造,AspectJ提供了對一系列橫切關注的模塊化實現的支持。
在現有的Java開發項目中采用AspectJ可能是一個簡單而且增量的任務。一條路徑是從開發方面開始,繼續使用生產方面,然后在使用AspectJ建立經驗之后再使用方面。采用也可以遵循其他途徑。例如,一些開發人員將從馬上使用生產方面受益。其他人可能幾乎可以立即編寫干凈的可重用方面。
AspectJ支持基于名稱和基于屬性的橫切。使用基于名稱的橫切的方面傾向于影響少數其他類。但是,盡管規模較小,但與普通的Java實現相比,它們通常可以消除顯著的復雜性。使用基于屬性的橫切的方面可以具有小規模或大規模。
使用AspectJ會導致橫切關注的干凈模塊化的實現。當作為AspectJ方面編寫時,橫切關注的結構是明確的且易于理解的。方面也是高度模塊化的,使得開發橫切功能的即插即用實現成為可能。
基礎知識
Join point 連接點
Join Points | 定義 | 解釋 |
---|---|---|
method call | 調用方法 | 一般在執行某個方法前會先調用該方法 |
method execution | 執行方法 | 這個點是已經執行到了方法的內部 |
constructor call | 調用構造方法 | 同調用方法 |
constructor execution | 執行構造方法 | 同執行方法 |
field get | 獲取參數 | 比如獲取某個變量的值,get() |
field set | 設置參數 | 比如設置某個變量的值,int num = 3 |
pre-initialization | 預初始化 | 在第一次初始化前會預初始化 |
initialization | 初始化 | 初始化類的時候會執行 |
static initialization | 靜態初始化 | 靜態塊或靜態類初始化的時候會執行 |
handler | 異常處理 | |
advice execution | 通知執行 |
是指程序中可能作為代碼注入目標的特定的點,例如一個方法調用或者方法入口。
程序中連接點有很多,下面做一個表格一一指出:
Join Points | 定義 | 解釋 |
---|---|---|
method call | 調用方法 | 一般在執行某個方法前會先調用該方法 |
method execution | 執行方法 | 這個點是已經執行到了方法的內部 |
constructor call | 調用構造方法 | 同調用方法 |
constructor execution | 執行構造方法 | 同執行方法 |
field get | 獲取參數 | 比如獲取某個變量的值,get() |
field set | 設置參數 | 比如設置某個變量的值,int num = 3 |
pre-initialization | 預初始化 | 在第一次初始化前會預初始化 |
initialization | 初始化 | 初始化類的時候會執行 |
static initialization | 靜態初始化 | 靜態塊或靜態類初始化的時候會執行 |
handler | 異常處理 | |
advice execution | 通知執行 |
//這里可以用一個例子來演示一下所有的連接點
Pointcut 切入點
一個程序中會有很多JPoint連接點,但不一定我們都要去關注。那么我們可以選擇我們需要的點來作為切入點。
我們利用Pointcut的功能來篩選出對我們有用的點作為切入,pointcut有一套專門的語法,只要搞懂他后面就不愁了。
一個例子
@Pointcut("within(@com.jie.aoptest.aop.DebugLog *)")
public void withinAnnotatedClass() {}
@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
public void methodInsideAnnotatedType() {}
@Pointcut("execution(@com.jie.aoptest.aop.DebugLog * *(..)) || methodInsideAnnotatedType()")
public void method() {}
這個例子表明切入點在DebugLog類中所有執行方法的點,這里用到了within和execution兩個指示符,within用于匹配指定類型內的方法執行,而exection則用于匹配方法執行的連接點。有synthetic標記的field和method是class內部使用的,正常的源代碼里不會出現synthetic field。
切入點指示符
指示符 | 說明 |
---|---|
execution | 用于匹配方法執行的連接點 |
within | 用于匹配指定類型內的方法執行 |
this: | 用于匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配 |
target | 用于匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配 |
args | 用于匹配當前執行的方法傳入的參數為指定類型的執行方法 |
@within | 用于匹配所以持有指定注解類型內的方法 |
@target | 用于匹配當前目標對象類型的執行方法,其中目標對象持有指定的注解 |
@args | 用于匹配當前執行的方法傳入的參數持有指定注解的執行 |
@annotation | 用于匹配當前執行方法持有指定注解的方法 |
常用指示符
指示符 | 說明 |
---|---|
execution | 用于匹配方法執行的連接點 |
within | 用于匹配指定類型內的方法執行 |
this: | 用于匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配 |
target | 用于匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配 |
args | 用于匹配當前執行的方法傳入的參數為指定類型的執行方法 |
@within | 用于匹配所以持有指定注解類型內的方法 |
@target | 用于匹配當前目標對象類型的執行方法,其中目標對象持有指定的注解 |
@args | 用于匹配當前執行的方法傳入的參數持有指定注解的執行 |
@annotation | 用于匹配當前執行方法持有指定注解的方法 |
AspectJ切入點支持的切入點指示符還有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode,感興趣的可以了解,就不一一說明了。
類型匹配語法
先來看一下AspectJ類型匹配的通配符
- *:匹配任何數量字符;
- ..:匹配任何數量字符的重復,如在類型模式中匹配任何數量子包;而在方法參數模式中匹配任何數量參數。
- +:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。
接下來看一下具體匹配表達式類型
- 注解:可選,方法上持有的注解,如@Deprecated;
- 修飾符:可選,如public、protected;
- 返回值類型:必填,可以是任何類型模式;“*”表示所有類型;
- 類型聲明:可選,可以是任何類型模式;
- 方法名:必填,可以使用“*”進行模式匹配;
- 參數列表:“()”表示方法沒有任何參數;“(..)”表示匹配接受任意個參數的方法,“(..,java.lang.String)”表示匹配接受java.lang.String類型的參數結束,且其前邊可以接受有任意個參數的方法;“(java.lang.String,..)” 表示匹配接受java.lang.String類型的參數開始,且其后邊可以接受任意個參數的方法;“(*,java.lang.String)” 表示匹配接受java.lang.String類型的參數結束,且其前邊接受有一個任意類型參數的方法;
- 異常列表:可選,以“throws 異常全限定名列表”聲明,異常全限定名列表如有多個以“,”分割,如throws java.lang.IllegalArgumentException, java.lang.ArrayIndexOutOfBoundsException。
組合切入點表達式
AspectJ使用 且(&&)、或(||)、非(!)來組合切入點表達式。
常用場景舉例
Advice通知參數
前面已經介紹了Join point 連接點和 Pointcut 切入點,如果基本掌握了的話那么恭喜你內功已經修煉7成了。
我們成功設置好切入點后需要獲取通知來執行要切入的代碼片段,這里的通知相當于鉤子/回調方法,在程序執行到JPoint時候會調起通知,接下來就介紹一下獲取通知的方式。
Advice通知有三種類型
類型 | 說明 |
---|---|
before() | 是指在JPoint之前可以執行一些操作 |
after() | 是指在JPoint之后可以執行一些操作 |
around() | 環繞JPoint執行操作,它包含了前后兩個過程,使用這種類型需要手動調用procees方法來執行原操作 |
來看一個例子
我們來用檢測是否登錄做一個例子
這個是檢查登錄的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface CheckLogin {
}
先用before和after兩個類型來做一個測試
@Pointcut("execution(@com.jie.aoptest.aop.CheckLogin * *(..))")
public void methodAnnotated() {
}
@Before("methodAnnotated()")
public void beforeMethod(ProceedingJoinPoint joinPoint) {
Log.d("aspect", "beforeMethod");
Log.d("login", "請您登錄");
Toast.makeText(App.getAppContext().getCurActivity(), "請您登錄", Toast.LENGTH_SHORT).show();
}
@After("methodAnnotated()")
public void afterMethod(ProceedingJoinPoint joinPoint) {
Log.d("aspect", "afterMethod");
}
來看一下打印日志是這樣的
11-12 06:30:52.476 10388-10388/com.jie.aoptest D/aspect: beforeMethod
11-12 06:30:52.477 10388-10388/com.jie.aoptest D/login: 請您登錄
11-12 06:30:52.486 10388-10388/com.jie.aoptest D/aspect: afterMethod
然后我們用around做一個測試
@Pointcut("execution(@com.jie.aoptest.aop.CheckLogin * *(..))")
public void methodAnnotated() {
}
@Around("methodAnnotated()")
public void aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
Log.d("aspect", "aroundMethod");
Log.d("login", "請您登錄");
Toast.makeText(App.getAppContext().getCurActivity(), "請您登錄", Toast.LENGTH_SHORT).show();
joinPoint.proceed();
Log.d("aspect", "aroundMethod");
}
打印日志是這樣的
11-12 06:35:09.696 10512-10512/com.jie.aoptest D/aspect: aroundMethod
11-12 06:35:09.696 10512-10512/com.jie.aoptest D/login: 請您登錄
11-12 06:35:09.700 10512-10512/com.jie.aoptest D/aspect: aroundMethod
兩種方式都可以,但要注意一點around和after兩種類型是有沖突的,around和before可以共存,所以還是建議兩種方式,一種before和after配合使用,一種around單獨使用。
參數的獲取
方法參數的獲取
方法參數的獲取很簡單,可以通過joinPoint.getArgs()來獲取參數,舉個例子:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
safe("haha", 20, true);
...
}
@Safe
private void safe(String a, int b, boolean c) {
Log.d("aop", "獲取參數")
}
通知方法
@Around("execution(!synthetic * *(..)) && methodAnnotated()")
public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
for (Object arg : joinPoint.getArgs()) {
Log.d("arg", arg.toString());
}
joinPoint.proceed(joinPoint.getArgs());
}
日志打印
11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: haha
11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: 20
11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: true
注解參數的獲取
直接上代碼例子
首先需要在注解上聲明參數
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
//聲明參數
String declaredPermission();
}
然后看一下Activity中的調用方法,注意這里在注解后設置參數值
@CheckPermission(declaredPermission="android.permission.READ_PHONE_STATE")
private void checkPhoneState(){
Log.d("CheckPermission","Read Phone State succeed");
}
看一下切片類的寫法,注意這里在切點上要用@annotation來獲取注解對象,然后我們在aroundMethod方法中多了一個checkPermission對象,最后從這個對象中拿到注解參數
@Aspect
public class CheckPermissionAspect {
@Pointcut("execution(@com.jie.aoptest.aop.CheckPermission * *(..)) && @annotation(checkPermission)")
public void checkPermission(CheckPermission checkPermission){};
@Around("checkPermission(checkPermission)")
public void aroundMethod(JoinPoint joinPoint, CheckPermission checkPermission){
//從注解信息中獲取聲明的權限。
String neededPermission = checkPermission.declaredPermission();
Log.d("CheckPermissionAspect", joinPoint.toShortString());
Log.d("CheckPermissionAspect", "\tneeded permission is " + neededPermission);
}
}
最后來看一下輸出日志,證明我們已經成功拿到注解參數了
11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermissionAspect: execution(MainActivity.checkPhoneState())
11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermissionAspect: needed permission is android.permission.READ_PHONE_STATE
11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermission: Read Phone State succeed
總結
AspectJ解析基本就到這里了,掌握了它就可以全面的用AOP思想去解決問題了,核心還是在解決問題的思路。我也是在邊學習邊整理從而寫出這篇文檔,這里講解了一些AspectJ的基礎用法,高級用法大家可以從參考文獻的書中去慢慢探索。
參考文獻
- 深入理解Android之AOP 博主寫的細致入微,我也從中有所參考。
- Manning.AspectJ.in.Action第二版