Android AOP技术入门之AspectJ初认识到业务实践

一、概念

AOP全称呼 Aspect Oriented Programming ,国内大致译作面向切面编程,跟OOP(面向对象编程思想)一样是一种编程思想,两者间相互补充。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

说人话的讲法可以大致这样说:在一处地方编写代码,然后自动编译到你指定的方法中,而不需要自己一个方法一个方法去添加。这就是面向切面编程。

AOP既然是一种思想,那么就有多种对这种思想的实现。其实这个我并没有做调研,推荐一下https://juejin.im/post/5c01533de51d451b80257752#heading-24
这篇文章中有对AOP的实现方案有一个全面的展示。

二、有什么用?(适用场景)

日志记录,性能统计,安全控制,事务处理,异常处理,热修复,权限控制等等等
将这些行为代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

最简单日常开发需求,比如对点击事件进行埋点上传行为数据、对方法进行耗时的统计、防止点击事件重复等。
假设要埋点的方法有几百个那在每个方法都进行同样的编码不仅显得臃肿,并且当需求变更的时候,涉及更改的地方有几百个想想都觉得头疼。

这个是时候面向切面编程的作用就显得非常重要了。

image.png

三、AOP的基本术语

  • Joinpoint(连接点): 那些被拦截到的点(方法),可以是方法的前面、后面,或者异常、属性等。
  • Advice(通知\增强): 指拦截到 Joinpoint (方法)之后所要做的事情就是通知,也就是我们要写的那些防止重复点击事件什么的。
  • Pointcut(切入点): 要对哪些Joinpoint (方法) 进行拦截的定义。
  • Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类 动态地添加一些方法或 Field。
  • Target(目标对象):代理的目标对象。
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程. AspectJ 采用编译期织入和类装在期织入 。
  • Proxy(代理):一个类被 AOP 织入增强后,就产生一个结果代理类 。
  • Aspect(切面):是切入点和通知(引介)的结合 。相当于一个集合,这个集合包含所有的切点跟通知等

给一段AspectJ的代码展示一下 加深印象:

@Aspect   // 切面类    类下可以定义多个切入点和通知(引介)
public class TestAnnoAspectJava {
  //自定义切点
  @Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.threadTest())")
    public void pointcut(){
  }
  //自定义切点   
  @Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.stepOn1(..))")
    public void pointcutOn(){
   }
  //在切点pointcut()前面运行
   @Before("pointcut()")
    public void before(JoinPoint point) {
    
    }
  //在切点pointcut()中运行,围绕的意思
  //需要注意的是这个记得写  joinPoint.proceed(); 
  // 写在代码后面就是在切入原方法前面运行
    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        
    }
   //在切点pointcut()方法后面运行
    @After("pointcut()")
    public void after(JoinPoint point) {
      
    }
   //在切点pointcut()方法返回后运行
    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
      
    }
 //在切点pointcut()抛异常后运行
    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {

    }

}

  • 注解图解
    注解.png
  • 切点表达式
<切入点指示符> (<@注解符>?<修饰符>? <返回类型> <方法名>(<参数>) <异常>?)

注意:注解符、 修饰符、异常 、参数(没有参数的时候)可以省略,其它的不能省略

示例:

//正常方法等的切点
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.threadTest())")
public void pointcut(){ }
//注解的切点
@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
public void checkLogin() { }
  • 通配符

*:匹配任何字符;
:匹配多个任何字符,如在类型模式中匹配任何数量子包;在方法参数模式中匹配任何数量参数。
+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

示例:

1. 匹配返回任何类型的修饰符,跟指定java文件下的`stepOn`开头的方法名
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.stepOn*(..))")
public void pointcutOn() { }

2. 匹配com.mzs.aopstudydemo包下的所有String返回类型的方法
@Pointcut("execution(String com.mzs.aopstudydemo..*(..))")
public void afterReturning(JoinPoint point, Object returnValue) { }

3. 匹配所有public方法,在方法执行之前打印"YOYO"。
@Before("execution(public * *(..))")
public void before(JoinPoint point) {
    System.out.println("YOYO");
}
4. 匹配com.mzs包及其子包中的所有方法,当方法抛出异常时,打印"ex = 报错信息"。
@AfterThrowing(value = "execution(* com.mzs..*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
    System.out.println("ex = " + ex.getMessage());
}
  • 切入点指示符

切入点指示符有好多,这里只用到了execution 其它的大家看一下https://blog.csdn.net/zhengchao1991/article/details/53391244这里就不展示了 有兴趣的同学看一下这个文章

四、使用AspectJ(仅适用于Java,后面提供kotlin的处理方案)

  • 基本概念

AspectJ是一个实现AOP的思想的框架,完全兼容Java,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件,只需要加上AspectJ提供的注解跟一些简单的语法就可以实现绝大部分功能上的需求了。

Android Studio与eclipse的导入方式不同,这里我展示的是Android studio的。(eclipse的话,麻烦同学百度下吧~~)

  • Gradle接入
  1. 在使用的modulebuild.gradle下面添加
dependencies {
...
implementation 'org.aspectj:aspectjrt:1.8.9'
}
  1. 在使用的modulebuild.gradle下面添加(跟android {}同级)
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

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;
            }
        }
    }
}
  • 开始使用
  1. 创建TestAnnoAspectJava.java类,并创建切点
