Android AOP 面向切面編程實(shí)現(xiàn)方案

前言:

在我們程序開(kāi)發(fā)中,嘗嘗會(huì)遇到同一事件多個(gè)地方調(diào)用的情況,比如說(shuō)用戶行為統(tǒng)計(jì),登錄狀態(tài)檢查,返回鍵事件處理,如果這些情況使用常規(guī)的OOP方式去實(shí)現(xiàn),勢(shì)必會(huì)造成大量重復(fù)代碼,而且耦合性也比較高,所以我們是否有其他更好的方式去實(shí)現(xiàn)呢,這就是我們今天要了解的AOP實(shí)現(xiàn)方案,它可以將多個(gè)同一事件統(tǒng)一處理,實(shí)現(xiàn)切面效果。
Github下載地址:
https://github.com/VincentStory/AopProject

1.首先要了解什么是AOP

AOP是Aspect Oriented Programming的縮寫,即“面向切面編程”,就是針對(duì)同一類問(wèn)題的統(tǒng)一處理,通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開(kāi)發(fā)的效率。

2.了解一下AspectJ

AspectJ是對(duì)AOP編程思想的一個(gè)實(shí)踐方案,AOP是一種思想,它在各個(gè)語(yǔ)言中都有各自的實(shí)踐方案,那AspectJ是Java中比較火的實(shí)踐方案,它可以完全兼容Java。當(dāng)然,除了使用AspectJ特殊的語(yǔ)言外,AspectJ還支持原生的Java,只要加上對(duì)應(yīng)的AspectJ注解就好。所以,使用AspectJ有兩種方法:

(1)完全使用AspectJ的語(yǔ)言,和Java幾乎一樣,也能在AspectJ中調(diào)用Java的任何類庫(kù)。AspectJ只是多了一些關(guān)鍵詞。
(2)使用純Java語(yǔ)言開(kāi)發(fā),然后使用AspectJ注解,簡(jiǎn)稱@AspectJ。

基礎(chǔ)概念:
Aspect 切面:切面是切入點(diǎn)和通知的集合。

PointCut 切入點(diǎn):切入點(diǎn)是指那些通過(guò)使用一些特定的表達(dá)式過(guò)濾出來(lái)的想要切入Advice的連接點(diǎn)。

Advice 通知:通知是向切點(diǎn)中注入的代碼實(shí)現(xiàn)方法。

Joint Point 連接點(diǎn):所有的目標(biāo)方法都是連接點(diǎn).

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

3.具體實(shí)踐

(1)首先在project中的build.gradle中配置AspectJ,如下:

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.2"

        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

(2)在app的build.gradle中配置AspectJ編譯器

buildscript { // 編譯時(shí)用Aspect專門的編譯器,不再使用傳統(tǒng)的javac
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

plugins {
    id 'com.android.application'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.vincent.aop.project"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

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 {

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation 'org.aspectj:aspectjrt:1.8.13'
}

(3)創(chuàng)建切面AspectJ,具體處理切面統(tǒng)一事件


@Aspect // 定義切面類
public class LoginCheckAspect {

    private final static String TAG = "aop_tag >>> ";

    // 1、應(yīng)用中用到了哪些注解,放到當(dāng)前的切入點(diǎn)進(jìn)行處理(找到需要處理的切入點(diǎn))
    // execution,以方法執(zhí)行時(shí)作為切點(diǎn),觸發(fā)Aspect類
    // * *(..)) 可以處理ClickBehavior這個(gè)類所有的方法
    @Pointcut("execution(@com.vincent.aop.project.annotation.LoginCheck * *(..))")
    public void pointCut() {
    }

    // 2、對(duì)切入點(diǎn)如何處理
    @Around("pointCut()")
    public Object joinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.獲取切入點(diǎn)所在目標(biāo)對(duì)象
        Object targetObj =joinPoint.getTarget();
        Log.e(TAG, "類名:"+targetObj.getClass().getName());
        // 2.獲取切入點(diǎn)方法的名字
        String methodName = joinPoint.getSignature().getName();
        Log.e(TAG, "切入方法名字:"+methodName);
        // 3. 獲取方法上的注解
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null)
        {
            LoginCheck apiLog=  method.getAnnotation(LoginCheck.class);
            Log.e(TAG, "切入方法注解的title:"+apiLog.title());
        }

        //4. 獲取方法的參數(shù)
        Object[] args = joinPoint.getArgs();
        for(Object o :args){
            Log.e(TAG, "切入方法的參數(shù):"+o);
        }

        Context context = (Context) joinPoint.getThis();
        if (MyApplication.isLogin()) {
            Log.e(TAG, "檢測(cè)到已登錄!跳轉(zhuǎn)其他頁(yè)面");
            return joinPoint.proceed();
        } else {
            Log.e(TAG, "檢測(cè)到未登錄!");
            Toast.makeText(context, "請(qǐng)先登錄!", Toast.LENGTH_SHORT).show();
            context.startActivity(new Intent(context, LoginActivity.class));
            return null; // 不再執(zhí)行方法(切入點(diǎn))
        }
    }
}

上面除了登錄檢查的邏輯還添加了一些獲取類名,方法名,方法參數(shù)值,注解參數(shù)值的方法,根據(jù)項(xiàng)目需要來(lái)具體選擇是否獲取。
(3)在Activity頁(yè)面使用AspectJ

  @LoginCheck
    public void myInfomation(View view) {
        Log.e(TAG, "開(kāi)始跳轉(zhuǎn)到 -> 我的資料 Activity");
    }

    @LoginCheck
    public void myMoney(View view) {
        Log.e(TAG, "開(kāi)始跳轉(zhuǎn)到 -> 我的余額 Activity");
    }

    public void myScore(View view) {
        checkLogin("參數(shù)1", "參數(shù)2");
    }

    @LoginCheck(title = "登錄檢查", isSaveRequestData = false)
    public void checkLogin(String str, String str2) {
        Log.e(TAG, "開(kāi)始跳轉(zhuǎn)到 -> 我的積分 Activity");
    }

點(diǎn)擊跳轉(zhuǎn)之后打印信息如下:

2021-11-15 10:53:43.930 6665-6665/com.vincent.aop.project E/aop_tag >>>: 類名:com.vincent.aop.project.MainActivity
2021-11-15 10:53:43.931 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法名字:checkLogin
2021-11-15 10:53:43.932 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法注解的title:登錄檢查
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法注解的isSaveRequestData:false
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法的參數(shù):參數(shù)1
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法的參數(shù):參數(shù)2
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 檢測(cè)到未登錄!
4.總結(jié):

通過(guò)上面的例子我們可以發(fā)現(xiàn),AOP解決方案可以使用很簡(jiǎn)潔的代碼實(shí)現(xiàn)我們想要的效果,而且對(duì)原有代碼毫無(wú)入侵性,理解這一原理,可以在工作中更好的以架構(gòu)師的思維去編寫代碼。

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

相關(guān)閱讀更多精彩內(nèi)容

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