Android AOP学习之:AspectJ实践

AOP

AOP(Aspect Oriented Programming),中文一般翻译成面向切面编程。在Java当中咱们经常说起到的是OOP(Object Oriented Programming)面向对象编程。其实这些都只是编程中从不一样的思考方向得出的一种编程思想、编程方法。javascript

在面向对象编程中,咱们经常说起到的是“everything is object”一切皆对象。咱们在编程过程当中,将一切抽象成对象模型,思考问题、搭建模型的时候,优先从对象的属性和行为职责出发,而不固执于具体实现的过程。html

但是当咱们深挖里面的细节的时候,就会发现一些很矛盾的地方。好比,咱们要完成一个事件埋点的功能,咱们但愿在原来整个系统当中,加入一些事件的埋点,监控并获取用户的操做行为和操做数据。java

按照面向对象的思想,咱们会设计一个埋点管理器,而后在每一个须要埋点的地方都加上一段埋点管理器的方法调用的逻辑。咋看起来,这样子并无什么问题。可是咱们会发现一个埋点的功能已经侵入到了咱们系统的内部,埋点的功能方法调用处处都是。若是咱们要对埋点的功能进行撤销、迁移或者重构的时候,都会存在不小的代价。android

那么AOP的思想能干什么呢?AOP提倡的是针对同一类问题的统一处理。好比咱们前面说起到的事件埋点功能,咱们的埋点功能散落在系统的每一个角落(虽然咱们的核心逻辑能够抽象在一个对象当中)。若是咱们将AOP与OOP二者相结合,将功能的逻辑抽象成对象(OOP,同一类问题,单一的原则),再在一个统一的地方,完成逻辑的调用(AOP,将问题的处理,也便是逻辑的调用统一)。这样子,咱们就能够用更加完美的结构完成系统的功能。git

上面其实已经揭示了AOP的实际使用场景:无侵入的在宿主系统中插入一些核心的代码逻辑:日志埋点、性能监控、动态权限控制、代码调试等等。github

实现AOP的的核心技术其实就是代码织入技术(code injection),对应的编程手段和工具其实有不少种,好比AspectJ、JavaAssit、ASMDex、Dynamic Proxy等等。关于这些技术的实践的对比,能够参考这篇文章。Practical Introduction into Code Injection with AspectJ, Javassist, and Java Proxysql

AspectJ

AspectJ其实是对AOP编程思想的一个实践。AspectJ提供了一套全新的语法实现,彻底兼容Java(其实跟Java之间的区别,只是多了一些关键词而已)。同时,还提供了纯Java语言的实现,经过注解的方式,完成代码编织的功能。所以咱们在使用AspectJ的时候有如下两种方式:编程

  • 使用AspectJ的语言进行开发
  • 经过AspectJ提供的注解在Java语言上开发

由于最终的目的其实都是须要在字节码文件中织入咱们本身定义的切面代码,无论使用哪一种方式接入AspectJ,都须要使用AspectJ提供的代码编译工具ajc进行编译。app

经常使用术语

在了解AspectJ的具体使用以前,先了解一下其中的一些基本的术语概念,这有利于咱们掌握AspectJ的使用以及AOP的编程思想。eclipse

在下面的关于AspectJ的使用相关介绍都是以注解的方式使用做为说明的。

JoinPoints

JoinPoints(链接点),程序中可能做为代码注入目标的特定的点。在AspectJ中能够做为JoinPoints的地方包括:

JoinPoints 说明 示例
method call 函数调用 好比调用Log.e(),这是一处Joint point
method execution 函数执行 好比Log.e()的执行内部,是一处Joint Point
constructor call 构造函数调用 与方法的调用类型
constructor executor 构造函数执行 与方法的执行执行
field get 获取某个变量
field set 设置某个变量
static initialization 类初始化
initialization object在构造函数中作的一些工做
handler 异常处理 对应try-catch()中,对应的catch块内的执行

PointCuts

PointCuts(切入点),其实就是代码注入的位置。与前面的JoinPoints不一样的地方在于,其实PointCuts是有条件限定的JoinPoints。好比说,在一个Java源文件中,会有不少的JoinPoints,可是咱们只但愿对其中带有@debug注解的地方才注入代码。因此,PointCuts是经过语法标准给JoinPoints添加了筛选条件限定。

Advice

Advice(通知),其实就是注入到class文件中的代码片。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行以前、执行后和彻底替代目标方法执行的代码。

Aspect

Aspect(切面),Pointcut 和 Advice 的组合看作切面。

Weaving

注入代码(advices)到目标位置(joint points)的过程

AspectJ使用配置

在android studio的android工程中使用AspectJ的时候,咱们须要在项目的build.gradle的文件中添加一些配置:

  • 首先在项目的根目录的build.gradle中添加以下配置:
