AOP入门介绍

概念

AOP(Aspect Oriented Programming)简而言之就是面向切面编程。它所要实现的目标就是解耦,提供代码的灵活性和可扩展性。

与OOP的区别

OOP(Object Oriented Programming)面对对象编程。
这其实是两种不同的设计思想:

  • OOP是把同一对象的属性放到一个对象里,把同一类别的类放到一个模块里;这里同一的评判标准可以按照业务来划分,也可以通过行为来划分
  • AOP则是提取相同的功能和方法,针对这样的横切面来归类。

用图来弥补下言语的匮乏:

OOP

AOP

AOP的应用领域

在Android开发过程中,我们时长遇到需要统计事件,性能监测,权限检查等需求。而这些需求是独立业务开发之外的。在业务开发过程中,开发人员不想被这些需求打扰而中断了业务逻辑的梳理。这个时候,就可以用到AOP的思想来解决问题。

  • 打印日志:独立的日志模块,在业务开发后期嵌入到各个业务模块中,在代码里不存在日志相关代码。
  • 性能监测:时长需要对生命周期函数,view绘制函数监测其运行时长来监测性能。但如果在代码开发阶段考虑就要,实现每个生命周期函数,在绘制view的函数中加入时间统计代码,这样不仅会导致冗余代码还会影响方法本身的性能。
  • 权限检查:使方法功能单一,剥离权限检查部分。

其实,总结起来,最终的目的就是为了解耦,尽量将业务无关的,且同一类方法,功能中需要做的重复动作,提取出来。

AspectJ

AOP的实现有很多中,AspectJ只是其中一种,在Java中用得比较多。AspectJ可以说是一种语言,它完全兼容Java,使用原生的Java来开发的话,只需要加上AspectJ的注解就可以。因此两种方式:

  • 通过AspectJ的关键字来实现
  • 原生java+AspectJ的相关注解来开发

但是无论是通过何种方式实现,其编译都必须要通过AspectJ的编译工具ajc来编译。

AspectJ的语法

在介绍AspectJ的语法之前,先介绍几个概念,也是AspectJ中的关键字,了解他们的含义,对于开发至关重要。

  • aspect(切面) 针对切面的模块。也就说独立于业务,需要被插入的部分。
  • joinpoint(连接点) 顾名思义就是连接切面模块和业务模块的地方;也可以理解为就是业务模块中需要被嵌入代码的地方。
  • pointcut 这个理解起来跟joinpoint应该是一个意思,只不过它可以添加一些附加条件
  • advice(处理逻辑) 说逻辑处理有点牵强,它表示的意思应该是被插入的代码,以及插入的时机,如:Before,After,Around等。

常用的切入点

切入点一般是通过joinpoint和advice的组合来实现的,常用的可以看下表:

joinpoint advice 切入点
execution before 方法执行之前,切入点在方法内
execution after 方法执行之后,切入点在方法内
execution around 方法执行前后,可以替换原方法,切入点在方法内
call before 方法调用之前,切入点在方法外
call after 方法调用之后,切入点在方法外
call around 方法调用前后,可以替换原方法,切入点在方法外

PS:以上是常用的一些切入点,还有通过cflow来切入每一行字节码。这个控制较难,控制不好会产生StackOverFlow,这个以后再说。
PSS: Advice的各个类型是可以组合使用的,但是切记Around与After是不可以同时使用的,会发生重复调用的问题。

JoinPoint的匹配规则

通过call 和 execution 我们可以知道切入点的时机是在方法调用还是在方法执行。但是如何才能找到方法呢,这就需要一定的匹配规则去找到需要切入的方法。

举个例子:
cn.test.fwl.Test.main() 这样的表达式,可以指定到包名为<cn.test.fwl>,类名为<Test>中无参数的main方法;那么如果我们需要匹配到这个类里所有的main方法,又或者我们需要匹配到这个包里所有类的main方法,再或者我们需要匹配到包含main字符的方法该如何来写表达式呢?

通配符
AspectJ中提供了一些通配符来方便我们找到满足规则的方法。

