AOP之AspectJ

aop實現(xiàn)的三大方式(反射 (xutil) apt注解(ButterKnife) aspect (本文即將講到的)) 說出各自的優(yōu)缺點

一、AOP概念

百度百科中對AOP的解釋如下:
在軟件業(yè),AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術。

AOP是OOP的延續(xù),是軟件開發(fā)中的一個熱點,也是很多框架如 java中的Spring框架中的一個重要內(nèi)容,是函數(shù)式編程的一種衍生范型。 利用AOP可以對業(yè)務邏輯的各個部分進行隔離,從而使得業(yè)務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。

AOP只是一種思想的統(tǒng)稱,實現(xiàn)這種思想的方法有挺多。AOP通過預編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術。利用AOP可以對業(yè)務邏輯的各個部分進行隔離,從而使得業(yè)務邏輯各部分之間的耦合度降低,提高程序的可重用性,提高開發(fā)效率。

(1)AOP與OOP的關系

OOP(面向?qū)ο缶幊蹋?/strong>針對業(yè)務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。但是也有它的缺點,最明顯的就是關注點聚焦時,面向?qū)ο鬅o法簡單的解決這個問題,一個關注點是面向所有而不是單一的類,不受類的邊界的約束,因此OOP無法將關注點聚焦來解決,只能分散到各個類中。
AOP(面向切面編程)則是針對業(yè)務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有著本質(zhì)的差異。
AOP并不是與OOP對立的,而是為了彌補OOP的不足。OOP解決了豎向的問題,AOP則解決橫向的問題。因為有了AOP我們的調(diào)試和監(jiān)控就變得簡單清晰。

簡單的來講,AOP是一種:可以在不改變原來代碼的基礎上,通過“動態(tài)注入”代碼,來改變原來執(zhí)行結(jié)果的技術。

(2)AOP主要應用場景

日志記錄,性能統(tǒng)計,安全控制,事務處理,異常處理等等。

(3)主要目標

將日志記錄,性能統(tǒng)計,安全控制,事務處理,異常處理等代碼從業(yè)務邏輯代碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非指導業(yè)務邏輯的方法中,進而改變這些行為的時候不影響業(yè)務邏輯的代碼。

[圖片上傳失敗...(image-f77692-1591768775090)]

上圖是一個APP模塊結(jié)構(gòu)示例,按照照OOP的思想劃分為“視圖交互”,“業(yè)務邏輯”,“網(wǎng)絡”等三個模塊,而現(xiàn)在假設想要對所有模塊的每個方法耗時(性能監(jiān)控模塊)進行統(tǒng)計。這個性能監(jiān)控模塊的功能就是需要橫跨并嵌入眾多模塊里的,這就是典型的AOP的應用場景。

AOP的目標是把這些橫跨并嵌入眾多模塊里的功能(如監(jiān)控每個方法的性能) 集中起來,放到一個統(tǒng)一的地方來控制和管理。如果說,OOP如果是把問題劃分到單個模塊的話,那么AOP就是把涉及到眾多模塊的某一類問題進行統(tǒng)一管理。

對比:

功能 OOP AOP
增加日志 所有功能模塊單獨添加,容易出錯 能夠?qū)⑼粋€關注點聚焦在一處解決
修改日志 功能代碼分散,不方便調(diào)試 能夠?qū)崿F(xiàn)一處修改,處處生效

例如:在不改變 main 方法的同時通過代碼注入的方式達到目的

/**
 * Before
 */
public class Test {
    public static void main(String[] args) {
        // do something
    }
}

/**
 * After
 */
public class Test {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        // do something
        long end = System.currentTimeMillis() - start;
    }
}

二、AOP代碼注入時機

代碼注入主要注解機制,根據(jù)注解時機的不同,主要分為運行時、加載時和編譯時。

運行時:你的代碼對增強代碼的需求很明確,比如,必須使用動態(tài)代理(這可以說并不是真正的代碼注入)。
加載時:當目標類被Dalvik或者ART加載的時候修改才會被執(zhí)行。這是對Java字節(jié)碼文件或者Android的dex文件進行的注入操作。
編譯時:在打包發(fā)布程序之前,通過向編譯過程添加額外的步驟來修改被編譯的類。aspect切面編程正是運用到編譯時

三、AOP的幾種實現(xiàn)方式

  • Java 中的動態(tài)代理,運行時動態(tài)創(chuàng)建 Proxy 類實例
  • APT,注解處理器,編譯時生成 .java 代碼
  • Javassist for Android:一個移植到Android平臺的非常知名的操縱字節(jié)碼的java庫,對 class 字節(jié)碼進行修改
  • AspectJ:和Java語言無縫銜接的面向切面的編程的擴展工具(可用于Android)。

四,Android中使用 AspectJ

代表項目:Hugo(打印每個方法的執(zhí)行時間) sa-sdk-android(全埋點技術)

(1)原理

AspectJ 意思就是Java的Aspect,Java的AOP。它的核心是ajc(編譯? aspectjtools)和 weaver(織入? aspectjweaver)。

ajc編譯?:基于Java編譯?之上的,它是用來編譯.aj文件,aspectj在Java編譯?的基礎上增加了一些它自己的關鍵字和方法。因此,ajc也可以編譯Java代碼。

weaver織入?:為了在java編譯?上使用AspectJ而不依賴于Ajc編譯?,aspectJ 5出現(xiàn)了 @AspectJ,使用注釋的方式編寫AspectJ代碼,可以在任何Java編譯?上使用。
由于AndroidStudio默認是沒有ajc編譯?的,所以在Android中使用@AspectJ來編寫。它在代碼的編譯期間掃描目標程序,根據(jù)切點(PointCut)匹配,將開發(fā)者編寫的Aspect程序編織(Weave)到目標程序的.class文件中,對目標程序作了重構(gòu)(重構(gòu)單位是JoinPoint),目的就是建立目標程序與Aspect程序的連接(獲得執(zhí)行的對象、方法、參數(shù)等上下文信息),從而達到AOP的目的。

