Android面向切面编程(AOP)

1、简述

一、AOP的概念

若是你用java作事后台开发,那么你必定知道AOP这个概念。若是不知道也无妨,套用百度百科的介绍,也能让你明白这玩意是干什么的:java

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,经过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP能够对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度下降,提升程序的可重用性,同时提升了开发的效率。android

二、项目场景

项目开发过程当中,可能会有这样的需求,须要咱们在方法执行完成后,记录日志(后台开发中比较常见~),或是计算这个方法的执行时间,在不使用AOP的状况下,咱们能够在方法最后调用另外一个专门记录日志的方法,或是在方法体的首尾分别获取时间,而后经过计算时间差来计算整个方法执行所消耗的时间,这样也能够完成需求。那若是不仅一个方法要这么玩怎么办?每一个方法都写上一段相同的代码吗?后期处理逻辑变了要怎么办?最后老板说这功能不要了咱们还得一个个删除?git

很明显,这是不可能的,咱们不只仅是代码的搬运工,咱们仍是有思考能力的软件开发工程师。这么low的作法绝对不干,这种问题咱们彻底能够用AOP来解决,不就是在方法前和方法后插入一段代码吗?AOP分分钟搞定。github

三、AOP的实现方式

要注意了,AOP仅仅只是个概念,实现它的方式(工具和库)有如下几种:spring

  • AspectJ: 一个 JavaTM 语言的面向切面编程的无缝扩展(适用Android)。
  • Javassist for Android: 用于字节码操做的知名 java 类库 Javassist 的 Android 平台移植版。
  • DexMaker: Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
  • ASMDEX: 一个相似 ASM 的字节码操做库,运行在Android平台,操做Dex字节码。

本篇的主角就是AspectJ,下面就来看看AspectJ方式的AOP如何在Android开发中进行使用吧。编程

2、AspectJ的引入

对于eclipse与Android Studio的引入是不同的,本篇只介绍Android Studio如何引入AspectJ,eclipse请自行百度。Android Studio须要在app模块的build.gradle文件中引入,总共分为3个步骤:bash

1)添加核心依赖

dependencies {
    ...
    compile 'org.aspectj:aspectjrt:1.8.9'
}复制代码

2)编写gradle编译脚本

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}复制代码

AspectJ须要依赖maven仓库。 app

3)添加gradle任务

dependencies {
    ...
}
// 贴上面那段没用的代码是为了说明:下面的任务代码与dependencies同级

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;
            }
        }
    }
}复制代码

直接粘贴到build.gradle文件的末尾便可,不要嵌套在别的指令中。框架

3、AOP的基本知识

在使用AspectJ以前,仍是须要先介绍下AOP的基本知识,熟悉的看官能够跳过这部分。eclipse

一、AOP术语

  1. 通知、加强处理(Advice):就是你想要的功能,也就是上面说的日志、耗时计算等。
  2. 链接点(JoinPoint):容许你通知(Advice)的地方,那可就真多了,基本每一个方法的前、后(二者都有也行),或抛出异常是时均可以是链接点(spring只支持方法链接点)。AspectJ还可让你在构造器或属性注入时都行,不过通常状况下不会这么作,只要记住,和方法有关的前先后后都是链接点。
  3. 切入点(Pointcut):上面说的链接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有十几个链接点了对吧,可是你并不想在全部方法附件都使用通知(使用叫织入,下面再说),你只是想让其中几个,在调用这几个方法以前、以后或者抛出异常时干点什么,那么就用切入点来定义这几个方法,让切点来筛选链接点,选中那几个你想要的方法。
  4. 切面(Aspect):切面是通知和切入点的结合。如今发现了吧,没链接点什么事,链接点就是为了让你好理解切点搞出来的,明白这个概念就好了。通知说明了干什么和何时干(何时经过before,after,around等AOP注解就能知道),而切入点说明了在哪干(指定究竟是哪一个方法),这就是一个完整的切面定义。
  5. 织入(weaving) 把切面应用到目标对象来建立新的代理对象的过程。

上述术语的解释引用自《AOP中的概念通知、切点、切面》这篇文章,做者的描述很是直白,很容易理解,点个赞。

