1. 程式人生 > >android AOP實現之AspectJ

android AOP實現之AspectJ

AOP

1.1 背景

OOP(面向物件程式設計)的精髓是把功能或問題模組化,每個模組都有自己的職責,理想狀態是隻處理自己職責之內的事務。但在實際中,理想的職責單一往往攜帶了一些其他的、“髒”的邏輯處理。舉個最簡單而又常見的例子:現在想為模組A加上日誌功能,要求模組執行時候能輸出日誌。在不知道AOP的情況下,一般的處理都是:先設計一個日誌輸出模組,這個模組提供日誌輸出API,比如Android中的Log類。然後,其他模組需要輸出日誌的時候呼叫Log類的幾個函式,比如e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。範圍再延伸一些,在其他模組中(模組A,模組B…)同樣需要記錄日誌的功能。而這些日誌的處理對於原模組來講就是“髒”的邏輯,如何把日誌這部分邏輯從原模組剔除出去集中處理?
AOP (面向切面程式設計)就是為了解決OOP過程中的橫向尷尬。

1.2 AOP定義與使用場景

AOP:(Aspect-Oriented Programming)是一種面向切面的程式設計正規化,可以通過預編譯方式和執行期動態代理實現在不修改原始碼的情況下給程式動態統一新增功能的技術。AOP是OOP的延續,是函數語言程式設計的一種衍生範型,將程式碼切入到類的指定方法、指定位置上的程式設計思想。主要實現的目的是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果,提高模組化。

1.3 AOP使用場景

使用場景:在OOP過程中的一些橫向的、跨越多個模組的任務,通過橫切能達到集中處理的任務。比如日誌記錄、任務執行時間統計、安全控制、許可權控制、效能統計、異常處理等等。

1.4 AOP實現調研

AOP實現之AspectJ

AspectJ是一個面向切面的框架,一個對Java的通用面向切面的擴充套件。AspectJ定義了AOP語法,包含一個專門的編譯器(ajc: 用於AspectJ和Java語言的位元組碼編譯)用來生成遵守Java位元組編碼規範的Class檔案。

在android 中使用AspectJ有兩種方法:1.完全使用AspectJ的語言(需要學習規範);2.使用純Java語言開發,然後使用AspectJ註解,簡稱@AspectJ。
(注:下文內容著重介紹第二種方法。)

1.AspectJ概念點

在使用AspectJ之前先了解幾個概念:Join points(連結點),Pointcuts(切入點),Advice(通知)。

1.1 連線點(Join Points)

連線點是程式執行過程中明確的一點,雖然切面定義了橫切的型別,但AspectJ系統不允許完全任意的橫切。相反,切面定義了在程式執行過程中切入原則性點的型別,這些原則性的點被稱為連線點。連線點可以是方法或建構函式呼叫和執行,異常處理,欄位分配和訪問等。AspectJ提供了多種連線點:

Join Points 解釋
Method call 當一個方法被呼叫時,不包括非靜態方法的super呼叫。
Method execution 當實際方法的程式碼體執行時。
Constructor call 構建一個物件並呼叫該物件的初始建構函式(即,不用於“super”或“this”建構函式呼叫)。被構造的物件在建構函式呼叫連線點返回,所以它的返回型別被認為是物件的型別,並且可以在返回通知後訪問物件本身。
Constructor execution 當實際建構函式的程式碼體執行時,在”this”或者“super”建構函式呼叫之後執行。正在構造的物件是當前正在執行的物件,因此可以使用此切入點來訪問。呼叫超級建構函式的建構函式的建構函式執行連線點還包含封裝類的任何非靜態初始化方法。沒有值從建構函式執行連線點返回,所以它的返回型別被認為是無效的。
Static initializer execution 當一個類的靜態初始化器執行時。沒有值從靜態初始化器執行連線點返回,所以它的返回型別被認為是無效的。
Object pre-initialization 在特定類的物件初始化程式碼執行之前。這包含從第一個被呼叫的建構函式開始到其父建構函式開始之間的時間。因此,這些連線點的執行包含評估this()和super()建構函式呼叫的引數的連線點。沒有值從物件預初始化連線點返回,所以它的返回型別被認為是無效的。
Object initialization 當一個特定類的物件初始化程式碼執行時。這包含了返回其父建構函式和返回其第一個建構函式之間的時間。它包含所有用於建立物件的動態初始化器和建構函式。正在構造的物件是當前正在執行的物件,因此可以使用此切入點來訪問。沒有值從建構函式執行連線點返回,所以它的返回型別被認為是無效的。
Field reference 引用非常量欄位時。 [請注意,對常量欄位(繫結到常量字串物件或原始值的靜態最終欄位)的引用不是連線點,因為Java要求將它們內聯。]
Field set 當一個欄位被分配給。 欄位集連線點被認為有一個引數,該欄位被設定為的值。 沒有值從欄位集合連線點返回,所以它的返回型別被認為是無效的。 [請注意,初始化常量欄位(初始化程式為常量字串物件或原始值的靜態最終欄位)不是連線點,因為Java要求將其引用內聯。]
Handler execution 當一個異常處理程式執行。 處理程式執行連線點被認為有一個引數,正在處理異常。 沒有值從欄位集合連線點返回,所以它的返回型別被認為是無效的。
Advice execution 當一條通知的程式碼體執行時。

