浅谈Android面向切面编程(AOP)

一、简述

1、AOP的概念

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

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

2、项目场景

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

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

3、AOP的实现方式

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

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

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

二、AspectJ的引入

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

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仓库。

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文件的末尾即可,不要嵌套在别的指令中。

三、AOP的基本知识

在使用AspectJ之前,还是需要先介绍下AOP的基本知识,熟悉的看官可以跳过这部分。

1、AOP术语

  1. 通知、增强处理(Advice):就是你想要的功能,也就是上面说的日志、耗时计算等。
  2. 连接点(JoinPoint):允许你通知(Advice)的地方,那可就真多了,基本每个方法的前、后(两者都有也行),或抛出异常是时都可以是连接点(spring只支持方法连接点)。AspectJ还可以让你在构造器或属性注入时都行,不过一般情况下不会这么做,只要记住,和方法有关的前前后后都是连接点。
  3. 切入点(Pointcut):上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有十几个连接点了对吧,但是你并不想在所有方法附件都使用通知(使用叫织入,下面再说),你只是想让其中几个,在调用这几个方法之前、之后或者抛出异常时干点什么,那么就用切入点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。
  4. 切面(Aspect):切面是通知和切入点的结合。现在发现了吧,没连接点什么事,连接点就是为了让你好理解切点搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过before,after,around等AOP注解就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。
  5. 织入(weaving) 把切面应用到目标对象来创建新的代理对象的过程。
  6. 引入(introduction) 允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗
  7. 目标(target) 引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咋们织入切面。二自己专注于业务本身的逻辑。
  8. 代理(proxy) 怎么实现整套AOP机制的,都是通过代理,这个一会儿给细说。
  9. 目标对象 – 项目原始的Java组件。
  10. AOP代理  – 由AOP框架生成java对象。
  11. AOP代理方法 = advice + 目标对象的方法。

2、AOP注解与使用

  1. @Aspect:声明切面,标记类
  2. @Pointcut(切点表达式):定义切点,标记方法
  3. @Before(切点表达式):前置通知,切点之前执行
  4. @Around(切点表达式):环绕通知,切点前后执行
  5. @After(切点表达式):后置通知,切点之后执行
  6. @AfterReturning(切点表达式):返回通知,切点方法返回结果之后执行
  7. @AfterThrowing(切点表达式):异常通知,切点抛出异常时执行
  8. @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("记录日志");
}

四、实战

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

1、切点

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

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

2、切面类

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

@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());
  }
}

3、各通知的执行结果

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

不对啊,按钮的点击事件中有打印"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通知是冲突的,在同个切点上不可能同时出现。

4、方法耗时计算的实现

因为@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");
}

5、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