通配符 含义
* 匹配除了[.]之外的所有字符,用在路径中表示任意包名字符串,用在类名中标识任意类名字符串,方法中表示任意方法名字符串
.. 表示任意的子package,或者任意的参数
+ 表示子类

举例:

  • java.*.Date : 可以表示java.sql.Date,也可以表示java.util.Date
  • Test* : 表示以Test开头的任意字符串
  • java..* : 表示java包中的任意类
  • java..*Model+ : 表示java包中以Model结尾的类的子类
  • test(..) : 表示方法名为test,任意的参数,没有参数,有一个,两个都可以匹配
  • test(int,char) : 表示方法名为test,有且仅有两个参数,类型为int,char
  • test(String,..) : 表示方法名为test,至少有一个参数,第一个类型为String,其他任意
  • test(String ...) : 表示方法名为test,参数个数不定,但必须都是String类型,这里的[...] 不是通配符,而是java中不定参数的意思。

JoinPoint的约束

除了上面的匹配规则外,AspectJ还提供了一些其他方法来更加精确的选择JoinPoint,比如某个类中的JoinPoint或者某个函数执行流程中的JoinPoint。

关键词 说明 举个栗子
within(pattern) pattern可以通过通配符表示,代表某个包或者类 满足pattern适配的JoinpPoint。比如说within(Test)就标识在Test类中(包括内部类)所有的JoinPoint。
withinCode(Constructor Signature/Method Signature) 表示某个构造函数或其他函数执行过程中涉及到的 JoinPoint 比如:withinCode(* Test.testMethod(..))表示testMethod涉及的JoinPoint; withinCode(*.Test.new(..))表示Test构造函数涉及的JoinPoint
cflow(pointcuts) cflow的条件是一个pointcut,表示某个流程中涉及的JoinPoint 比如cflow(call Test.testMethod):表示调用Test.testMethod函数时所包含的JoinPoint,包括testMethod的call这个JoinPoint本身
cflowbelow(pointcuts) 比如:cflowbelow(call Test.testMethod):表示调用Test.testMethod函数时所包含的JoinPont,不包含testMethod的call这个JoinPont本身
this(Type) JoinPoint的this对象是Type类型。包括其子类 JPoint所在的这个类的类型是Type标示的类型或是其子类,则和它相关的JPoint将全部被选中。比如:Animal中的Move方法,则Bird,cat中的Move方法都会被选中
target(Type) JoinPoint的target对象是Type类型 target一般用在call的情况。call一个函数,这个函数可能定义在其他类。比如Bird的move方法在调用时被选中,那么其他的Move的方法则不会。
args(Type) 用来对JoinPoint的参数进行条件约束 比如args(int,..),表示第一个参数是int,后面参数个数和类型不限

Advice的注意点

关于Advice前面已经说过了,他其实就是被嵌入的部分,而嵌入的时机,也在切入点的表格里提到过。这里主要讲下注意点:

  • After:表示函数执行或者调用完成后运行被嵌入的代码部分。但是函数可能执行结束可能有两种退出方式:一个正常的Return,或者抛出异常,因此After也做了区分: after():return(type)
    after():throwing(Throwable)
  • Around: 除了之前说Around和After不能同时使用之外,Around因为是可以替代原函数执行的,因此,要特别注意被嵌入的代码的返回值一定要和原来的方法一致。

环境配置

Eclipse
Android现在很少有用Eclipse开发的了,但是Eclipse的插件却是对AspectJ开发支持最友好的。基础的AOP实例,打算用Eclipse来开发AspectJ对Java的横切,因此,这里也介绍下,Eclipse的搭建。
Help -> Install New Software

Eclipse插件安装

然后一直下一步就好

Android Studio
首先在工程目录中导入相关的编译工具:

