Android AOP面向切面編程詳解

歡迎閱讀系列文章

Android aop切點表達式(execution)
Android aop Advice(通知、增強)
Android aop(AspectJ)查看新的代理類
Android aop AspectJX與第三方庫沖突的解決方案
Android AOP面向切面編程詳解
防止按鈕連續點擊
Android aop工作原理

項目需求描述

個人中心.jpg

我想類似于這樣的個人中心的界面,大家都不會陌生吧。那幾個有箭頭的地方都是可以點擊進行頁面跳轉的,但是需要先判斷用戶是否登錄,如果已經登錄,則正常跳轉,如果沒有登錄,則跳轉到登錄頁面先登錄,但凡是有注冊,登錄的APP,這樣的操作,大家應該都很熟悉吧。一般情況下,我們的邏輯是這樣的...

/**
     * 跳轉到我的關注頁面
     */
    public void toMyAttention() {
        // 判斷當前用戶是否登錄
        if(LoginHelper.isLogin(this)) {
            // 如果登錄才跳轉,進入我的關注頁面
            Intent intent = new Intent(this, WaitReceivingActivity.class);
            startActivity(intent);
        }else{
          //跳轉到登錄頁面,先登錄
            Intent intent = new Intent(this, LoginActivity.class);
            startActivity(intent);
        }
    }

這段代碼確實沒有任何問題,也完全符合我們的需求,也就是在所有需要判斷登錄的地方去if else做重復的邏輯操作,但是如果這樣的判斷有10多處,甚至幾十處,我們就得重復很多次這樣的體力勞動,或者有一天需求變動,我們估計要改動多處,想想都可怕。而且類似的還有網絡判斷,權限管理,Log日志的統一管理這樣的問題。那么,我們也沒有更優雅的方式來解決這一類的問題呢,答案是有的,煩請各位接著往下看。

先給出我解決了上述問題之后的代碼

/**
    *  跳轉到我的關注頁面
    */
    @CheckLogin
    public void toMyAttention() {
         Intent intent = new Intent(this, WaitReceivingActivity.class);
         startActivity(intent);
    }

大家也看到了,代碼變得簡潔了,而且重復的操作越多,優勢越明顯,更重要的是,方便需求改變時候的修改,便于維護。在這里,我通過一個@CheckLogin注解的方式就去除了判斷登錄這樣的操作,這是什么個情況?這就是今天為大家帶來的Android AOP(面向切面編程)詳解。接下來,我先為大家帶來AOP的一些基礎概念,再來講解具體的實現方式。

什么是AOP

AOP是Aspect Oriented Programming的縮寫,即『面向切面編程』。它和我們平時接觸到的OOP都是編程的不同思想,OOP,即『面向對象編程』,它提倡的是將功能模塊化,對象化,而AOP的思想,則不太一樣,它提倡的是針對同一類問題的統一處理,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生范型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

AspectJ

AspectJ實際上是對AOP編程思想的一個實踐,AOP雖然是一種思想,但就好像OOP中的Java一樣,一些先行者也開發了一套語言來支持AOP。目前用得比較火的就是AspectJ了,它是一種幾乎和Java完全一樣的語言,而且完全兼容Java(AspectJ應該就是一種擴展Java,但它不是像Groovy那樣的拓展。)。當然,除了使用AspectJ特殊的語言外,AspectJ還支持原生的Java,只要加上對應的AspectJ注解就好。所以,使用AspectJ有兩種方法:

  • 完全使用AspectJ的語言。這語言一點也不難,和Java幾乎一樣,也能在AspectJ中調用Java的任何類庫。AspectJ只是多了一些關鍵詞罷了。
  • 或者使用純Java語言開發,然后使用AspectJ注解,簡稱@AspectJ

基礎概念

  • Aspect 切面:切面是切入點和通知的集合。

  • PointCut 切入點:切入點是指那些通過使用一些特定的表達式過濾出來的想要切入Advice的連接點。

  • Advice 通知:通知是向切點中注入的代碼實現方法。

  • Joint Point 連接點:所有的目標方法都是連接點.

  • Weaving 編織:主要是在編譯期使用AJC將切面的代碼注入到目標中, 并生成出代碼混合過的.class的過程.

實踐步驟

1、在android studio中直接配置AspectJ,這個配置很重要,如果失敗,后面就無法成功,先貼出我的配置,在app的build.gradle中做如下配置

apply plugin: 'com.android.application'
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'
    }
}
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;
            }
        }
    }
}

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.zx.aopdemo"
        minSdkVersion 17
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'org.aspectj:aspectjrt:1.8.9'
    testCompile 'junit:junit:4.12'
}