6、注解切点

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

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地址:https://github.com/GitLqr/AndroidAopDemo

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android AOP注解Annotation详解(一)

    Android 注解Annotation 相关文章: Android AOP注解Annotation详解(一) Android AOP之注解处理解释器详解(二) Android AOP 注解详解及简单使用实例(三) Android AOP 等在Android上应用越来越广泛,例如框架ButterKnife,Dagger2,EventBus3等等,这里我自己总结了一个学习路程. - Java的注解Annotation - 注解处理解析器APT(Annotation Processing Tool)

  • Android AOP 注解详解及简单使用实例(三)

    Android  注解 相关文章: Android AOP注解Annotation详解(一) Android AOP之注解处理解释器详解(二) Android AOP 注解详解及简单使用实例(三) 一.简介 在Android 里面 注解主要用来干这么几件事: 和编译器一起给你一些提示警告信息. 配合一些ide 可以更加方便快捷 安全有效的编写Java代码.谷歌出的support-annotations这个库 就是主要干这个的. 和反射一起 提供一些类似于spring 可配置的功能,方便简洁. 二

  • Android AOP之注解处理解释器详解(二)

    Android APO 注解处理解释器 相关文章: Android AOP注解Annotation详解(一) Android AOP之注解处理解释器详解(二) Android AOP 注解详解及简单使用实例(三) 一.提取Annotation信息 当开发者使用了Annotation修饰了类.方法.Field等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的代码来提取并处理Annotation信息.这些处理提取和处理Annotation的代码统称为APT(Annotation

  • 浅谈Android面向切面编程(AOP)

    一.简述 1.AOP的概念 如果你用java做过后台开发,那么你一定知道AOP这个概念.如果不知道也无妨,套用百度百科的介绍,也能让你明白这玩意是干什么的: AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型.利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合

  • Android面向切面基于AOP实现登录拦截的场景示例

    目录 前言 一.了解面向切面AOP 二.集成AOP框架 三.定义注解实现功能 总结 前言 场景如下:用户第一次下载App,点击进入首页列表,点击个人页面,需要校验登录,然后跳转到登录页面,注册/登录完成跳转到个人页面. 非常常见的场景,正常我们开发就只能判断是否已经登录,如果未登录就跳转到登录,然后登录完成之后怎么继续执行?如何封装?有哪些方式?其实很多人并不清楚. 这里做一个系列总结一下,看看公共有多少种方式实现,你们使用的是哪一种方案,或者说你们觉得哪一种方案最好用. 这一次分享的是全网最多

  • Python 面向切面编程 AOP 及装饰器

    目录 什么是 AOP 装饰器 函数装饰器 类装饰器 1.函数装饰函数 2.类装饰函数 3.函数装饰类 4.类装饰类 什么是 AOP AOP,就是面向切面编程,简单的说,就是动态地将代码切入到类的指定方法.指定位置上的编程思想就是面向切面的编程. 我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类.哪些方法则叫切入点.这样我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为. 这种思想,可以使原有代码逻辑更清晰,对原有代码毫无入侵性,常用于像权限

  • Spring面向切面编程AOP详情

    目录 1. 面向切面编程 2. AOP核心概念 3. AOP的实现 4. Spring 对AOP支持 4.1 支持@Aspect 4.2 声明一个切面 4.3 声明一个切入点 4.4 声明增强 5. 用AOP实现日志拦截 5.1 一般的实现 5.2 仅拦截需要的方法 5.3 requestId传递 5.4 关于增强执行的顺序 6. 思考 1. 面向切面编程 定义:面向切面编程(AOP,Aspect Oriented Programming)是通过预编译方式和运行期间动态代理实现程序功能的统一维护

  • Spring 面向切面编程AOP实现详解

    简介 1.什么叫做面向切面编程? 概念:把一个个的横切关注点(某种业务的实现代码)放到某个模块中去,称之为切面.每个切面影响业务的一种功能,切面的目的就是为了功能增强,将需要增强的方法做成切面,实现对业务的增强,就是面向切面编程. 目的:将与业务本身无关,却被业务模块所共同调用的功能代码封装成切面,以减少系统的重复代码,降低耦合,提高可扩展性. 优势:把多个方法前/后的共同代码抽离出来,使用动态代理机制来控制,先执行抽离出来的代码,再执行每一个真实方法. 2.Spring中的AOP使用动态代理来

  • 浅谈Java面向接口编程

    我想,对于各位使用面向对象编程语言的程序员来说,"接口"这个名词一定不陌生,但是不知各位有没有这样的疑惑:接口有什么用途?它和抽象类有什么区别?能不能用抽象类代替接口呢?而且,作为程序员,一定经常听到"面向接口编程"这个短语,那么它是什么意思?有什么思想内涵?和面向对象编程是什么关系?本文将一一解答这些疑问. 1.面向接口编程和面向对象编程是什么关系 首先,面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思

  • MVC AOP面向切面编程简单介绍及实例

    MVC AOP面向切面编程 AOP这个词相信大家都没有接触太多过,但是实际上你们已经有所接触了,就在设计模式中.AOP所用的思想其实和设计模式是一样的,即在不修改原代码的情况下统一增加或者修改功能.还有,AOP大多用在spring里面,但是本文所写的只是在MVC中的应用,要注意. 一.简介 所谓AOP(Aspect Oriented Programming的缩写)意为面向切面的编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是OOP的延续,是软件开发中的一个热点,也是

  • 面向切面编程(AOP)的理解

    在传统的编写业务逻辑处理代码时,我们通常会习惯性地做几件事情:日志记录.事务控制及权限控制等,然后才是编写核心的业务逻辑处理代码.当代码编写完成回头再看时,不禁发现,扬扬洒洒上百行代码中,真正用于核心业务逻辑处理才那么几行,如图6-4所示.方法复方法,类复类,就这样子带着无可奈何遗憾地度过了多少个春秋.这倒也罢,倘若到了项目的尾声,突然决定在权限控制上需要进行大的变动时,成千上万个方法又得一一"登门拜访",痛苦"雪上加霜". 如果能把图6-4中众多方法中的所有共有代

  • Javascript aop(面向切面编程)之around(环绕)分析

    Aop又叫面向切面编程,其中"通知"是切面的具体实现,分为before(前置通知).after(后置通知).around(环绕通知),用过spring的同学肯定对它非常熟悉,而在js中,AOP是一个被严重忽视的技术点.但是利用aop可以有效的改善js代码逻辑,比如前端框架dojo和yui3中AOP则被提升至自定义事件的一种内在机制,在源码中随处可见.得益于这种抽象使得dojo的自定义事件异常强大和灵活.dojo中aop的实现在dojo/aspect模块中,主要有三个方法:before.

  • Java实现AOP面向切面编程的实例教程

    介绍 众所周知,AOP(面向切面编程)是Spring框架的特色功能之一.通过设置横切关注点(cross cutting concerns),AOP提供了极高的扩展性.那AOP在Spring中是怎样运作的呢?当你只能使用core java,却需要AOP技术时,这个问题的解答变得极为关键.不仅如此,在高级技术岗位的面试中,此类问题也常作为考题出现.这不,我的朋友最近参加了一个面试,就被问到了这样一个棘手的问题--如何在不使用Spring及相关库,只用core Java的条件下实现AOP.因此,我将在

随机推荐