一、引言
本博文的目的不是詳細的介紹AspectJ的細節,而是最近項目用到了AspectJ,因此對其作了一些使用和重要概念上的總結。
相信很多做過Web的同學對AspectJ都不陌生,Spring的AOP就是基於它而來的。如果說平常我們隨便寫寫程序的時候,基本也不會用到它,需要調試的話無非就是多加一個System.out.printfln()或者Log.d()。但是由於基於面向對象的固有缺陷,導致很多同模塊、同一水平上的工作要在許多類中重複出現。比如說:輸出日誌,監控方法執行時間,修改程序運行時的參數等等這樣的事情,其實它們的代碼都是可以重用的。
如果在一個大型的項目當中,使用手動修改源碼的方式來達到調試、監控的目的,第一,需要插入許多重複代碼(打印日誌,監控方法執行時間),代碼無法複用;第二,修改的成本太高,處處需要手動修改(分分鐘累死、眼花)。
OOP: 面向對象把所有的事物都當做對象看待,因此每一個對象都有自己的生命週期,都是一個封裝的整體。每一個對象都有自己的一套垂直的系列方法和屬性,使得我們使用對象的時候不需要太多的關係它的內部細節和實現過程,只需要關注輸入和輸出,這跟我們的思維方式非常相近,極大的降低了我們的編寫代碼成本(而不像C那樣讓人頭痛!)。但在現實世界中,並不是所有問題都能完美得劃分到模塊中。舉個最簡單而又常見的例子:現在想爲每個模塊加上日誌功能,要求模塊運行時候能輸出日誌。在不知道AOP的情況下,一般的處理都是:先設計一個日誌輸出模塊,這個模塊提供日誌輸出API,比如Android中的Log類。然後,其他模塊需要輸出日誌的時候調用Log類的幾個函數,比如e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。
AOP: 面向對象編程固然是開啓另一個編程時代(吹逼一下),但是久而久之也顯露了它的缺點,最明顯的一點就是它無法橫向切割某一類方法、屬性,當我們需要了解某一類方法、某一類屬性的信息時,就必須要在每一個類的方法裏面(即便他們是同樣的方法,只因是不同的類所以不同)添加監控代碼,在代碼量龐大的情況下,這是一個不可取的方法。因此,AOP編產生了,基於AOP的編程可以讓我們橫向的切割某一類方法和屬性(不需要關心他是什麼類別!),我覺得AOP並不是與OOP對立的,而是爲了彌補OOP的不足,因爲有了AOP我們的調試和監控就變得簡單清晰。
二、什麼是AspectJ?
2.1 它只是一個代碼編譯器
AspectJ 意思就是Java的Aspect,Java的AOP。它其實不是一個新的語言,它就是一個代碼編譯器(ajc,後面以此代替),在Java編譯器的基礎上增加了一些它自己的關鍵字識別和編譯方法。因此,ajc也可以編譯Java代碼。它在編譯期將開發者編寫的Aspect程序編織到目標程序中,對目標程序作了重構,目的就是建立目標程序與Aspect程序的連接(耦合,獲得對方的引用(獲得的是聲明類型,不是運行時類型)和上下文信息),從而達到AOP的目的(這裏在編譯期還是修改了原來程序的代碼,但是是ajc替我們做的)。
2.2 它是用來做AOP編程的
AspectJ就是AOP,只不過是面向java的。AOP裏面有一些重要基本的概念:
aspect(切面):實現了cross-cutting功能,是針對切面的模塊。最常見的是logging模塊、方法執行耗時模塊,這樣,程序按功能被分爲好幾層,如果按傳統的繼承的話,商業模型繼承日誌模塊的話需要插入修改的地方太多,而通過創建一個切面就可以使用AOP來實現相同的功能了,我們可以針對不同的需求做出不同的切面。
jointpoint(連接點):連接點是切面插入應用程序的地方,該點能被方法調用,而且也會被拋出意外。連接點是應用程序提供給切面插入的地方,在插入地建立AspectJ程序與源程序的連接。
下面列表上的是被AspectJ認爲是joinpoint的:
advice(處理邏輯): advice是我們切面功能的實現,它是切點的真正執行的地方。比如像寫日誌到一個文件中,advice(包括:before、after、around等)在jointpoint處插入代碼到應用程序中。我們來看一看原AspectJ程序和反編譯過後的程序。看完下面的圖我們就大概明白了AspectJ是如何達到監控源程序的信息了。
原Activity代碼:
Advise:
反編譯後的原代碼:
pointcut(切點): pointcut可以控制你把哪些advice應用於jointpoint上去,通常你使用pointcuts通過正則表達式來把明顯的名字和模式進行匹配應用。決定了那個jointpoint會獲得通知。分爲call、execution、target、this、within等關鍵字(具體含義見第四節)
2.3、爲什麼要用AspectJ?
1、非侵入式監控: 可以在不修監控目標的情況下監控其運行,截獲某類方法,甚至可以修改其參數和運行軌跡!
2、學習成本低: 它就是Java,只要會Java就可以用它。
3、功能強大,可拓展性高: 它就是一個編譯器+一個庫,可以讓開發者最大限度的發揮,實現形形色色的AOP程序!
三、AspectJ原理與運用
先放一塊AspectJ代碼,(這裏使用的都是AspectJ較爲常用的知識),接着在解釋。
import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Path;
import android.os.Build;
import org.android10.gintonic.internal.ChooseDialog;
import org.android10.gintonic.internal.DebugLog;
import org.android10.gintonic.internal.MethodMsg;
import org.android10.gintonic.internal.StopWatch;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
/**
* 截獲類名最後含有Activity、Layout的類的所有方法
* 監聽目標方法的執行時間
*/
@Aspect
public class TraceAspect {
private static Object currentObject = null;
//進行類似於正則表達式的匹配,被匹配到的方法都會被截獲
////截獲任何包中以類名以Activity、Layout結尾,並且該目標類和當前類是一個Object的對象的所有方法
private static final String POINTCUT_METHOD =
"(execution(* *..Activity+.*(..)) ||execution(* *..Layout+.*(..))) && target(Object) && this(Object)";
//精確截獲MyFrameLayou的onMeasure方法
private static final String POINTCUT_CALL = "call(* org.android10.viewgroupperformance.component.MyFrameLayout.onMeasure(..))";
private static final String POINTCUT_METHOD_MAINACTIVITY = "execution(* *..MainActivity+.onCreate(..))";
//切點,ajc會將切點對應的Advise編織入目標程序當中
@Pointcut(POINTCUT_METHOD)
public void methodAnnotated() {}
@Pointcut(POINTCUT_METHOD_MAINACTIVITY)
public void methodAnootatedWith(){}
/**
* 在截獲的目標方法調用之前執行該Advise
* @param joinPoint
* @throws Throwable
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Before("methodAnootatedWith()")
public void onCreateBefore(JoinPoint joinPoint) throws Throwable{
Activity activity = null;
//獲取目標對象
activity = ((Activity)joinPoint.getTarget());
//插入自己的實現,控制目標對象的執行
ChooseDialog dialog = new ChooseDialog(activity);
dialog.show();
//做其他的操作
buildLogMessage("test",20);
}
/**
* 在截獲的目標方法調用返回之後(無論正常還是異常)執行該Advise
* @param joinPoint
* @throws Throwable
*/
@After("methodAnootatedWith()")
public void onCreateAfter(JoinPoint joinPoint) throws Throwable{
Log.e("onCreateAfter:","onCreate is end .");
}
/**
* 在截獲的目標方法體開始執行時(剛進入該方法實體時)調用
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("methodAnnotated()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
if (currentObject == null){
currentObject = joinPoint.getTarget();
}
//初始化計時器
final StopWatch stopWatch = new StopWatch();
//開始監聽
stopWatch.start();
//調用原方法的執行。
Object result = joinPoint.proceed();
//監聽結束
stopWatch.stop();
//獲取方法信息對象
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className;
//獲取當前對象,通過反射獲取類別詳細信息
className = joinPoint.getThis().getClass().getName();
String methodName = methodSignature.getName();
if (currentObject != null && currentObject.equals(joinPoint.getTarget())){
DebugLog.log(new MethodMsg(className, buildLogMessage(methodName, stopWatch.getTotalTimeMicros()),stopWatch.getTotalTimeMicros()));
}else if(currentObject != null && !currentObject.equals(joinPoint.getTarget())){
DebugLog.log(new MethodMsg(className, buildLogMessage(methodName, stopWatch.getTotalTimeMicros()),stopWatch.getTotalTimeMicros()));
currentObject = joinPoint.getTarget();
DebugLog.outPut(new Path()); //日誌存儲
DebugLog.ReadIn(new Path()); //日誌讀取
}
return result;
}
/**
* 創建一個日誌信息
*
* @param methodName 方法名
* @param methodDuration 執行時間
* @return
*/
private static String buildLogMessage(String methodName, long methodDuration) {
StringBuilder message = new StringBuilder();
message.append(methodName);
message.append(" --> ");
message.append("[");
message.append(methodDuration);
if (StopWatch.Accuracy == 1){
message.append("ms");
}else {
message.append("mic");
}
message.append("] ");
return message.toString();
}
}
3.1 基本原理
在編譯期對目標對象、方法做標記,對目標類、方法進行重構,將PointCut插入目標中,截獲該目標的信息以及上下文環境,以達到非侵入代碼監控的目的——注意,它只能獲得對象的聲明,如果對象的聲明式接口,那麼默認情況下(不使用this、target約束切點),獲取的是聲明類型,而不是具體運行時的類。
1、編寫Aspect:聲明Aspect、PointCut和Advise。
2、ajc編織: AspectJ編譯器在編譯期間對所切點所在的目標類進行了重構,在編譯層將AspectJ程序與目標程序進行雙向關聯,生成新的目標字節碼,即將AspectJ的切點和其餘輔助的信息類段插入目標方法和目標類中,同時也傳回了目標類以及其實例引用。這樣便能夠在AspectJ程序裏對目標程序進行監聽甚至操控。
3、execution: 顧名思義,它截獲的是方法真正執行的代碼區,Around方法塊就是專門爲它存在的。調用Around可以控制原方法的執行與否,可以選擇執行也可以選擇替換。
//截獲任何包中以類名以Activity、Layout結尾,並且該目標類和當前類是一個Object的對象的所有方法
private static final String POINTCUT_METHOD =
"(execution(* *..Activity+.*(..)) ||execution(* *..Layout+.*(..))) && target(Object) && this(Object)";
//基於execution的切點
@Pointcut(POINTCUT_METHOD)
public void methodAnnotated() {}
4 . call: 同樣,從名字可以看出,call截獲的是方法的調用區,它並不截獲代碼真正的執行區域,它截獲的是方法調用之前與調用之後(與before、after配合使用),在調用方法的前後插入JoinPoint和before、after通知。它截獲的信息並沒有execution那麼多,它無法控制原來方法的執行與否,只是在方法調用前後插入切點,因此它比較適合做一些輕量的監控(方法調用耗時,方法的返回值等)。
//精確截獲MyFrameLayou的onMeasure方法
private static final String POINTCUT_CALL = "call(* org.android10.viewgroupperformance.component.MyFrameLayout.onMeasure(..))";
//基於call的切點
@Pointcut(POINTCUT_METHOD_MAINACTIVITY)
public void methodAnootatedWith(){}
5 、Around替代原理:目標方法體被Around方法替換,原方法重新生成,名爲XXX_aroundBody(),如果要調用原方法需要在AspectJ程序的Around方法體內調用joinPoint.proceed()還原方法執行,是這樣達到替換原方法的目的。達到這個目的需要雙方互相引用,橋樑便是Aspect類,目標程序插入了Aspect類所在的包獲取引用。AspectJ通過在目標類裏面加入Closure(閉包)類,該類構造函數包含了目標類實例、目標方法參數、JoinPoint對象等信息,同時該類作爲切點原方法的執行代理,該閉包通過Aspect類調用Around方法傳入Aspect程序。這樣便達到了關聯的目的,便可以在Aspect程序中監控和修改目標程序。
/**
* 在截獲的目標方法體開始執行時(剛進入該方法實體時)調用
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("methodAnnotated()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
if (currentObject == null){
currentObject = joinPoint.getTarget();
}
//初始化計時器
final StopWatch stopWatch = new StopWatch();
//開始監聽
stopWatch.start();
//調用原方法的執行。
Object result = joinPoint.proceed();
//監聽結束
stopWatch.stop();
//獲取方法信息對象
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className;
//獲取當前對象,通過反射獲取類別詳細信息
className = joinPoint.getThis().getClass().getName();
String methodName = methodSignature.getName();
if (currentObject != null && currentObject.equals(joinPoint.getTarget())){
DebugLog.log(new MethodMsg(className, buildLogMessage(methodName, stopWatch.getTotalTimeMicros()),stopWatch.getTotalTimeMicros()));
}else if(currentObject != null && !currentObject.equals(joinPoint.getTarget())){
DebugLog.log(new MethodMsg(className, buildLogMessage(methodName, stopWatch.getTotalTimeMicros()),stopWatch.getTotalTimeMicros()));
currentObject = joinPoint.getTarget();
DebugLog.outPut(new Path()); //日誌存儲
DebugLog.ReadIn(new Path()); //日誌讀取
}
return result;
}
6 、 Before與After: Before與After只是在方法被調用前和調用之後添加JoinPoint和通知方法(直接插入原程序方法體中),調用AspectJ程序定義的Advise方法,它並不替代原方法,是在方法call之前和之後做一個插入操作。After分爲returnning和throwing兩類,前者是在正常returning之後調用,後者是在throwing發生之後調用。默認的After是在finally處調用,因此它包含了前面的兩種情況。
/**
* 在截獲的目標方法調用之前執行該Advise
* @param joinPoint
* @throws Throwable
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Before("methodAnootatedWith()")
public void onCreateBefore(JoinPoint joinPoint) throws Throwable{
Activity activity = null;
//獲取目標對象
activity = ((Activity)joinPoint.getTarget());
//插入自己的實現,控制目標對象的執行
ChooseDialog dialog = new ChooseDialog(activity);
dialog.show();
//做其他的操作
buildLogMessage("test",20);
}
/**
* 在截獲的目標方法調用返回之後(無論正常還是異常)執行該Advise
* @param joinPoint
* @throws Throwable
*/
@After("methodAnootatedWith()")
public void onCreateAfter(JoinPoint joinPoint) throws Throwable{
Log.e("onCreateAfter:","onCreate is end .");
}
7 、重要關鍵字 :
在其它關鍵字中,必須要注意的就是this、target的使用和區別,同時還有一個很重要的方法Signature.getDeclaringType() AspectJ是在編譯期截獲的對象信息,因此它獲得的標籤只是對象的聲明(比如:接口、抽象類),而不是運行時具體的對象。如果想要獲得運行時對象,就需要用this、target關鍵字
this :用於匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配;
target:用於匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配;
args:用於匹配當前執行的方法傳入的參數爲指定類型的執行方法;
within:用於匹配指定類型內的方法執行;
更加詳細的解說請參考深入理解Android之AOP,該博文對於AspectJ的其他詳細概念、定義、細節示例解說的非常清楚,如果想要詳細瞭解請務必要看。
3.2 使用方式
3.2.1 純註解方式
上面貼的代碼就是該方式,也是最普遍的方式,它不需要其他插件的支持(Eclipse中有AJDT可以支持AspectJ關鍵字聲明,但Android Studio中沒有改插件),使用Java的註解和ajc以及它的庫就可以完成AOP編程,非常方便,而且可以在絕大部分支持Java的IDE中使用。缺點就是對於註釋部分的匹配沒有檢錯功能。
/**
* Created by lingyi.mly on 2016/5/21.
*/
@Aspect
public class TraceAspect3 {
private static volatile Object currentObject = null;
private ExecutorService ThreadPool = Executors.newFixedThreadPool(10);
private static final String POINTCUT_METHOD =
"call(* *.*(..))&&target(Object) &&!within(*.TimeMonitorFragment)";
@Pointcut(POINTCUT_METHOD)
public void methodAnnotated() {
}
StopWatch stopWatch;
MethodSignature methodSignature;
String methodName;
String className;
@Before("methodAnnotated()")
public void beforeInvoked(final JoinPoint joinPoint) {
className = "call target: " + joinPoint.getTarget().getClass().getName();
methodSignature = (MethodSignature) joinPoint.getSignature();
methodName = methodSignature.getName();
stopWatch = new StopWatch();
stopWatch.start();
}
@After("methodAnnotated()")
public void afterInvoked(final JoinPoint joinPoint) {
stopWatch.stop();
double methodDuration = stopWatch.getTotalTime(StopWatch.Accuracy);
DebugLog.log(new MethodMsg(className, methodName, methodDuration, StopWatch.Accuracy));
}
}
3.2.2 AspectJ語言
在Eclipse中使用AJDT插件,可以識別AspectJ的語法。這樣編寫起來相對於註解要方便許多,還提供檢錯功能,比較強大。不過不是所有的IDE都支持,比如Android Studio目前就沒有(我哭了好久)。
package main;
import java.util.HashMap;
import java.util.Map;
/**
* 只有call才能區分this 與target 在與的情況下兩者不共存,在交的情況下共存。
* execution匹配this 與 target時無論是 與 還是 交集 都是同一個對象
* @author lingyi.mly
*
*/
public aspect Aspect{
static int count = 0;
pointcut targetTest() : call(* main.*.*(..)) &&( target(Object) );
pointcut thisTest( ) : execution(* main.*.*(..)) && (target(Object) ||this(Object));
Object around() : thisTest() {
if (thisJoinPoint.getThis() != null) {
System.out.println(thisJoinPoint.getThis().getClass().getName() + " " + thisJoinPoint.getSourceLocation());
}else if (thisJoinPoint.getTarget() != null) {
System.out.println(thisJoinPoint.getTarget().getClass().getName() + " " + thisJoinPoint.getSourceLocation());
}
return null;
}
before() : targetTest() {
if (thisJoinPoint.getThis() != null) {
System.out.println("this: "+thisJoinPoint.getThis().getClass().getName() + " " + thisJoinPoint.getSourceLocation());
}else if (thisJoinPoint.getTarget() != null) {
System.out.println("target: "+thisJoinPoint.getTarget().getClass().getName() + " " + thisJoinPoint.getSourceLocation());
}
}
private static Map<String, Integer> threadMap = new HashMap<String,Integer>();
}
3.2.3 結合自定義註解使用
這個是混合用法,可以在execution、call中使用註解,然後該註解標註在目標方法上就可以實現關聯,並且截獲。這樣做的好處實在想不到,最多就是可以精確定位到某一個方法(那使用絕對路徑匹配不也可以?)。而且還侵入了源碼。實在是不推薦,不過我在網上看到有人這麼用了,所以也貼上來了。如果哪位高手知道這樣做的精髓,請一定指教。下面貼一下它的用法實現。
自定義註解及被標記的方法:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 表明被註釋的方法將被跟蹤(僅在Debug模式下)並且將會與Aspect程序中截獲該註釋的Advise關聯,調用該切點
* 的Advise
*/
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface DebugTrace {}
/**
* 被註解的類
*/
public class MyFrameLayout extends FrameLayout {
//........
//被註解的方法
@DebugTrace
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@DebugTrace
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
}
切面:
package org.android10.gintonic.aspect;
/**
* 跟蹤被DebugTrace註解標記的方法和構造函數
*/
@Aspect
public class TraceAspect {
//跟蹤DebugTrace註解
private static final String POINTCUT_METHOD =
"execution(@org.android10.gintonic.annotation.DebugTrace * *(..))";
@Pointcut(POINTCUT_METHOD)
public void methodAnnotatedWithDebugTrace() {}
@Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
// Do SomeThing
stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();
// Do SomeThing
return result;
}
// ........省略
}
四、AspectJ實戰——監聽方法執行耗時,打印並輸出
源程序代碼:Android-AOPExample-master
關鍵代碼:
private static final String POINTCUT_METHOD =
"(execution(* *..Activity+.*(..)) ||execution(* *..Layout+.*(..))) && target(Object) && this(Object)";
// ...........
@Pointcut(POINTCUT_METHOD)
public void methodAnnotated() {}
// .........
@Around("methodAnnotated()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
if (currentObject == null){
currentObject = joinPoint.getTarget();
}
//初始化計時器
final StopWatch stopWatch = new StopWatch();
//開始監聽
stopWatch.start();
//調用原方法的執行。
Object result = joinPoint.proceed();
//監聽結束
stopWatch.stop();
//獲取方法信息對象
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className;
//獲取當前對象,通過反射獲取類別詳細信息
className = joinPoint.getThis().getClass().getName();
String methodName = methodSignature.getName();
String msg = buildLogMessage(methodName, stopWatch.getTotalTime(1));
if (currentObject != null && currentObject.equals(joinPoint.getTarget())){
DebugLog.log(new MethodMsg(className,msg,stopWatch.getTotalTime(1)));
}else if(currentObject != null && !currentObject.equals(joinPoint.getTarget())){
DebugLog.log(new MethodMsg(className, msg,stopWatch.getTotalTime(1)));
Log.e(className,msg);
currentObject = joinPoint.getTarget();
// DebugLog.outPut(new Path()); //日誌存儲
// DebugLog.ReadIn(new Path()); //日誌讀取
}
return result;
}
監聽方法執行時間:
TimeMonitor:: org.android10.viewgroupperformance.activity.RelativeLayoutTestActivity onCreate --> [8.636ms]
org.android10.viewgroupperformance.activity.MainActivity openActivity --> [6.561ms]
org.android10.viewgroupperformance.activity.MainActivity mapGUI --> [0.061ms]
五、一些比較常見的問題
(1)問題:AspectJ中Signature提供的getDeclareType返回的是聲明類型,無法獲取運行時類型,因此無法準確獲取接口運行時類別。
方案:使用target關鍵字約束pointCut,獲取目標對象,通過反射獲取其運行時類別。
(2)問題:使用target關鍵字約束pointcut獲取目標對象Object之後,無法獲取靜態方法(不屬於對象)
方案:單獨將靜態方法提出來,再與前面的target關鍵字約束的集合取並集。
(3)問題:使用Before、After通知,測試方法耗時的精確度誤差較大
方案:改用execution+around。兩點,第一:由於Before、After是在原方法調用前後插入通知(會影響本來所在方法快的執行速率);第二:同時Before、After兩個操作無法保證是原子操作,多線程情況下會有誤差。因此該用execution關鍵字,截獲方法體的真正執行處,使用Around通知,替代原方法(原方法被更名,但結構不變),在Around通知體內調用原方法計時,這樣能夠真正還原方法執行耗時;