1.2 切入點(Pointcuts)

官方給出的介紹是:A pointcut is a program element that picks out join points and exposes data from the execution context of those join points。那麼我對 pointcuts 的理解就是過濾規則+結果集,符合AespectJ規則的連線點會有很多,我們需要使用過濾規則來濾出我們關心的所有連結點,這裡的過濾規則即切入點。

原始Pointcuts 解釋 對應Join Points 示例
call (MethodPattern) 捕獲簽名與MethodPattern匹配的方法呼叫連線點 Method call call(* android.app.Activity.on**(..))
execution (MethodPattern) 捕獲簽名與MethodPattern匹配的方法執行連線點 Method execution execution(* android.support.v4.app.Fragment.on**(..))
call (ConstructorPattern) 捕獲簽名與ConstructorPattern匹配的建構函式呼叫連線點。 Constructor call call(*.new(int, int))
execution (ConstructorPattern) 捕獲簽名與ConstructorPattern匹配的建構函式執行連線點。 Constructor execution 類似方法執行
get (FieldPattern) 捕獲簽名與FieldPattern匹配的欄位引用連線點。 [請注意,對常量欄位(繫結到常量字串物件或原始值的靜態最終欄位)的引用不是連線點,因為Java要求將它們內聯。] Field reference
set (FieldPattern) 捕獲簽名與FieldPattern匹配的欄位集連線點。 [請注意,初始化常量欄位(初始化程式為常量字串物件或原始值的靜態最終欄位)不是連線點,因為Java要求將其引用內聯。] Field set
initialization(ConstructorPattern) 捕獲簽名與ConstructorPattern匹配的物件初始化連線點。 Object initialization
preinitialization(ConstructorPattern) 捕獲簽名與ConstructorPattern匹配的物件預初始化連線點。 Object pre-initialization
staticinitialization(TypePattern) 捕獲簽名與TypePattern匹配的靜態初始化執行連線點。 Static initializer execution
handler(TypePattern) 捕獲簽名與TypePattern匹配的異常處理連線點。 Handler execution handler(ArrayOutOfBoundsException)
組合Pointcuts 解釋 示例
within(TypePattern) 捕獲其執行的程式碼是在TypePattern匹配的型別中定義的連線點(注:這裡表示的是包名或者類名,也就是說與之匹配的包/類下所有符合規則的連線點) within(com.skx.tomike.fragment.business.*) within(com.skx.tomike.fragment.business.CatalogFragment)
withincode(ConstructorPattern l MethodPattern) 捕獲其執行程式碼是在簽名與MethodPattern匹配的方法中定義的連線點。 withincode(void m())
cflow(Pointcut) 捕獲在控制流中符合切入點的所有連線點,包括本身 cflow(call(void Test.main()))
this(Type or Id) 捕獲當前正在執行的Type物件例項(繫結到此的物件)或識別符號Id的型別(必須在封閉通知或切入點定義中繫結)的連線點。 不會匹配來自靜態上下文的任何連線點。 this(SomeType)
target(Type or Id) 捕獲目標物件(應用了呼叫或欄位操作的物件)是Type的例項或識別符號Id的型別(必須在封閉通知或切入點定義中繫結)的連線點,。 不匹配任何呼叫,獲取或靜態成員集。 target(com.skx.tomike.activity.function.AopTestActivity)
args(Type or Id, …)
if(BooleanExpression) 捕獲布林表示式計算結果為true的連線點
! Pointcut 捕獲Pointcut未選取的連線點。 !this(Point)
Pointcut0 && Pointcut1 捕獲滿足與條件的連線點 target(Point) && call(int *())
Pointcut0 ll Pointcut1 捕獲滿足或條件的連線點 call(void setX(int)) ll call(void setY(int))
( Pointcut )

程式碼中定義切入點


    @Pointcut("within(com.skx.tomike.fragment.business.CatalogFragment) && execution(*  android.support.v4.app.Fragment.on**(..))")
    public void pointcutTest() {
    }

解析:

  1. @Pointcut 用來宣告這是一個切入點
  2. pointcutTest 是我們定義的切入點的名稱,具體的命名規則看你了。
  3. within 和 execution 都是切入點的定義規則 。第一個規則表示com.skx.tomike.fragment.business.CatalogFragment 這個包下定義的所有連線點;第二個規則表示Fragment下所有匹配on**規則的方法。&& 是關係符,表示與。類似的還有 || 表示或、!表示非。
  4. “ * android.support.v4.app.Fragment.on**(..)” 前面的 * 號是萬用字元表示匹配任何返回值;後面的 * * 用來匹配名稱的;(..) 用來匹配形參的,.. 表示匹配任意型別。

思考兩個問題:
1.如果我把第一個條件 “ within(com.skx.tomike.fragment.business.CatalogFragment) ”
替換成 “ within(com.skx.tomike.fragment.business.*) ” 會有什麼變化?
2. 下面這個切入點如何理解?


    @Pointcut("call(@com.skx.tomike.aop.LogRecordAnnotation * *(..))")
    public void methodAnnotatedWithDebugTrace() {
    }