buuildscript{
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

然后在Aspect的module的build.gradle中添加依赖库:

compile 'org.aspectj:aspectjrt:1.8.9'

添加aspect编译的脚本:

def variants = android.libraryVariants
variants.all{ variant ->
    LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast{
        String[] args = [
            "-showWeaveInfo",
            "-1.5",
            "-inpath",
            javaCompile.destinationDir.toString(),
            "-aspectpath",
            javaCompile.classpath.asPath,
            "-d",
            javaCompile.destinationDir.toString(),
            "-classpath",
            javaCompile.classpath.asPath,
            "-bootclasspath",
            project.android.bootClasspath.join(
                    File.pathSeparator
            )
        ]

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

        def log = project.logger
        for(IMessage msg: handler.getMessages(null,true)){
            switch(msg.getKind()){
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error msg.message, msg.thrown
                    break;
                case IMessage.WARNING:
                    log.warn msg.message, msg.thrown
                    break;
                case IMessage.INFO:
                    log.info msg.message,msg.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug msg.message,msg.thrown
                    break;
            }
        }
    }
}

最后在app的module里添加对aspect module的依赖,同时添加上对aspectJ编译的脚本:

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.5",
            "-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 msg: handler.getMessages(null,true)){
            switch (msg.getKind()){
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error msg.message,msg.thrown
                    break;
                case IMessage.WARNING:
                    log.warn msg.message, msg.thrown
                    break;
                case IMessage.INFO:
                    log.info msg.message,msg.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug msg.message,msg.thrown
                    break;
            }
        }
    }
}

举个栗子

先通过Eclipse上创建AJ的工程来熟悉下AspectJ的相关语法。
首先,创建一个AspectJ的工程,在已经完成AJDT的插件的前提下,在新建工程的时候,就可以看到可以创建AspectJ Project这样的工程,如图:

创建工程

之后创建两个不同的包,来区分java文件和aj文件
创建Test1.java文件

public class Test1 {
    public static void main(String[] args) {
        test();
    }
    public static void test(){
        System.out.println("this is test method!");
    }
}

现在我们要在test()方法执行打印之前,插入我们的操作(这里也插入一句打印)
注意我们这里创建文件的时候,不再是java文件,而是.aj的文件
创建AspectJ.aj文件

public aspect AspectJ{
    public pointcut aspect1(): execution(* test(..));

    before():aspect1(){
        System.out.println("this is before test method: execution");
    }
}

完成这个文件之后,就会发现之前Test1.javatest()这个方法里上多了箭头的标志。这就表明插入成功了。
可以运行看下结果:

运行结果

AspectJ.aj中的注入的打印已经被打印出来了。那么被注入之后的Test1.class是样的:

class file

可以看到在打印System.out.println("this is test method!");之前被插入了一段代码,而这段正是before():aspect1()方法中所执行的内容。


上面以executionbefore的组合举了一个简单的例子,主要是阐述了下如何创建Aspecj的工程,以及相应的文件。下面的例子会包含call,executionbefore,after的两两组合。
Test.java

public class Test1 {


    public static void main(String[] args) {
        testBeforeExecution();
        testBeforeCall();
        testAfterExecution();
        testAfterCall();
        testAfterReturn();
        testAfterThrowable();

    }


    public static void testBeforeExecution(){
        System.out.println("this is test before-execution!");
    }

    public static void testBeforeCall(){
        System.out.println("this is test before-call");
    }

    public static void testAfterExecution(){
        System.out.println("this is test after-execution");
    }

    public static void testAfterCall(){
        System.out.println("this is test after-call");
    }

    public static String testAfterReturn(){
        String a = "test parameter";
        System.out.println("this is test after-return");
        return a;
    }

    public static String testAfterThrowable(){
        String a = null;
        System.out.println("this is test after-throwable");
        a.equals("test");
        return a;
    }

}

AspectJ.aj

public aspect AspectJ{

    public pointcut aspect1(): execution(* testBeforeExecution(..));
    public pointcut aspect2(): call(* testBeforeCall(..));
    public pointcut aspect3(): execution(* testAfterExecution(..));
    public pointcut aspect4(): call(* testAfterCall(..));
    public pointcut aspect5(): execution(* testAfterReturn(..));
    public pointcut aspect6(): execution(* testAfterThrowable(..));



    before():aspect1(){
        System.out.println("this is before test : execution");
    }

    before():aspect2(){
        System.out.println("this is before test: call");
    }

    after():aspect3(){
        System.out.println("this is after test:execution");
    }

    after():aspect4(){
        System.out.println("this is after test: call");
    }

    after() returning(String s):aspect5(){
        System.out.println("this is after test : return->"+s);
    }

    after() throwing(Exception e):aspect6(){
        System.out.println("this is after test: throwable->"+e.getMessage());
    }
}