二、AOP注解与使用

  • @Aspect:声明切面,标记类
  • @Pointcut(切点表达式):定义切点,标记方法
  • @Before(切点表达式):前置通知,切点以前执行
  • @Around(切点表达式):环绕通知,切点先后执行
  • @After(切点表达式):后置通知,切点以后执行
  • @AfterReturning(切点表达式):返回通知,切点方法返回结果以后执行
  • @AfterThrowing(切点表达式):异常通知,切点抛出异常时执行

@Pointcut、@Before、@Around、@After、@AfterReturning、@AfterThrowing须要在切面类中使用,即在使用@Aspect的类中。

1)切点表达式是什么?

这就是切点表达式:execution (* com.lqr..*.*(..))。切点表达式的组成以下:

execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)复制代码

除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。

修饰符模式指的是public、private、protected,异常模式指的是NullPointException等。

对于切点表达式的理解不是本篇重点,下面列出几个例子说明一下就行了:

@Before("execution(public * *(..))")
public void before(JoinPoint point) {
    System.out.println("CSDN_LQR");
}复制代码

匹配全部public方法,在方法执行以前打印"CSDN_LQR"。

@Around("execution(* *to(..))")
public void around(ProceedingJoinPoint joinPoint) {
    System.out.println("CSDN");
    joinPoint.proceed();
    System.out.println("LQR");
}复制代码

匹配全部以"to"结尾的方法,在方法执行以前打印"CSDN",在方法执行以后打印"LQR"。

@After("execution(* com.lqr..*to(..))")
public void after(JoinPoint point) {
    System.out.println("CSDN_LQR");
}复制代码

匹配com.lqr包下及其子包中以"to"结尾的方法,在方法执行以后打印"CSDN_LQR"。

@AfterReturning("execution(int com.lqr.*(..))")
public void afterReturning(JoinPoint point, Object returnValue) {
    System.out.println("CSDN_LQR");
}复制代码

匹配com.lqr包下全部返回类型是int的方法,在方法返回结果以后打印"CSDN_LQR"。

@AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
    System.out.println("ex = " + ex.getMessage());
}复制代码

匹配com.lqr包及其子包中的全部方法,当方法抛出异常时,打印"ex = 报错信息"。

2)@Pointcut的使用

@Pointcut是专门用来定义切点的,让切点表达式能够复用。

你可能须要在切点执行以前和切点报出异常时作些动做(如:出错时记录日志),能够这么作:

@Before("execution(* com.lqr..*(..))")
public void before(JoinPoint point) {
    System.out.println("CSDN_LQR");
}

@AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
    System.out.println("记录日志");
}复制代码

能够看到,表达式是同样的,那要怎么重用这个表达式呢?这就须要用到@Pointcut注解了,@Pointcut注解是注解在一个空方法上的,如:

@Pointcut("execution(* com.lqr..*(..))")
public void pointcut() {}复制代码

这时,"pointcut()"就等价于"execution(* com.lqr..*(..))",那么上面的代码就能够这么改了:

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

@AfterThrowing(value = "pointcut()", throwing = "ex")
public void afterThrowing(Throwable ex) {
    System.out.println("记录日志");
}复制代码

4、实战

通过上面的学习,下面是时候实战一下了,这里咱们来一个简单的例子。

一、切点

这是界面上一个按钮的点击事件,就是一个简单的方法而已,咱们拿它来试刀。

public void test(View view) {
    System.out.println("Hello, I am CSDN_LQR");
}复制代码

二、切面类

要织入一段代码到目标类方法的前先后后,必需要有一个切面类,下面就是切面类的代码:

@Aspect
public class TestAnnoAspect {

    @Pointcut("execution(* com.lqr.androidaopdemo.MainActivity.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");
    }

     @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());
    }
}复制代码

三、各通知的执行结果

先来试试看,这几个注解的执行结果如何。

不对啊,按钮的点击事件中有打印"Hello, I am CSDN_LQR"的,这里没有,怎么肥事?

这里由于@Around环绕通知会拦截原方法内容的执行,咱们须要手动放行才能够。代码修改以下:

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("@Around");
    joinPoint.proceed();// 目标方法执行完毕
}复制代码

