Android中AOP实践之三AspectJ解析篇

介绍

AspectJ是Java的一个简单实用的面向方面的扩展。通过几个新的构造,AspectJ提供了对一系列横切关注的模块化实现的支持。

在现有的Java开发项目中采用AspectJ可能是一个简单而且增量的任务。一条路径是从开发方面开始,继续使用生产方面,然后在使用AspectJ建立经验之后再使用方面。采用也可以遵循其他途径。例如,一些开发人员将从马上使用生产方面受益。其他人可能几乎可以立即编写干净的可重用方面。

AspectJ支持基于名称和基于属性的横切。使用基于名称的横切的方面倾向于影响少数其他类。但是,尽管规模较小,但与普通的Java实现相比,它们通常可以消除显着的复杂性。使用基于属性的横切的方面可以具有小规模或大规模。

使用AspectJ会导致横切关注的干净模块化的实现。当作为AspectJ方面编写时,横切关注的结构是明确的且易于理解的。方面也是高度模块化的,使得开发横切功能的即插即用实现成为可能。

基础知识

Join point 连接点

Join Points 定义 解释
method call 调用方法 一般在执行某个方法前会先调用该方法
method execution 执行方法 这个点是已经执行到了方法的内部
constructor call 调用构造方法 同调用方法
constructor execution 执行构造方法 同执行方法
field get 获取参数 比如获取某个变量的值,get()
field set 设置参数 比如设置某个变量的值,int num = 3
pre-initialization 预初始化 在第一次初始化前会预初始化
initialization 初始化 初始化类的时候会执行
static initialization 静态初始化 静态块或静态类初始化的时候会执行
handler 异常处理
advice execution 通知执行

是指程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
程序中连接点有很多,下面做一个表格一一指出:

Join Points 定义 解释
method call 调用方法 一般在执行某个方法前会先调用该方法
method execution 执行方法 这个点是已经执行到了方法的内部
constructor call 调用构造方法 同调用方法
constructor execution 执行构造方法 同执行方法
field get 获取参数 比如获取某个变量的值,get()
field set 设置参数 比如设置某个变量的值,int num = 3
pre-initialization 预初始化 在第一次初始化前会预初始化
initialization 初始化 初始化类的时候会执行
static initialization 静态初始化 静态块或静态类初始化的时候会执行
handler 异常处理
advice execution 通知执行

//这里可以用一个例子来演示一下所有的连接点

Pointcut 切入点

一个程序中会有很多JPoint连接点,但不一定我们都要去关注。那么我们可以选择我们需要的点来作为切入点。

我们利用Pointcut的功能来筛选出对我们有用的点作为切入,pointcut有一套专门的语法,只要搞懂他后面就不愁了。

一个例子

    @Pointcut("within(@com.jie.aoptest.aop.DebugLog *)")
    public void withinAnnotatedClass() {}

    @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
    public void methodInsideAnnotatedType() {}

    @Pointcut("execution(@com.jie.aoptest.aop.DebugLog * *(..)) || methodInsideAnnotatedType()")
    public void method() {}

这个例子表明切入点在DebugLog类中所有执行方法的点,这里用到了within和execution两个指示符,within用于匹配指定类型内的方法执行,而exection则用于匹配方法执行的连接点。有synthetic标记的field和method是class内部使用的,正常的源代码里不会出现synthetic field。

切入点指示符

指示符 说明
execution 用于匹配方法执行的连接点
within 用于匹配指定类型内的方法执行
this: 用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
target 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
args 用于匹配当前执行的方法传入的参数为指定类型的执行方法
@within 用于匹配所以持有指定注解类型内的方法
@target 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
@args 用于匹配当前执行的方法传入的参数持有指定注解的执行
@annotation 用于匹配当前执行方法持有指定注解的方法

常用指示符

指示符 说明
execution 用于匹配方法执行的连接点
within 用于匹配指定类型内的方法执行
this: 用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
target 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
args 用于匹配当前执行的方法传入的参数为指定类型的执行方法
@within 用于匹配所以持有指定注解类型内的方法
@target 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
@args 用于匹配当前执行的方法传入的参数持有指定注解的执行
@annotation 用于匹配当前执行方法持有指定注解的方法

AspectJ切入点支持的切入点指示符还有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode,感兴趣的可以了解,就不一一说明了。

类型匹配语法

先来看一下AspectJ类型匹配的通配符

  1. *:匹配任何数量字符;
  2. ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
  3. +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

接下来看一下具体匹配表达式类型

  1. 注解:可选,方法上持有的注解,如@Deprecated;
  2. 修饰符:可选,如public、protected;
  3. 返回值类型:必填,可以是任何类型模式;“*”表示所有类型;
  4. 类型声明:可选,可以是任何类型模式;
  5. 方法名:必填,可以使用“*”进行模式匹配;
  6. 参数列表:“()”表示方法没有任何参数;“(..)”表示匹配接受任意个参数的方法,“(..,java.lang.String)”表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法;“(java.lang.String,..)” 表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法;“(*,java.lang.String)” 表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法;
  7. 异常列表:可选,以“throws 异常全限定名列表”声明,异常全限定名列表如有多个以“,”分割,如throws java.lang.IllegalArgumentException, java.lang.ArrayIndexOutOfBoundsException。

