AspectJ教程--AOP面向切面程式設計框架(Android)
AOP的概念很久前就接觸過了,不過沒有真正寫過專案,甚至Demo都沒有,今天把這點缺陷補上。
推薦兩篇文章(本文部分圖片引自這兩篇文章):
1. 【翻譯】Android中的AOP程式設計
2. 【深入理解Android之AOP】
1. 本篇文章總覽
2. 什麼是AOP
2.1 定義
AOP是Aspect Oriented Program的首字母縮寫,譯為:面向切面程式設計。類似的OOP,譯為:面向物件程式設計。
- OOP:面向物件思想簡單理解就是,需要把各功能封裝為獨立模組,然後把他們簡單拼裝成為產品。Android系統的各個模組封裝就遵循OOP(下圖)。
- AOP:在這些獨立的模組間,在特定的切入點進行hook,將共同的邏輯新增到模組中而不影響原有模組的獨立性。下圖,在不同的模組中加入日誌、快取、效能檢測功能,並不影響原有的架構。
2.2 相關術語
術語名稱 | 術語解釋 |
---|---|
Cross-cutting concerns(橫切關注點) | 多個模組可能新增相同附屬功能的點 |
Advice(通知) | 注入到class檔案中的程式碼。典型的 Advice 型別有 before、after 和 around,分別表示在目標方法執行之前、執行後和完全替代目標方法執行的程式碼。 除了在方法中注入程式碼,也可能會對程式碼做其他修改,比如在一個class中增加欄位或者介面。 |
join Point(連線點) | 所有可以注入程式碼的地方 |
PointCut(切入點) | 告訴AOP框架,我應該在哪個join point注入一段程式碼 |
Aspect(切面) | 由PointCut和Advice組成的公共邏輯成為切面,切面邏輯只需開發一次,多處呼叫 |
Weaving(織入) | 注入程式碼到目標位置 |
2.3 AOP使用場景
- 日誌相關
- 持久化操作
- 效能監控
- 資料校驗
- 快取
- 等…
3 OOP和AOP實現具體需求
統計三個模組耗時。
3.1 OOP實現轉向AOP實現圖例
3.2 OOP實現
3.2.1 編寫登入模組
簡寫程式碼如下:
/**
* OOP 登入模組
* Created by Administrator on 2017/10/13.
*/
public class LoginUtils {
private static final String TAG = "OOP";
public static boolean Login(String userName, String passWord){
long start=System.currentTimeMillis();
long end;
StringBuffer stringBuffer = new StringBuffer();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if ("張三".equals(userName) && "123456".equals(passWord)){
end = System.currentTimeMillis();
stringBuffer.append("登入成功,耗時:")
.append(end - start);
Log.e(TAG,stringBuffer.toString());
return true;
}else{
end = System.currentTimeMillis();
stringBuffer.append("登入失敗,耗時:")
.append(end - start);
Log.e(TAG,stringBuffer.toString());
return false;
}
}
}
3.2.2 編寫檔案上傳模組
簡寫程式碼如下
/**
* OOP 檔案上傳模組
* Created by Administrator on 2017/10/13.
*/
public class UploadFileUtils {
private static final String TAG = "OOP";
public static boolean upload(String url, String path){
long start=System.currentTimeMillis();
long end;
StringBuffer stringBuffer1 = new StringBuffer();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("從本地上傳")
.append(path)
.append("到")
.append(url);
Log.e(TAG,stringBuffer.toString());
end = System.currentTimeMillis();
stringBuffer1.append("檔案上傳成功,耗時:")
.append(end - start);
Log.e(TAG,stringBuffer1.toString());
return true;
}
}
3.2.3 依次執行登入、轉賬、本地上傳
日誌如下:
10-12 13:28:17.300 14605-14605/com.aspectjdemo E/OOP: 登入成功,耗時:2000
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 從111112賬戶轉出100.0到222221
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 轉賬成功,耗時:2001
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 從本地上傳/sd/example.png到www.baidu.com
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 檔案上傳成功,耗時:2001
3.2.4 找出弊端
按照上面的實現方式,弊端有以下幾個:
- 程式碼冗餘
- 邏輯不清晰
- 重構不方便
3.3 AOP實現
3.3.1 AspectJ實現AOP
AspectJ是一個非侵入式的AOP框架,下一章專門介紹。此處只寫Android Studio的實現方式,Eclipse實現方式不太一樣。
3.3.1.1 第一步 配置AspectJ
在app的gradle檔案中新增如下程式碼,作用:使用ajc代替javac編譯java程式碼。具體說明見:【翻譯】Android中的AOP程式設計
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
apply plugin: 'com.android.application'
repositories {
mavenCentral()
}
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
dependencies {
compile 'org.aspectj:aspectjrt:1.8.11'
}
3.3.1.2 第二步 自定義註解
/**
* 自定義註解
* Created by Administrator on 2017/10/13.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeTrace {
String value();
}
3.3.1.3 第三步 實現各模組,並加上@TimerTrace註解
拿登入模組來舉例,該模組中已經不包含耗時統計的邏輯。
/**
* AOP 登入模組
* Created by Administrator on 2017/10/13.
*/
public class AOPLoginUtils {
private static final String TAG = "OOP";
@TimeTrace(value = "登入")
public static boolean Login(String userName, String passWord){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if ("張三".equals(userName) && "123456".equals(passWord)){
return true;
}else{
return false;
}
}
}
3.3.1.4 第四步 用@Aspect標註切面類
@Aspect
public class TimeTraceAspect {
}
3.3.1.5 第五步 在切面類中定義PointCut(切入點)
// 語法:execution(@註解 訪問許可權 返回值的型別 包名.函式名(引數))
// 表示:使用TimeTrace註解的任意型別返回值任意方法名(任意引數)
@Pointcut("execution(@com.aspectjdemo.aop.TimeTrace * *(..))")
public void myPointCut(){
}
3.3.1.6 第六步 在切面類中定義Advance(通知)
具體注入的程式碼
// Advance比較常用的有:Before():方法執行前,After():方法執行後,Around():代替原有邏輯
@Around("myPointCut()")
public Object dealPoint(ProceedingJoinPoint point) throws Throwable {
// 方法執行前先記錄時間
long start=System.currentTimeMillis();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
// 獲取註解
TimeTrace annotation = methodSignature.getMethod().getAnnotation(TimeTrace.class);
String value = annotation.value();
// 執行原方法體
Object proceed = point.proceed();
// 方法執行完成後,記錄時間,列印日誌
long end = System.currentTimeMillis();
StringBuffer stringBuffer = new StringBuffer();
if (proceed instanceof Boolean){
// 返回的是boolean
if ((Boolean)proceed){
stringBuffer.append(value)
.append("成功,耗時:")
.append(end - start);
}else{
stringBuffer.append(value)
.append("失敗,耗時:")
.append(end - start);
}
}
Log.e(TAG,stringBuffer.toString());
return proceed;
}
3.3.1.6 第六步 依次執行登入、轉賬、本地上傳
10-12 13:25:44.106 12332-12332/com.aspectjdemo E/AOP: 登入成功,耗時:2001
10-12 13:25:46.136 12332-12332/com.aspectjdemo E/OOP: 從111112賬戶轉出100.0到222221
10-12 13:25:46.137 12332-12332/com.aspectjdemo E/AOP: 轉賬成功,耗時:2002
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/OOP: 從本地上傳/sd/example.png到www.baidu.com
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/AOP: 檔案上傳成功,耗時:2001
3.3.1.7 總結優點
- 減少程式碼冗餘
- 程式碼邏輯更清晰
- 方便擴充套件、重構
3.3.2 其他方式實現AOP
不是本文重點,不深入,其實是我還沒了解其他方式(~ ̄▽ ̄)~,稍微羅列一下。
- 原始的AOP模式
- 動態代理實現AOP
- 等…
4 AspectJ詳解
建議這一部分直接去看這個文章,這個文章,這部分很詳細,很多語法,各種說明:【深入理解Android之AOP】
4.1 AspectJ介紹
4.2 語法
本節只講解AspectJ的註解語法
4.2.1 Join Points介紹
在Aspect的術語章節講過,join Point(連線點):所有可以注入程式碼的地方,在AspectJ中是有規定的,只有在下表的幾個地方才認為是join Ponit。
Join Points |
說明 |
示例 |
method call |
函式呼叫 |
比如呼叫Log.e(),這是一處JPoint |
method execution |
函式執行 |
比如Log.e()的執行內部,是一處JPoint。注意它和method call的區別。method call是呼叫某個函式的地方。而execution是某個函式執行的內部。 |
constructor call |
建構函式呼叫 |
和method call類似 |
constructor execution |
建構函式執行 |
和method execution類似 |
field get |
獲取某個變數 |
比如讀取DemoActivity.debug成員 |
field set |
設定某個變數 |
比如設定DemoActivity.debug成員 |
pre-initialization |
Object在建構函式中做得一些工作。 |
很少使用,詳情見下面的例子 |
initialization |
Object在建構函式中做得工作 |
詳情見下面的例子 |
static initialization |
類初始化 |
比如類的static{} |
handler |
異常處理 |
比如try catch(xxx)中,對應catch內的執行 |
advice execution |
這個是AspectJ的內容,稍後再說 |
4.2.2 Pointcuts介紹
在Aspect的術語章節講過,PointCut(切入點):告訴AOP框架,我應該在哪個join point注入一段程式碼。那麼Pointcuts就是篩選出來的符合條件的所有切入點。
1、 直接選擇Join Point
Join Point | Ponitcut語法 | 示例 |
---|---|---|
Method execution | execution(MethodSignature) | 在Activtiy的所有生命週期執行前,注入程式碼:@Before(“execution(* android.app.Activity.on**(..))”) |
Method call | call(MethodSignature) | 在呼叫指定方法後,注入程式碼:@Before(“execution(* android.app.Activity.on**(..))”) |
constructor call | call(ConstructorSignature) | 在呼叫指定構造方法後,注入程式碼:@Before(“call(com.aspectjdemo.aopexample.UIUtils.new())”) |
constructor execution | execution(ConstructorSignature) | 在執行指定構造方法後,注入程式碼:@After(“call(com.aspectjdemo.aopexample.UIUtils.new())”) |
field get | get(FieldSignature) | 在呼叫指定欄位get方法後,注入程式碼:@After(“get(String com.aspectjdemo.aopexample.AspectJActivity.userName)”) |
field set | set(FieldSignature) | 在呼叫指定欄位get方法前,注入程式碼:@Before(“set(String com.aspectjdemo.aopexample.AspectJActivity.userName)”) |
Object initialization | initialization(ConstructorSignature) | 在指定的物件初始化後,注入程式碼:@After(“initialization(com.aspectjdemo.aopexample.UIUtils.new())”) |
MethodSignature匹配規則:
@註解 訪問許可權 返回值的型別 包名.函式名(引數)
1. @註解和訪問許可權(public/private/protect,以及static/final)屬於可選項。如果不設定它們,則預設都會選擇。以訪問許可權為例,如果沒有設定訪問許可權作為條件,那麼public,private,protect及static、final的函式都會進行搜尋。
2. 返回值型別就是普通的函式的返回值型別。如果不限定型別的話,就用*
萬用字元表示
3. 包名.函式名用於查詢匹配的函式。可以使用萬用字元,包括*
和..以及+號。其中*
號用於匹配除.號之外的任意字元,而..則表示任意子package,+號表示子類。
比如:
java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase,也可以表示TestDervied
java..*:表示java任意子類
java..*Model+:表示Java任意package中名字以Model結尾的子類,比如TabelModel,TreeModel
等
4. 最後來看函式的引數。引數匹配比較簡單,主要是引數型別,比如:
(int, char):表示引數只有兩個,並且第一個引數型別是int,第二個引數型別是char
(String, ..):表示至少有一個引數。並且第一個引數型別是String,後面引數型別不限。在引數匹配中,
..代表任意引數個數和型別
(Object …):表示不定個數的引數,且型別都是Object,這裡的…不是萬用字元,而是Java中代表不定引數的意思
ConstructorSignature匹配規則:
Constructorsignature和Method Signature類似,只不過建構函式沒有返回值,而且函式名必須叫new。比如:
public *..TestDerived.new(..):
public:選擇public訪問許可權
*..代表任意包名
TestDerived.new:代表TestDerived的建構函式
(..):代表引數個數和型別都是任意
FieldSignature匹配規則:
Field Signature標準格式:
@註解 訪問許可權 型別 類名.成員變數名
其中,@註解和訪問許可權是可選的型別:成員變數型別,*
代表任意型別類名.成員變數名:成員變數名可以是*,代表任意成員變數
比如,
set(*.base):表示設定所有包名下base變數時的JPoint
2、 間接選擇Join Point
這一類,在demo中沒有示例,有興趣的自己在Demo中新增檢視效果。
關鍵詞 |
說明 |
示例 |
within(TypePattern) |
TypePattern標示package或者類。TypePatter可以使用萬用字元 |
表示某個Package或者類中的所有JPoint。比如 within(Test):Test類中(包括內部類)所有JPoint。圖2所示的例子就是用這個方法。 |
withincode(Constructor Signature|Method Signature) |
表示某個建構函式或其他函式執行過程中涉及到的JPoint |
比如 withinCode(* TestDerived.testMethod(..)) 表示testMethod涉及的JPoint withinCode( *.Test.new(..)) 表示Test建構函式涉及的JPoint |
cflow(pointcuts) |
cflow是call flow的意思 cflow的條件是一個pointcut |
比如 cflow(call TestDerived.testMethod):表示呼叫TestDerived.testMethod函式時所包含的JPoint,包括testMethod的call這個JPoint本身 |
cflowbelow(pointcuts) |
cflow是call flow的意思。 |
比如 cflowblow(call TestDerived.testMethod):表示呼叫TestDerived.testMethod函式時所包含的JPoint,不包括testMethod的call這個JPoint本身 |
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。 |
4.2.3 advice介紹
前面例子中已經用過了,具體看下面表中說明即可。
關鍵詞 |
說明 |
示例 |
before() |
before advice |
表示在JPoint執行之前,需要乾的事情 |
after() |
after advice |
表示JPoint自己執行完了後,需要乾的事情。 |
after():returning(返回值型別) after():throwing(異常型別) |
returning和throwing後面都可以指定具體的型別,如果不指定的話則匹配的時候不限定型別 |
假設JPoint是一個函式呼叫的話,那麼函式呼叫執行完有兩種方式退出,一個是正常的return,另外一個是拋異常。 注意,after()預設包括returning和throwing兩種情況 |
返回值型別 around() |
before和around是指JPoint執行前或執行後備觸發,而around就替代了原JPoint |
around是替代了原JPoint,如果要執行原JPoint的話,需要呼叫proceed |
4.3 實現步驟
PS:這一節已經很詳細的在 3.3.1中寫了,還有程式碼示例,往上看。
4.4 AspectJ原理
4.4.1 找到AS下的class檔案
路徑如下:app->build->intermediates->classes->debug->com包下即是我們使用ajc編譯後的class程式碼。
4.4.2 Around原理
使用Around處理後,編譯出來的class檔案
@TimeTrace("登入")
public static boolean Login(String userName, String passWord) {
JoinPoint var5 = Factory.makeJP(ajc$tjp_0, (Object)null, (Object)null, userName, passWord);
TimeTraceAspect var10000 = TimeTraceAspect.aspectOf();
Object[] var6 = new Object[]{userName, passWord, var5};
return Conversions.booleanValue(var10000.dealPoint((new AOPLoginUtils$AjcClosure1(var6)).linkClosureAndJoinPoint(65536)));
}
可以看出來,在這個方法的開頭和結尾,都被注入了一些程式碼,成為我們最終執行到手機上的class檔案。
5. 總結
本文只是對AspectJ做了一個入門的介紹,很多高階的用法都未加入進來,在實際專案使用時再進行挖掘吧。
如果有對應的AOP使用場景,建議使用AspectJ,你會感覺到很爽的。