Android AOP 面向切面编程实现方案

前言:

在我们程序开发中,尝尝会遇到同一事件多个地方调用的情况,比如说用户行为统计,登录状态检查,返回键事件处理,如果这些情况使用常规的OOP方式去实现,势必会造成大量重复代码,而且耦合性也比较高,所以我们是否有其他更好的方式去实现呢,这就是我们今天要了解的AOP实现方案,它可以将多个同一事件统一处理,实现切面效果。
Github下载地址:
https://github.com/VincentStory/AopProject

1.首先要了解什么是AOP

AOP是Aspect Oriented Programming的缩写,即“面向切面编程”,就是针对同一类问题的统一处理,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

2.了解一下AspectJ

AspectJ是对AOP编程思想的一个实践方案,AOP是一种思想,它在各个语言中都有各自的实践方案,那AspectJ是Java中比较火的实践方案,它可以完全兼容Java。当然,除了使用AspectJ特殊的语言外,AspectJ还支持原生的Java,只要加上对应的AspectJ注解就好。所以,使用AspectJ有两种方法:

(1)完全使用AspectJ的语言,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词。
(2)使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ。

基础概念:
Aspect 切面:切面是切入点和通知的集合。

PointCut 切入点:切入点是指那些通过使用一些特定的表达式过滤出来的想要切入Advice的连接点。

Advice 通知:通知是向切点中注入的代码实现方法。

Joint Point 连接点:所有的目标方法都是连接点.

Weaving 编织:主要是在编译期使用AJC将切面的代码注入到目标中, 并生成出代码混合过的.class的过程

3.具体实践

(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 { // 编译时用Aspect专门的编译器,不再使用传统的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)创建切面AspectJ,具体处理切面统一事件


@Aspect // 定义切面类
public class LoginCheckAspect {

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

    // 1、应用中用到了哪些注解,放到当前的切入点进行处理(找到需要处理的切入点)
    // execution,以方法执行时作为切点,触发Aspect类
    // * *(..)) 可以处理ClickBehavior这个类所有的方法
    @Pointcut("execution(@com.vincent.aop.project.annotation.LoginCheck * *(..))")
    public void pointCut() {
    }

    // 2、对切入点如何处理
    @Around("pointCut()")
    public Object joinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.获取切入点所在目标对象
        Object targetObj =joinPoint.getTarget();
        Log.e(TAG, "类名:"+targetObj.getClass().getName());
        // 2.获取切入点方法的名字
        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. 获取方法的参数
        Object[] args = joinPoint.getArgs();
        for(Object o :args){
            Log.e(TAG, "切入方法的参数:"+o);
        }

        Context context = (Context) joinPoint.getThis();
        if (MyApplication.isLogin()) {
            Log.e(TAG, "检测到已登录!跳转其他页面");
            return joinPoint.proceed();
        } else {
            Log.e(TAG, "检测到未登录!");
            Toast.makeText(context, "请先登录!", Toast.LENGTH_SHORT).show();
            context.startActivity(new Intent(context, LoginActivity.class));
            return null; // 不再执行方法(切入点)
        }
    }
}

上面除了登录检查的逻辑还添加了一些获取类名,方法名,方法参数值,注解参数值的方法,根据项目需要来具体选择是否获取。
(3)在Activity页面使用AspectJ

  @LoginCheck
    public void myInfomation(View view) {
        Log.e(TAG, "开始跳转到 -> 我的资料 Activity");
    }

    @LoginCheck
    public void myMoney(View view) {
        Log.e(TAG, "开始跳转到 -> 我的余额 Activity");
    }

    public void myScore(View view) {
        checkLogin("参数1", "参数2");
    }

    @LoginCheck(title = "登录检查", isSaveRequestData = false)
    public void checkLogin(String str, String str2) {
        Log.e(TAG, "开始跳转到 -> 我的积分 Activity");
    }

点击跳转之后打印信息如下:

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 >>>: 切入方法的参数:参数1
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法的参数:参数2
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 检测到未登录!
4.总结:

通过上面的例子我们可以发现,AOP解决方案可以使用很简洁的代码实现我们想要的效果,而且对原有代码毫无入侵性,理解这一原理,可以在工作中更好的以架构师的思维去编写代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容