(2)AspectJ 術語

切面(Aspect):一個關注點的模塊化,這個關注點實現(xiàn)可能另外橫切多個對象。其實就是共有功能的實現(xiàn)。如日志切面、權(quán)限切面、事務切面等。

通知(Advice):是切面的具體實現(xiàn)。以目標方法為參照點,根據(jù)放置的地方不同,可分為

  1. 前置通知(Before)、
  2. 后置通知(AfterReturning)、
  3. 異常通知(AfterThrowing)、
  4. 最終通知(After)
  5. 環(huán)繞通知(Around)5種。

在實際應用中通常是切面類中的一個方法,具體屬于哪類通知由配置指定的。

切入點(Pointcut):用于定義通知應該切入到哪些連接點上。不同的通知通常需要切入到不同的連接點上,這種精準的匹配是由切入點的正則表達式來定義的。
連接點(JoinPoint):就是程序在運行過程中能夠插入切面的地點。例如,方法調(diào)用、異常拋出或字段修改等。

目標對象(Target Object):包含連接點的對象,也被稱作被通知或被代理對象。這些對象中已經(jīng)只剩下干干凈凈的核心業(yè)務邏輯代碼了,所有的共有功能等代碼則是等待AOP容器的切入。

AOP代理(AOP Proxy):將通知應用到目標對象之后被動態(tài)創(chuàng)建的對象??梢院唵蔚乩斫鉃?,代理對象的功能等于目標對象的核心業(yè)務邏輯功能加上共有功能。代理對象對于使用者而言是透明的,是程序運行過程中的產(chǎn)物。

編織(Weaving):將切面應用到目標對象從而創(chuàng)建一個新的代理對象的過程。這個過程可以發(fā)生在編譯期、類裝載期及運行期,當然不同的發(fā)生點有著不同的前提條件。譬如發(fā)生在編譯期的話,就要求有一個支持這種AOP實現(xiàn)的特殊編譯器(如AspectJ編譯器);

發(fā)生在類裝載期,就要求有一個支持AOP實現(xiàn)的特殊類裝載器;只有發(fā)生在運行期,則可直接通過Java語言的反射機制與動態(tài)代理機制來動態(tài)實現(xiàn)(如搖一搖)。

引入(Introduction):添加方法或字段到被通知的類。

(3)在Android項目中使用AspectJ
  • gradle配置的方式:引入AspectJ是有點復雜的,需要引入大量的gradle命令配置有點麻煩,在build文件中添加了一些腳本,文章出處:https://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
  • 使用 gradle 插件(也是對 gradle 命令進行了包裝):Jake Wharton 大神的 hugo 項目(一款日志打印的插件)

上海滬江團隊的 gradle_plugin_android_aspectjx 一個基于AspectJ并在此基礎上擴展出來可應用于Android開發(fā)平臺的AOP框架,可作用于java源碼,class文件及jar包,同時支持kotlin的應用。

AOP的用處非常廣,從spring到Android,各個地方都有使用,特別是在后端,Spring中已經(jīng)使用的非常方便了,而且功能非常強大,但是在Android中,AspectJ的實現(xiàn)是略閹割的版本,并不是所有功能都支持,但對于一般的客戶端開發(fā)來說,已經(jīng)完全足夠用了。

(4)以 AspectJX 接入說明
  • 首先,需要在項目根目錄的build.gradle中增加依賴:
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
    }
}
  • 然后module項目的 build.gradle 中加入 AspectJ 的依賴:
apply plugin: 'android-aspectjx'
dependencies {
        compile 'org.aspectj:aspectjrt:1.8.+'
    }

aspectjx {
    //排除所有package路徑中包含`android.support`的class文件及庫(jar文件)
    exclude 'org.apache.httpcomponents'
    exclude 'android.support'
}
@Aspect
public class AspectTest {

    private static final String TAG = "xuyisheng";

    @Before("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.e(TAG, "onActivityMethodBefore: " + key);
    }

    @After("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.e(TAG, "onActivityMethodAfter: " + key);
    }

    @Around("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodAfter(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.e(TAG, "onActivityMethodBefore: " + key);
        joinPoint.proceed();
        Log.e(TAG, "onActivityMethodAfter: " + key);
    }
}

在類的最開始,我們使用 @Aspect 注解來定義這樣一個AspectJ文件,編譯器在編譯的時候,就會自動去解析,并不需要主動去調(diào)用AspectJ類里面的代碼。

具體的切面表達式可以參考execution表達式解析

(5)編織速度優(yōu)化建議
  • 盡量使用精確的匹配規(guī)則,降低匹配時間。
  • 排除不需要掃描的包。

通過這種方式編譯后,我們來看下生成的代碼是怎樣的。AspectJ的原理實際上是在編譯的時候,根據(jù)一定的規(guī)則解析,然后插入一些代碼,通過aspectj生成的代碼,會在Build目錄下:

[圖片上傳失敗...(image-fac24-1591768775090)]

四、總結(jié):

Aspectj:
  • AspectJ除了hook之外,AspectJ還可以為目標類添加變量,接口。另外,AspectJ也有抽象,繼承等各種更高級的玩法。它能夠在編譯期間直接修改源代碼生成class。
  • AspectJ語法比較多,但是掌握幾個簡單常用的,就能實現(xiàn)絕大多數(shù)切片,完全兼容Java(純Java語言開發(fā),然后使用AspectJ注解,簡稱@AspectJ。)

Kotlin無法生效的問題請參考此處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容