為什么這么配置?因為AspectJ是對java的擴展,而且是完全兼容java的。但是編譯時得用Aspect專門的編譯器,這里的配置就是使用Aspect的編譯器,單獨加入aspectj依賴是不行的。到這里準備工作已完成,可以開始看看具體實現了。

注意:這種自定義Gradle插件的方式很麻煩,容易出錯,不再推薦使用這種方式,推薦滬江的集成方案aspectjx

2、創建切面AspectJ
用來處理觸發切面的回調

@Aspect
public class CheckLoginAspectJ {
    private static final String TAG = "CheckLogin";

    /**
     * 找到處理的切點
     * * *(..)  可以處理CheckLogin這個類所有的方法
     */
    @Pointcut("execution(@com.zx.aopdemo.login.CheckLogin  * *(..))")
    public void executionCheckLogin() {
    }

    /**
     * 處理切面
     *
     * @param joinPoint
     * @return
     */
    @Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i(TAG, "checkLogin: ");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
        if (checkLogin != null) {
            Context context = (Context) joinPoint.getThis();
            if (MyApplication.isLogin) {
                Log.i(TAG, "checkLogin: 登錄成功 ");
                return joinPoint.proceed();
            } else {
                Log.i(TAG, "checkLogin: 請登錄");
                Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
                return null;
            }
        }
        return joinPoint.proceed();
    }
}

這里要使用Aspect的編譯器編譯必須給類打上標注,@Aspect。
還有這里的Pointcut注解,就是切點,即觸發該類的條件。里面的字符串如下

AspectJ中的Join Point.png

在Pointcut這里,我使用了execution,也就是以方法執行時為切點,觸發Aspect類。而execution里面的字符串是觸發條件,也是具體的切點。我來解釋一下參數的構成。“execution(@com.zx.aopdemo.login.CheckLogin * *(..))”這個條件是所有加了CheckLogin注解的方法或屬性都會是切點,范圍比較廣。

  • **:表示是任意包名
  • ..:表示任意類型任意多個參數

“com.zx.aopdemo.login.CheckLogin”這是我的項目包名下需要指定類的絕對路徑。再來看看@Around,Around是指JPoint執行前或執行后被觸發,除了Around還有其他幾種方式。

類型 描述
Before 前置通知, 在目標執行之前執行通知
After 后置通知, 目標執行后執行通知
Around 環繞通知, 在目標執行中執行通知, 控制目標執行時機
AfterReturning 后置返回通知, 目標返回時執行通知
AfterThrowing 異常通知, 目標拋出異常時執行通知

創建完Aspect類之后,還需要一個注解類,它的作用是:哪里需要做切點,那么哪里就用注解標注一下,這樣方便快捷。

3、創建注解類

package com.zx.aopdemo.login;

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

@Target(ElementType.METHOD) //可以注解在方法 上
@Retention(RetentionPolicy.RUNTIME) //運行時(執行時)存在
public @interface CheckLogin {
}

4、Activity使用登錄的注解

public class LoginActivity extends AppCompatActivity implements View.OnClickListener, RadioGroup.OnCheckedChangeListener {

    private RadioGroup radioGroup;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

       test();
    }

    @CheckLogin
   public void test(){  
        Log.i("tag","判斷是否登錄");  
    }  

test()方法執行時就是一個切點。在執行test()時,會回調上面的CheckLoginAspectJ類的executionCheckLogin()方法。然后會執行
如下方法

 /**
     * 處理切面
     *
     * @param joinPoint
     * @return
     */
    @Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i(TAG, "checkLogin: ");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
        if (checkLogin != null) {
            Context context = (Context) joinPoint.getThis();
            if (MyApplication.isLogin) {
                Log.i(TAG, "checkLogin: 登錄成功 ");
                return joinPoint.proceed();
            } else {
                Log.i(TAG, "checkLogin: 請登錄");
                Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
                return null;
            }
        }
        return joinPoint.proceed();
    }

如果使用的是以方法相關為切點,那么使用MethodSignature來接收joinPoint的Signature。如果是屬性或其他的,那么可以使用Signature類來接收。之后可以使用Signature來獲取注解類。,那么通過jointPoint.getThis()獲取使用該注解的的上下文對象

GitHub地址(歡迎下載完整Demo)
https://github.com/zhouxu88/AOPDemo

參考如下文章,感謝作者:
aspect-oriented-programming-in-android

翻譯 Android中的AOP編程

Android 開發中使用 AOP

深入理解Android之AOP

Android基于AOP的非侵入式監控之——AspectJ實戰

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容