也不对啊,少了一个@AfterThrowing通知。这个通知只有在切点抛出异常时才会执行,咱们可让代码出现一个简单的运行时异常:

public void test(View view) {
    System.out.println("Hello, I am CSDN_LQR");
    int a = 1 / 0;
}复制代码

这下@AfterThrowing通知确实被调用了,并且也打印出了错误信息(divide by zero)。但@AfterReturning通知反而不执行了,缘由很简单,都抛出异常了,切点确定是不能返回结果的。也就是说:@AfterThrowing通知与@AfterReturning通知是冲突的,在同个切点上不可能同时出现。

四、方法耗时计算的实现

由于@Around是环绕通知,能够在切点的先后分别执行一些操做,AspectJ为了能确定操做是在切点前仍是在切点后,因此在@Around通知中须要手动执行joinPoint.proceed()来肯定切点已经执行,故在joinPoint.proceed()以前的代码会在切点执行前执行,在joinPoint.proceed()以后的代码会切点执行后执行。因而,方法耗时计算的实现就是这么简单:

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    long beginTime = SystemClock.currentThreadTimeMillis();
    joinPoint.proceed();
    long endTime = SystemClock.currentThreadTimeMillis();
    long dx = endTime - beginTime;
    System.out.println("耗时:" + dx + "ms");
}复制代码

五、JoinPoint的做用

发现没有,上面全部的通知都会至少携带一个JointPoint参数,这个参数包含了切点的全部信息,下面就结合按钮的点击事件方法test()来解释joinPoint能获取到的方法信息有哪些:

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName(); // 方法名:test
Method method = signature.getMethod(); // 方法:public void com.lqr.androidaopdemo.MainActivity.test(android.view.View)
Class returnType = signature.getReturnType(); // 返回值类型:void
Class declaringType = signature.getDeclaringType(); // 方法所在类名:MainActivity
String[] parameterNames = signature.getParameterNames(); // 参数名:view
Class[] parameterTypes = signature.getParameterTypes(); // 参数类型:View复制代码

六、注解切点

前面的切点表达式结构是这样的:

execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)复制代码

但实际上,上面的切点表达式结构并不完整,应该是这样的:

execution(<@注解类型模式>? <修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)复制代码

这就意味着,切点能够用注解来标记了。

1)自定义注解

若是用注解来标记切点,通常会使用自定义注解,方便咱们拓展。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnoTrace {
    String value();
    int type();
}复制代码
  • @Target(ElementType.METHOD):表示该注解只能注解在方法上。若是想类和方法均可以用,那能够这么写:@Target({ElementType.METHOD,ElementType.TYPE}),依此类推。
  • @Retention(RetentionPolicy.RUNTIME):表示该注解在程序运行时是可见的(还有SOURCE、CLASS分别指定注解对于那个级别是可见的,通常都是用RUNTIME)。

其中的value和type是本身拓展的属性,方便存储一些额外的信息。

2)使用自定义注解标记切点

这个自定义注解只能注解在方法上(构造方法除外,构造方法也叫构造器,须要使用ElementType.CONSTRUCTOR),像日常使用其它注解同样使用它便可:

@TestAnnoTrace(value = "lqr_test", type = 1)
public void test(View view) {
    System.out.println("Hello, I am CSDN_LQR");
}复制代码

3)注解的切点表达式

既然用注解来标记切点,那么切点表达式确定是有所不一样的,要这么写:

@Pointcut("execution(@com.lqr.androidaopdemo.TestAnnoTrace * *(..))")
public void pointcut() {}复制代码

切点表达式使用注解,必定是@+注解全路径,如:@com.lqr.androidaopdemo.TestAnnoTrace。

亲测可用 ,不贴图了。

4)获取注解属性值

上面在编写自定义注解时就声明了两个属性,分别是value和type,并且在使用该注解时也都为之赋值了,那怎么在通知中获取这两个属性值呢?还记得JoinPoint这个参数吧,它就能够获取到注解中的属性值,以下所示:

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 经过Method对象获得切点上的注解
TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
String value = annotation.value();
int type = annotation.type();复制代码

最后贴下Demo地址

github.com/GitLqr/Andr…

相关文章
相关标签/搜索