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

1、概念

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

说人话的讲法能够大体这样说:在一处地方编写代码,而后自动编译到你指定的方法中,而不须要本身一个方法一个方法去添加。这就是面向切面编程。android

AOP既然是一种思想,那么就有多种对这种思想的实现。其实这个我并无作调研,推荐一下juejin.im/post/5c0153… 这篇文章中有对AOP的实现方案有一个全面的展现。git

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

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

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

这个是时候面向切面编程的做用就显得很是重要了。安全

image.png

3、AOP的基本术语

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

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

@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
  • 切点表达式
<切入点指示符> (<@注解符>?<修饰符>? <返回类型> <方法名>(<参数>) <异常>?)
复制代码

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

示例:框架

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

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

示例:

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…这里就不展现了 有兴趣的同窗看一下这个文章

4、使用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: 这里是登陆成功后才会显示的数据——浪里个浪~~~
复制代码

6、兼容Kotlin

上面的示例用的是Java,可是若是使用Kotlin的话就支持不了。因此须要的话可使用沪江的gradle_plugin_android_aspectjx,简称AspectJX 这里就不作展现了。有须要的同窗本身去翻看一下。

示例代码地址

github.com/lovebluedan…

感谢

github.com/feelschaoti… github.com/HujiangTech… juejin.im/post/5c0153… www.jianshu.com/p/aa1112dbe… blog.csdn.net/zhengchao19…

相关文章
相关标签/搜索