组合切入点表达式

AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式。

常用场景举例

Advice通知参数

前面已经介绍了Join point 连接点和 Pointcut 切入点,如果基本掌握了的话那么恭喜你内功已经修炼7成了。

我们成功设置好切入点后需要获取通知来执行要切入的代码片段,这里的通知相当于钩子/回调方法,在程序执行到JPoint时候会调起通知,接下来就介绍一下获取通知的方式。

Advice通知有三种类型

类型 说明
before() 是指在JPoint之前可以执行一些操作
after() 是指在JPoint之后可以执行一些操作
around() 环绕JPoint执行操作,它包含了前后两个过程,使用这种类型需要手动调用procees方法来执行原操作

来看一个例子

我们来用检测是否登录做一个例子
这个是检查登录的注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface CheckLogin {
}

先用before和after两个类型来做一个测试

    @Pointcut("execution(@com.jie.aoptest.aop.CheckLogin * *(..))")
    public void methodAnnotated() {
    }

    @Before("methodAnnotated()")
    public void beforeMethod(ProceedingJoinPoint joinPoint) {
        Log.d("aspect", "beforeMethod");
        Log.d("login", "请您登录");
        Toast.makeText(App.getAppContext().getCurActivity(), "请您登录", Toast.LENGTH_SHORT).show();
    }

    @After("methodAnnotated()")
    public void afterMethod(ProceedingJoinPoint joinPoint) {
        Log.d("aspect", "afterMethod");
    }

来看一下打印日志是这样的

11-12 06:30:52.476 10388-10388/com.jie.aoptest D/aspect: beforeMethod
11-12 06:30:52.477 10388-10388/com.jie.aoptest D/login: 请您登录
11-12 06:30:52.486 10388-10388/com.jie.aoptest D/aspect: afterMethod

然后我们用around做一个测试

    @Pointcut("execution(@com.jie.aoptest.aop.CheckLogin * *(..))")
    public void methodAnnotated() {
    }

    @Around("methodAnnotated()")
    public void aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.d("aspect", "aroundMethod");
        Log.d("login", "请您登录");
        Toast.makeText(App.getAppContext().getCurActivity(), "请您登录", Toast.LENGTH_SHORT).show();
        joinPoint.proceed();
        Log.d("aspect", "aroundMethod");
    }

打印日志是这样的

11-12 06:35:09.696 10512-10512/com.jie.aoptest D/aspect: aroundMethod
11-12 06:35:09.696 10512-10512/com.jie.aoptest D/login: 请您登录
11-12 06:35:09.700 10512-10512/com.jie.aoptest D/aspect: aroundMethod

两种方式都可以,但要注意一点around和after两种类型是有冲突的,around和before可以共存,所以还是建议两种方式,一种before和after配合使用,一种around单独使用。

参数的获取

方法参数的获取

方法参数的获取很简单,可以通过joinPoint.getArgs()来获取参数,举个例子:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        safe("haha", 20, true);
        ...
    }
    
    @Safe
    private void safe(String a, int b, boolean c) {
        Log.d("aop", "获取参数")
    }

通知方法

    @Around("execution(!synthetic * *(..)) && methodAnnotated()")
    public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
        for (Object arg : joinPoint.getArgs()) {
            Log.d("arg", arg.toString());
        }
        joinPoint.proceed(joinPoint.getArgs());
    }

日志打印

11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: haha
11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: 20
11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: true

注解参数的获取

直接上代码例子
首先需要在注解上声明参数

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
    //声明参数
    String declaredPermission();
}

然后看一下Activity中的调用方法,注意这里在注解后设置参数值

    @CheckPermission(declaredPermission="android.permission.READ_PHONE_STATE")
    private void checkPhoneState(){
        Log.d("CheckPermission","Read Phone State succeed");
    }

看一下切片类的写法,注意这里在切点上要用@annotation来获取注解对象,然后我们在aroundMethod方法中多了一个checkPermission对象,最后从这个对象中拿到注解参数

@Aspect
public class CheckPermissionAspect {

    @Pointcut("execution(@com.jie.aoptest.aop.CheckPermission * *(..)) && @annotation(checkPermission)")
    public void checkPermission(CheckPermission checkPermission){};

    @Around("checkPermission(checkPermission)")
    public void aroundMethod(JoinPoint joinPoint, CheckPermission checkPermission){
        //从注解信息中获取声明的权限。
        String neededPermission = checkPermission.declaredPermission();
        Log.d("CheckPermissionAspect", joinPoint.toShortString());
        Log.d("CheckPermissionAspect", "\tneeded permission is " + neededPermission);
    }
}

最后来看一下输出日志,证明我们已经成功拿到注解参数了

11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermissionAspect: execution(MainActivity.checkPhoneState())
11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermissionAspect:     needed permission is android.permission.READ_PHONE_STATE
11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermission: Read Phone State succeed

总结

AspectJ解析基本就到这里了,掌握了它就可以全面的用AOP思想去解决问题了,核心还是在解决问题的思路。我也是在边学习边整理从而写出这篇文档,这里讲解了一些AspectJ的基础用法,高级用法大家可以从参考文献的书中去慢慢探索。

参考文献

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

推荐阅读更多精彩内容