AOP编程实战-AspectJ

简介:Aspect Oriented Programming,面向切面编程。 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。 AOP是OOP的延续,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

可能看简介有点抽象,接下来我们从一个开发中常见的问题入手,应该会更具体一些。我们知道在app开发中经常有判断当前是否登录的情况,比如在抖音首页中就算我们没有登录还是可以看视频,但是如果想要点赞评论或者切换下面tab时就会需要我们登录了。按照最简单的思路,我们当然可以在点击这所有的按钮的时候判断一下当前的登录状态,如果未登录的话就跳转到登录页面。但是,这样做的话是否太过复杂?该页面中有上十个按钮都需要判断登录状态,重复的代码太多,这种情况就需要优化了。那么应该如何进行优化呢?我们可以参考一下ARouter的实现,在ARouter中有一个拦截器,我们通过实现拦截器可以拦截下来跳转请求,并且在其中进行判断登录状态。重点是拦截器只需要写一次代码,节省重复劳动。


那么拦截器是什么原理呢?这个我们通过ARouter源码可以看到,ARouter中的拦截器是通过@Interceptor注解进行标识,然后在ARouter初始化时会调用init方法,其中就是执行自定义拦截器的process方法。这里的将Interceptor统一处理拦截的方式就是AOP操作,提炼出了这些操作的共性,并且进行统一处理,共性就是需要拦截和判断。



除了APT来实现AOP还有其他的方式,具体的方式以及优劣见下图。

我们知道APT是在编译器通过注解来采集信息,然后通过注解处理器来生成代码,生成的代码和普通的java代码一样会被打包成class使用。AspectJ是第三方提供的框架,是在编译以后,按照class文件的规则通过文件流去修改class文件。AspectJ底层是使用了ASM,和ASM原理类似。Qzone超级补丁、AS中的Instant run功能都是使用的ASM直接操作字节码。以上三种方式都是属于预编译方式来实现AOP,而不是运行期动态代理的方式,所以不影响效率。

接下来笔者会以工作中经常碰到的其他2种场景举例,演示一下AOP的简单实现。那么应该用以上三种方式的哪一种呢,APT的方式时通过生成一个class类文件,然后再运行的时候去调用这个新文件中的代码来实现我们想要的功能,因为生成以后还需要调用所以侵入性很强,适合那种写模板代码的情况,但现在的需求是修改已有代码,那么ASM?ASM是最轻量的、性能最好、最强大的,但是使用起来太复杂,所以优先使用AspectJ来实现。


在此之前,先简单介绍一下AspectJ的简单使用方式

步骤一 导包

前面说过AspectJ是一个第三方库,所以需要导入相关的依赖到工程中

1.在buildscript中加入

buildscript {

    repositories {

       google()

      jcenter()

   } dependencies {

      classpath 'org.aspectj:aspectjtools:1.9.2'

   }

}

2.在模块的的dependencies中加入

dependencies {

     implementation 'org.aspectj:aspectjrt:1.9.2'

}

步骤二  配置Aspect执行脚本

需要将Aspect的执行脚本复制到gradle中

//在构建工程时,执行编织
project.android.applicationVariants.all { variant ->    
JavaCompile javaCompile = variant.javaCompile    
//在编译后 增加行为    
javaCompile.doLast {        
println "执行AspectJ编译器......"        
String[] args = [                
"-1.7",               
 //aspectJ 处理的源文件                
"-inpath", javaCompile.destinationDir.toString(),               
 //输出目录,aspectJ处理完成后的输出目录                
"-d", javaCompile.destinationDir.toString(),                
//aspectJ 编译器的classpath aspectjtools                
"-aspectpath", javaCompile.classpath.asPath,                
//java的类查找路径                
"-classpath", javaCompile.classpath.asPath,               
 //覆盖引导类的位置  android中使用android.jar 而不是jdk                
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]       
 new Main().runMain(args, false)    }}复制代码


案例1.进行线程切换时,每次都要调用一大堆的Handle/RxJava代码API

步骤一  定义2个注解分别代表子线程和主线程

其中Async代表子线程标识,Main代表主线程标识,定义注解的原因是为了和Aspect配合使用,来标识需要使用切面的方法,毕竟不是所有的方法都需要使用线程切换的切面的。

步骤二  定义好子线程和主线程的切面

需要告诉系统,应该怎么处理注解标识好的方法,这里就要使用到Aspect库了。我们定义一个类,用@Aspect进行标记,然后定义一个方法,以异步线程为例,在定义好的方法上面用@Around标注,然后标注中按照execution(@注解类名 返回值 方法名和参数)的方式进行标注,其中*为通配符,如下图中我用void *(..)代表处理无返回值的任意方法、任意参数的方法。也就是说只要是用Async注解进行了标注了并且没有返回值的方法就会被拦截下来不再执行,被我们定义的doAsyncMethod方法代替。在拦截到方法后,我们使用rxjava中的线程切换将当前线程切换到子线程。需要注意的是,这个joinPoint参数就是核心了,它代表着原方法中所有的执行步骤,以参数的形式传到了切面,可供我们在任何想要的地方进行调用。本例中,在线程切换完成后,我们调用了原方法,所以达到了切换线程的目的。当然,主线程标记以一样。

步骤三  用自定义的注解去标识需要使用切面的类

必须让系统知道哪些方法需要切换线程,所以我们需要用注解进行标识,这也是我们定义注解的原因。比如我们在io流读写文件的时候用子线程,读取完成以后需要更新UI必须再主线程,所以我们将读写文件的方法用Async来标记,然后将UI操作的方法使用Main来操作。操作完成以后,AspectJ就会拦截这些方法,根据使用的哪个注解来执行相应的切面。



案例2.需要输入一些日志,每次都需要组装不同字符串

案例2和案例1非常类似,这次我们新增一个参数代表日志的类型吧,在定义注解的时候新增了一个value字段,用来搜集打日志的类型。然后需要注意的是,如果将注解中的参数传递过来,需要获取到注解类,调用注解类的方法。具体的切面代码就不贴了,和案例1类似,可以参照案例1,然后下面贴出获取到注解传的参数的方式。


Logger logger = method.getAnnotation(Logger.class);    //获取到注解

String loggerType = looger.value();    //获取到日志类型参数复制代码


总结:本文介绍了跳转登录、线程切换、打日志等几种情况下AOP的应用。但是实际上AOP能用到的场景远远不止这些,比如参数校验和判空、动态权限处理、埋点、性能统计等很多其他地方都可以使用AOP进行优化。除此之外,本文只是介绍了编译时修改的方式进行AOP编程,还有运行期动态代理的方式没有介绍,等以后有空再更新。


你可能感兴趣的:(移动开发,java,ui)