可以看到运行结果:

运行结果

同时也可以看到编译后的class文件:

class file


接下来再举个关于Around的用法的例子:
Test2.java

public class Test2 {

    public static void main(String[] args) {
        testAroundCall();
        testAroundExecution();
        testAroundReplace();
    
        System.out.println(testAroundRetrun());

    }


    public static void testAroundCall(){
        System.out.println("this is testAroundCall method");
    }

    public static void testAroundExecution(){
        System.out.println("this is testAroundExecution method");
    }

    public static void testAroundReplace(){
        System.out.println("this is testAroundReplace");
    }


    public static String testAroundRetrun(){
        String a = "the return value";
        System.out.println("this is  test around return");
    
        return a;
    
    }

}

AspectJ1.aj

public aspect AspectJ1{

    public pointcut test1():execution(* testAroundCall(..));
    public pointcut test2():call(* testAroundExecution(..));
    public pointcut test3():call(* testAroundReplace(..));
    public pointcut test4():call(* testAroundRetrun(..));


    void around():test1(){
        System.out.println("around-execution test before");
        proceed();
        System.out.println("around-execution test after");
    
    }

    void around():test2(){
        System.out.println("around-call test before");
        proceed();
        System.out.println("around-call test after");
    }

    void around():test3(){
        System.out.println("do replace ... ");
    
    }


    String around():test4(){
        String a = "string in aspect";
        System.out.println("replace return value");
        proceed();
        return a;
    }
}

运行结果如下:

运行结果

class 文件

class file


以上都是AspectJ语言写的,那么如果使用纯Java的方式该如何来实现呢,看下面的例子:
Aspectj2.java

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspectj2 {



    @Pointcut("execution(* testBeforeExecution(..))")
    public void test1(){
    
    }

    @Pointcut("call(* testBeforeCall(..))")
    public void test2(){
    
    }

    @Pointcut("execution(* testAfterExecution(..))")
    public void test3(){
    
    }

    @Pointcut("call(* testAfterCall(..))")
    public void test4(){
        
    }

    @Pointcut("execution(* testAfterReturn(..))")
    public void test5(){
    
    }

    @Pointcut("execution(* testAfterThrowable(..))")
    public void test6(){
    
    }



    @Before("test1()")
    public void execute1(){
        System.out.println("before-execution aspectj");
    }

    @Before("test2()")
    public void execute2(){
        System.out.println("before-call aspectj");
    }

    @After("test3()")
    public void execute3(){
        System.out.println("after-execution aspectj");
    }

    @After("test4()")
    public void execute4(){
        System.out.println("after-call aspectj");
    }

    @AfterReturning("test5()")
    public void execute5(){
        System.out.println("after-return aspectj");
    }

    @AfterThrowing("test6()")
    public void execute6(){
        System.out.println("after-throw aspectj");
    }

}

特别提醒下:类的注释@Aspect千万不能少,在这入坑了好几次

运行结果如下:

运行结果

再看下编译后的文件:

class file

栗子就先吃这么多~~~后面会再补一篇关于带参数,返回值处理的栗子。

AspectJ在Android中的应用

后续会在github上传一个关于权限检查的库,有时间也会写个文档介绍下这个库。

总结

AOP的知识接触得还不多,写了些demo和Android的库,总结下来,重点还是在JoinPoint的适配,如何才能精确得适配到自己想要的切入点,还需要将JoinPoint和Advice结合多加练习。
Eclipse上对的AJDT的插件对Aspect的语法还有错误检查,但是Android Studio上还没有,所以写的时候,要特别仔细。

TODO

  • 带参数,返回值的栗子
  • 权限检查的工程和分析文档
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容