buildscript {
    ...
    dependencies {        
        classpath 'org.aspectj:aspectjtools:1.8.6'
        ...
    }
}复制代码
  • 单独定一个module用于编写aspect的切面代码,在该module的build.gradle目录中添加以下配置(若是咱们的切面代码并非独立为一个module的能够忽略这一步):
apply plugin: 'com.android.library'

android {
    ...
}

android.libraryVariants.all { variant ->
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        //下面的1.8是指咱们兼容的jdk的版本
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", android.bootClasspath.join(File.pathSeparator)]

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler)

        def log = project.logger
        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:
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

dependencies {
    ...
    compile 'org.aspectj:aspectjrt:1.8.6'
    ...

}复制代码
  • 在咱们的app module的build.gradle文件中添加以下配置:
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

apply plugin: 'com.android.application'

android {
    ...
}

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;
            }
        }
    }
}


dependencies {
    ...
    compile 'org.aspectj:aspectjrt:1.8.6'
    //本身定义的切面代码的模块
    compile project(":aspectj")
    ...
}复制代码

其实,第二步和第三步的配置是同样的,而且在配置当中,咱们使用了gradle的log日志打印对象logger。所以咱们在编译的时候,能够得到关于代码织入的一些异常信息。咱们能够利用这些异常信息帮助检查咱们的切面代码片断是否语法正确。要注意的是:logger的日志输出是在android studio的Gradle Console控制台显示的,并非咱们常规的logcat

经过上面的方式,咱们就完成了在android studio中的android项目工程接入AspectJ的配置工做。这个配置有点繁琐,所以网上其实已经有人写了相应的gradle插件,具体能够参考:AspectJ Gradle插件

Pointcut使用语法

在前面术语当中说起到,Pointcut实际上是加了筛选条件限制的JoinPoints,而每种类型的JoinPoint都会对应有本身的筛选条件的匹配格式,Pointcut的定义就是要根据不一样的JoinPoint声明合适的筛选条件表达式。

直接对JoinPoint的选择

JoinPoint类型 Pointcut语法
Method Execution(方法执行) execution(MethodSignature)
Method Call(方法调用) call(MethodSignature)
Constructor Execution(构造器执行) execution(ConstructorSignature)
Construtor Call(构造器调用) call(ConstructorSignature)
Class Initialization(类初始化) staticinitialization(TypeSignature)
Field Read(属性读) get(FieldSignature)
Field Set(属性写) set(FieldSignature)
Exception Handler(异常处理) handler(TypeSignature)
Object Initialization(对象初始化) initialization(ConstructorSignature)
Object Pre-initialization(对象预初始化) preinitialization(ConstructorSignature)
Advice Execution(advice执行) adviceexecution()
  1. 在上面表格中所说起到的MethodSignature、ConstructorSignature、TypeSignature、FieldSignature,它们的表达式均可以使用通配符进行匹配。
  2. 表格当中的execution、call、set、get、initialization、preinitialization、adviceexecution、staticinitialization这些都是属于AspectJ当中的关键词
  3. 表格当中的handler只能与advice中的before(advice的相应关键词及使用参考后文)一块儿使用
  • 经常使用通配符
通配符 意义 示例
* 表示除”.”之外的任意字符串 java.*.Date:能够表示java.sql. Date,java.util. Date
.. 表示任意子package或者任意参数参数列表 java..*:表示java任意子包;void getName(..):表示方法参数为任意类型任意个数
+ 表示子类 java..*Model+:表示java任意包中以Model结尾的子类
  • MethodSignature

    定义MethodSignature的条件表达式与定义一个方法类型,其结构以下:

    • 表达式:

      [@注解] [访问权限] 返回值的类型 类全路径名(包名+类名).函数名(参数)

    • 说明:

      1. []当中的内容表示可选项。当没有设定的时候,表示全匹配
      2. 返回值类型、类全路径、函数名、参数均可以使用上面的通配符进行描述。
    • 例子:

      public (..) :表示任意参数任意包下的任意函数名任意返回值的public方法

      @com.example.TestAnnotation com.example..(int) :表示com.example下被TestAnnotation注解了的带一个int类型参数的任意名称任意返回值的方法

  • ConstructorSignature

    Constructorsignature和Method Signature相似,只不过构造函数没有返回值,并且函数名必须叫new.

    • 表达式:

      [@注解] [访问权限] 类全路径名(包名+类名).new(参数)

    • 例子:

      public *..People.new(..) :表示任意包名下面People这个类的public构造器,参数列表任意

  • FieldSignature

    与在类中定一个一个成员变量的格式相相似。

    • 表达式:

      [@注解] [访问权限] 类型 类全路径名.成员变量名

    • 例子:

      String com.example..People.lastName :表示com.example包下面的People这个类中名为lastName的String类型的成员变量

  • TypeSignature

    TypeSignature其实就是用来指定一个类的。所以咱们只须要给出一个类的全路径的表达式便可

间接对JoinPoint进行选择

