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

2,226 阅读7分钟

一、概念

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

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

AOP既然是一种思想,那么就有多种对这种思想的实现。其实这个我并没有做调研,推荐一下juejin.cn/post/684490… 这篇文章中有对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 其它的大家看一下blog.csdn.net/zhengchao19…这里就不展示了 有兴趣的同学看一下这个文章

四、使用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 这里就不做展示了。有需要的同学自己去翻看一下。

示例代码地址

github.com/lovebluedan…

感谢

github.com/feelschaoti… github.com/HujiangTech… juejin.cn/post/684490… www.jianshu.com/p/aa1112dbe… blog.csdn.net/zhengchao19…