/**
 * Create by ldr
 * on 2020/1/8 9:26.
 */
@Aspect
public class TestAnnoAspectJava {

    @Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.test())")
    public void pointcut() {
    }


    @Before("pointcut()")
    public void before(JoinPoint point) {
        System.out.println("@Before");
    }

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("@Around");
        joinPoint.proceed();
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        System.out.println("@After");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
        System.out.println("@AfterReturning");
    }

    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        System.out.println("@afterThrowing");
        System.out.println("ex = " + ex.getMessage());
    }
}

  1. com.mzs.aopstudydemo.MainJavaActivity定义方法
public void test() {
  System.out.println("Hello,I am LIN");
}
-------------------打印的信息
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @Before
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @Around
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: Hello,I am LIN
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @After
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @AfterReturning

反编译看一下生成的test方法的源码:

public void test() {
  JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this, this);
  try {
    TestAnnoAspectJava.aspectOf().before(joinPoint);
    test_aroundBody1$advice(this, joinPoint, TestAnnoAspectJava.aspectOf(), (ProceedingJoinPoint)joinPoint);
    } finally {
      TestAnnoAspectJava.aspectOf().after(joinPoint);
    } 
}

在反编译的源码下可以看到,编译后的源码加上了TestAnnoAspectJava中定义的对应逻辑。
还有一个关键点所有的通知都会至少携带一个JointPoint参数

  • Joinpoint(连接点)提供给我们的一些方法
point.getKind() : method-execution //point的种类
point.getSignature() : void com.mzs.aopstudydemo.MainJavaActivity.stepOn1()  // 函数的签名信息
point.getSourceLocation() : MainJavaActivity.java:74 //源码所在的位置
point.getStaticPart() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1()) //返回一个对象,该对象封装了静态部分的连接点
point.getTarget() :  com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回目标对象
point.getThis() :com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回当前对象
point.toShortString() : execution(MainJavaActivity.stepOn1())
point.toLongString() : execution(private void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())
point.toString() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())

五 实践:判断是否登录

  • 前提:Java提供的元注解
    image.png

关于怎么自定义注解之类不是本章的重点,请大家可以看一下其它的相关类型的文章,下面切入正题~~

1. 自定义注解
创建注解类CheckLogin,定义对应的元注解信息,具体解释看上面的图。
并声明一个isSkip值。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckLogin {
    boolean isSkip() default false;//增加额外的信息,决定要不要跳过检查,默认不跳过
}

2.定义切点,定义通知
在切面类TestAnnoAspectJava

  //定义一个变量模拟登录状态
   public static  Boolean isLoagin = false;
  //定义切点
    @Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
    public void checkLogin() {
    }
  //定义切入信息通知
  @Around("checkLogin()")
    public void checkLoginPoint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //1. 获取函数的签名信息,获取方法信息
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        //2. 检查是否存在我们定义的CheckLogin注解
        CheckLogin annotation = method.getAnnotation(CheckLogin.class);
        //判断是要跳过检查
        boolean isSkip = annotation.isSkip();
        //3.根据注解情况进行处理
        if (annotation != null) {
            if (isSkip) {
                Log.i(TAG, "isSkip=true 这里不需要检查登录状态~~~~~~");
                proceedingJoinPoint.proceed();
            } else {
                if (isLoagin) {
                    Log.i(TAG, "您已经登录过了~~~~");
                    proceedingJoinPoint.proceed();
                } else {
                    Log.i(TAG, "请先登录~~~~~");
                }
            }
        }
    }

这里有@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))"):切点表达式使用注解,一定是@+注解全路径!!

3. 使用

@CheckLogin()
public void LoginAfter(){
  Log.i(TAG,"这里是登录成功后才会显示的数据——浪里个浪~~~");
}

@CheckLogin(isSkip = true)
public void unCheckLogin(){
  Log.i(TAG,"这里是不需求要登录判断的~~~~");
}

button4.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    TestAnnoAspectJava.isLoagin = !TestAnnoAspectJava.isLoagin;
   }
});
button5.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    LoginAfter();
  }
});
button6.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    unCheckLogin();
  }
});
}
----------------------------------------------------------------------------------------
---------------点击button6打印出来的Log-----------------------------------------

I/TestAnnoAspectJava: isSkip=true 这里不需要检查登录状态~~~~~~
I/MainActivity: 这里是不需求要登录判断的~~~~

---------------先点击button5,再点击button4,再点击button5---打印出来的Log------

I/TestAnnoAspectJava: 请先登录~~~~~
I/TestAnnoAspectJava: 您已经登录过了~~~~
I/MainActivity: 这里是登录成功后才会显示的数据——浪里个浪~~~

六、兼容Kotlin

上面的示例用的是Java,但是如果使用Kotlin的话就支持不了。所以需要的话可以使用沪江的gradle_plugin_android_aspectjx,简称AspectJX
这里就不做展示了。有需要的同学自己去翻看一下。

示例代码地址

https://github.com/lovebluedan/AOPStudyDemo.git

感谢

https://github.com/feelschaotic/AndroidKnowledgeSystem/blob/master/7.%20%E8%BF%9B%E9%98%B6/AOP/AOP.md
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
https://juejin.im/post/5c01533de51d451b80257752#heading-24
https://www.jianshu.com/p/aa1112dbebc7
https://blog.csdn.net/zhengchao1991/article/details/53391244

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