除了上面表格当中说起到的直接对Join Point选择以外,还有一些Pointcut关键字是间接的对Join Point进行选择的。以下表所示:

Pointcut语法 说明 示例
within(TypeSignature) 表示在某个类中全部的Join Point within(com.example.Test):表示在com.example.Test类当中的所有Join Point
withincode(ConstructorSignature/MethodSignature) 表示在某个函数/构造函数当中的Join Point withincode( ..Test(..)):表示在任意包下面的Test函数的全部Join Point
args(TypeSignature) 对Join Point的参数进行条件筛选 args(int,..):表示第一个参数是int,后面参数不限的Join Point

除了上面几个以外,其实还有target、this、cflow、cflowbelow。由于对这几个掌握不是很清楚,这里不详细说明。有兴趣的能够参考这篇文章的说明:深刻理解Android之AOP

组合Pointcut进行选择

Pointcut之间可使用“&& | !”这些逻辑符号进行拼接,将两个Pointcut进行拼接,完成一个最终的对JoinPoint的选择操做。(其实就是将上面的间接选择JoinPoint表中关键字定义的Pointcut与直接选择JoinPoint表关键字定义的Pointcut进行拼接)

Advice语法使用

AspectJ提供的Advice类型以下表所示:

Advice语法 说明
before 在选择的JoinPoint的前面插入切片代码
after 在选择的JoinPoint的后面插入切片代码
around around会替代原来的JoinPoint(咱们能够彻底修改一个方法的实现),若是须要调用原来的JoinPoint的话,能够调用proceed()方法
AfterThrowing 在选择的JoinPoint异常抛出的时候插入切片的代码
AfterReturning 在选择的JoinPoint返回以前插入切片的代码

AspectJ实践

如下关于AspectJ的实践都是使用AspectJ提供的Java注解的方式来实现。

直接使用Pointcut

定义一个People类,里面包含一个静态代码块

public class People {
    ...
    static {
        int a = 10;
    }
    ...
}复制代码

接下来定义一个切片,里面包含一个Advice往静态代码块当中插入一句日志打印

// 这里使用@Aspect注解,表示这个类是一个切片代码类。
// 每个定义了切片代码的类都应该添加上这个注解

@Aspect
public class TestAspect {

    public static final String TAG = "TestAspect";

    //@After,表示使用After类型的advice,里面的value其实就是一个poincut

    @After(value = "staticinitialization(*..People)")
    public void afterStaticInitial(){
        Log.d(TAG,"the static block is initial");
    }
}复制代码

最后,在apk当中的dex文件的People的class文件中会多出下面这样的一段代码:

static {
    TestAspect.aspectOf().afterStaticInitial();
}复制代码

自定义Pointcut && 组合Pointcut

咱们可使用AspectJ提供的“@Pointcut”注解完成自定义的Pointcut。下面经过“在MainActivity这个类里面完成异常捕捉的切片代码”这个例子来演示自定义Pointcut和组合Pointcut的使用

public class MainActivity extends Activity {

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        test("this is tag for test");
    }

public void test(String test) {
    try {
        throw new IllegalArgumentException("self throw exception");
        } catch (Exception e) {

        }
    }
}复制代码
@Aspect
public class TestAspect {

    @Pointcut(value = "handler(Exception)")
    public void handleException(){

    }

    @Pointcut(value = "within(*..MainActivity)")
    public void codeInMain(){

    }

    // 这里经过&&操做符,将两个Pointcut进行了组合
    // 表达的意思其实就是:在MainActivity当中的catch代码块

    @Before(value = "codeInMain() && handleException()")
    public void catchException(JoinPoint joinPoint){
        Log.d(TAG,"this is a try catch block");
    }
}复制代码

最后编译后的MainActivity当中test方法变成了:

public void test(String test) {
        try {
            throw new IllegalArgumentException("self throw exception");
        } catch (Object e) {
            TestAspect.aspectOf().catchException(Factory.makeJP(ajc$tjp_0, (Object) this, null, e));
        }
    }复制代码

使用总结

通过上面两个简单的小例子基本上可以明白了在android studio的android项目中接入AspectJ的流程,这里简单总结一下:

  1. 环境搭建(主要是配置代码编译使用ajc编译器,而且添加gralde的logger日志输出,方便调试)
  2. 使用@Aspect注解,标示某个类做为咱们的切片类
  3. 使用@Pointcut注解定义pointcut。这一步实际上是可选的。可是为了提升代码的可读性,能够经过合理拆分粒度,定义切点,并经过逻辑操做符进行组合,达到强大的切点描述
  4. 根据实际须要,经过注解定义advice的代码,这些注解包括:@Before,@After,@AfterThrowing,@AfterReturning,@Around.

参考文献

  1. 深刻理解Android之AOP
  2. @AspectJ cheat sheet
  3. Practical Introduction into Code Injection with AspectJ, Javassist, and Java Proxy
相关文章
相关标签/搜索