注意: AspectJ本身就是一種編譯期的AOP,檢查程式碼並匹配連線點於切入點的代價是昂貴的,所以,定義切入點的時候,盡量使用精確的匹配規則來降低匹配時間,減少不必要的資源浪費。那麼,如何定義一個好的切入點呢?一個好的切入點應該包含以下幾個方面:

  1. 選擇特定型別的連線點:如:execution, get, set, call handler
  2. 確定連線點範圍,如within ,withincode
  3. 匹配上下文資訊,如:this,target,@annotation

除了在切入點上規範之外還可以通過排除不需要掃描的包來降低匹配時間
關於連線點(Join Point)和切入點(pointcuts)我找個了例子幫助理解,插排:連線點就是提供的所有可用的插孔,切入點是某一個裝置連結到的某個特定的連線點。

1.3 通知(Advice)

Advice定義了橫切行為,它是根據切入點定義的,不同的通知型別決定了插入的程式碼片段與切入點所選擇的每個連線點進行何種方式的互動。AspectJ支援三種類型的advice:

  • Before:其連線點之前執行
  • After:在連線點之後執行
  • Around:代替(或“圍繞”)連線點執行

注意:after advice 詳細分有三種情況,包括在連線點執行完成後,在丟擲異常之後,或者在執行完一個異常之後。

  • after( Formals )
  • after( Formals ) returning [ ( Formal ) ]
  • after( Formals ) throwing [ ( Formal ) ]

    @After("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
        Log.e(TAG, "after  - 在Activity 的on**()方法體之後執行");
    }

    // 注意: 這裡的pointcutTest() 表示的切入點,其意義和直接在advice後面寫匹配公式是一樣的
    @Pointcut("execution(* android.app.Activity.on**(..))")
    public void pointcutTest() {
    }

    @Before("pointcutTest()")
    public void onActivityMethodWithin(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.e(TAG, "before - 在 " + key + " 方法體之前執行");
    }

兩種寫法:一種是由advice直接接入切入點的規則;一種是先用@Pointcut 定義好切入點方法,然後由advice接入定義好的切入點方法。這兩種寫法都沒毛病,第二種方法我認為在條件比較多的情況下更實用些,因為切入點組合變化也更靈活些。

2. Join points與Pointcuts和advice之前的關係

之前在看了一張圖,很明確的表示了Join points與Pointcuts和advice之前的關係。
AspectJ Join points與Pointcuts和advice關係圖
pointcuts和advice 搭配組成了一個切面。之前我用插排來舉例,現在我來分析下,程式裡的連線點好比是插排裡的插孔,有三孔插口、兩孔插口、USB插口。什麼是切入點,如果是一個USB的連線裝置,那麼切入點就是所有的USB插口。現在加一個規則,總共有3個USB插口,但是最右邊的那個你不能用,那麼現在的切入點是不是相當於:所有的USB插孔 && !最右邊的USB插孔 ?

AOP實現之AspectJ pointcuts的理解

自定義註解型別的Pointcuts

以log輸出為例:
1.定義註解

package com.skx.tomike.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Log註解
 */
@Retention(RetentionPolicy.CLASS)//編譯時註解
@Target({ElementType.METHOD})
public @interface LogRecordAnnotation {
    /**
     * @Target說明了Annotation所修飾的物件範圍:Annotation可被用於 packages、types(類、介面、列舉、Annotation型別)、
     * 型別成員(方法、構造方法、成員變數、列舉值)、方法引數和本地變數(如迴圈變數、catch引數)。
     * 在Annotation型別的宣告中使用了target可更加明晰其修飾的目標。
     *
     * 作用:用於描述註解的使用範圍(即:被描述的註解可以用在什麼地方)

      取值(ElementType)有:
        1.CONSTRUCTOR:用於描述構造器
        2.FIELD:用於描述域
        3.LOCAL_VARIABLE:用於描述區域性變數
        4.METHOD:用於描述方法
        5.PACKAGE:用於描述包
        6.PARAMETER:用於描述引數
        7.TYPE:用於描述類、介面(包括註解型別) 或enum宣告
     */
}

2.定義切面類、切面方法和要插入的程式碼

package com.skx.tomike.aop;

import android.util.Log;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;


/**
 * Created by shiguotao on 2018/3/22.
 */
@Aspect
public class LogAspect {

    private static final String TAG = "LogAspect";

    private static final String POINTCUT_METHOD = "call(@com.skx.tomike.aop.LogRecordAnnotation * *(..))";

    @Pointcut(POINTCUT_METHOD)
    public void logPointcuts() {
    }

    @Before("logPointcuts()")
    public void logPointcutsTest(ProceedingJoinPoint joinPoint) throws Throwable {
        // 這裡是插入的程式碼
        Log.e(TAG, "aop 測試!");
    }

}

3.在需要log記錄的地方使用註解


    @LogRecordAnnotation
    private void testAOP() {
        Log.e("testAOP", "11111111");
    }

參考連結