AOP

13.3.1 AOP

1. 简介

OOP(Object Oriented Programming)面向对象编程。在OOP的世界中,问题或者功能都被划分到一个一个的模块里边。每个模块专心干自己的事情,模块之间通过设计好的接口交互。如下图就是Android Framework中的模块:

Android Framework中的模块

OOP的精髓是把++功能或问题模块化,每个模块处理自己的家务事++。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单而又常见的例子:现在想为每个模块加上日志功能,要求模块运行时候能输出日志。在不知道AOP的情况下,一般的处理都是:先设计一个日志输出模块,这个模块提供日志输出API,比如Android中的Log类。然后,其他模块需要输出日志的时候调用Log类的几个方法。这种方式功能是得到了满足,但是好像没有Oriented的感觉了。是的,随意加日志输出功能,使得其他模块的代码和日志模块耦合非常紧密。而且,将来要是日志模块修改了API,则使用它们的地方都得改。

AOP(Aspect Oriented Programming)面向切向编程。AOP的目标==是把这些分散在各个模块中的功能集中起来,放到一个统一的地方来控制和管理==。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。比如我们可以设计两个Aspects,一个是管理某个软件中所有模块的日志输出的功能,另外一个是管理该软件中一些特殊函数调用的权限检查。

2. AspectJ

AOP是一种思想,就好像OOP中的Java一样,一些先行者也开发了一套语言来支持AOP。目前用得比较火的就是AspectJ了,它是一种几乎和Java完全一样的==语言==,而且完全兼容Java。当然,除了使用AspectJ特殊的语言外,AspectJ还支持原生的Java,只要加上对应的AspectJ注解就好。所以,使用AspectJ有两种方法:

  • 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
  • 或者使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ。(推荐)

不论哪种方法,最后都需要AspectJ的编译工具ajc来编译。由于AspectJ实际上脱胎于Java,所以ajc工具也能编译java源码。接下来介绍下几个概念。

(1) Join Points

Join Points(以后简称==JPoints==)是AspectJ中最关键的一个概念,JPoints就是==程序运行时的一些执行点==。那么,一个程序中,哪些执行点是JPoints呢?比如:

  • 一个函数的调用可以是一个JPoint。比如Log.e()这个函数。e的执行可以是一个JPoint,而调用e的函数也可以认为是一个JPoint。
  • 设置一个变量,或者读取一个变量,也可以是一个JPoint。比如Demo类中有一个disable的boolean变量。设置它的地方或者读取它的地方都可以看做是JPoints。
  • for循环可以看做是JPoint。

理论上说,一个程序中很多地方都可以被看做是JPoint,但是AspectJ中,只有如表1所示的几种执行点被认为是JPoints:

Join Points 说明 示例
method call 函数调用 比如调用Log.e(),这是一处JPoint
method execution 函数执行 比如Log.e()的执行内部,是一处JPoint
constructor call 构造函数调用 和method call类似
constructor execution 构造函数执行 和method execution类似
field get 获取变量 比如读取DemoActivity.debug成员
field set 设置变量 比如设置DemoActivity.debug成员
static initialization 类初始化 比如类的static{}
handler 异常处理 比如try catch(xxx)中,对应catch内的执行
(2) Pointcuts

一个程序会有很多的JPoints,即使是同一个函数(比如testMethod这个函数),还分为==call类型和execution类型==的JPoint。显然,不是所有的JPoint,也不是所有类型的JPoint都是我们关注的。比如我们只要求在Activity的几个生命周期函数中打印日志,只有这几个生命周期函数才是我们业务需要的JPoint,而其他的什么JPoint我不需要关注。

怎么从一堆一堆的JPoints中选择自己想要的JPoints呢?恩,这就是Pointcuts的功能。一句话,==Pointcuts的目标是提供一种方法使得开发者能够选择自己感兴趣的JoinPoints==。

(i) 一个Pointcuts例子

@Pointcut("call(public * *.println(..)) && !within(TestAspect)")

  • call(public * *.println(..))是一种选择条件。call表示我们选择的Joinpoint类型为call类型。
  • public * *.println(..):这小行代码使用了通配符。由于我们这里选择的JoinPoint类型为call类型,它对应的目标JPoint一定是某个函数。所以我们要找到这个/些函数。public 表示目标JPoint的访问类型(public/private/protect)。第一个*表示返回值的类型是任意类型。第二个*用来指明包名。此处不限定包名。紧接其后的println是函数名。这表明我们选择的函数是任何包中定义的名字叫println的函数。当然,唯一确定一个函数除了包名外,还有它的参数。在(..)中,就指明了目标函数的参数应该是什么样子的。比如这里使用了通配符..,代表任意个数的参数,任意类型的参数。
  • call后面的&&:AspectJ可以把几个条件组合起来,目前支持 &&,||,以及!这三个条件。
  • !within(TestAspectJ):前面的!表示不满足某个条件。within是另外一种类型选择方法,特别注意,这种类型和前面讲到的joinpoint的那几种类型不同。within的类型是数据类型,而joinpoint的类型更像是动态的,执行时的类型。

上例中的pointcut合起来就是:

  1. 选择那些调用println(而且不考虑println函数的参数是什么)的Joinpoint。
  2. 调用者的类型不要是TestAspect的。
(ii) 直接针对JoinPoint的选择

pointcuts中最常用的选择条件和Joinpoint的类型密切相关:

不同类型的JPoint对应的pointcuts查询方法

一个==Method Signature==的完整表达式为:@注解 访问权限 返回值类型 包名.函数名(参数)

  • @注解和访问权限(public/private/protect,以及static/final)属于可选项。如果不设置它们那么public,private,protect及static、final的函数都会进行搜索。
  • 返回值类型就是普通的函数的返回值类型。如果不限定类型的话,就用*通配符表示
  • 包名.函数名用于查找匹配的函数。可以使用通配符,包括*和..以及+号。其中*号用于匹配除.号之外的任意字符,而..则表示任意子package,+号表示子类。

java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase,也可以表示TestDervied

java..*:表示java任意子类
java..*Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,TreeModel等

  • 最后来看函数的参数。参数匹配比较简单,主要是参数类型。

(int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char
(String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限。在参数匹配中,
..代表任意参数个数和类型
(Object ...):表示不定个数的参数,且类型都是Object,这里的...不是通配符,而是Java中代表不定参数的意思

(iii) 间接针对JPoint的选择

除了根据前面提到的Signature信息来匹配JPoint外,AspectJ还提供其他一些选择方法来选择JPoint。比如某个类中的所有JPoint,每一个函数执行流程中所包含的JPoint。

下表列出了一些常用的非JPoint选择方法:

关键词 说明 示例
within(TypePattern) TypePattern标示package或者类,可以使用通配符 表示某个Package或者类中的所有JPoint。比如within(Test):Test类中(包括内部类)所有JPoint。图2所示的例子就是用这个方法。
this(Type) JPoint的this对象是Type类型。(其实就是判断Type是不是某种类型,即是否满足instanceof Type的条件) JPoint是代码段(不论是函数,异常处理,static block),从语法上说,它都属于一个类。如果这个类的类型是Type标示的类型,则和它相关的JPoint将全部被选中。图2示例的testMethod是TestDerived类。所以this(TestDerived)将会选中这个testMethod JPoint
target(Type) JPoint的target对象是Type类型 和this相对的是target。不过target一般用在call的情况。call一个函数,这个函数可能定义在其他类。比如testMethod是TestDerived类定义的。那么target(TestDerived)就会搜索到调用testMethod的地方。但是不包括testMethod的execution JPoint
args(TypeSignature) 用来对JPoint的参数进行条件搜索的 比如args(int,..),表示第一个参数是int,后面参数个数和类型不限的JPoint。
(3) advice

现在,我们知道如何通过pointcuts来选择合适的JPoint。那么,下一步工作就是选择这些JPoint后,需要干一些事情的。比如前面例子中的输出都有before,after之类的。这其实JPoint在执行前,执行后,都执行了一些我们设置的代码。在AspectJ中,这段代码叫advice。简单点说,advice就是一种Hook。

关键词 说明
before() 表示在JPoint执行之前,需要干的事情
after() 表示JPoint自己执行完了后,需要干的事情
返回值类型 around() ==替代了原JPoint==,如果要执行原JPoint的话,需要调用proceed
(4) 例子
(1) 示例一
@Aspect   //必须使用@AspectJ标注
public class DemoAspect {  
    static final String TAG = "DemoAspect";  
/* 
@Pointcut:定义一个pointcut,这个注解是针对一个函数的,比如此处的logForActivity() 
其实它代表了这个pointcut的名字。如果是带参数的pointcut,则把参数类型和名字放到 
代表pointcut名字的logForActivity中,然后在@Pointcut注解中使用参数名。 
*/  
@Pointcut("execution(* com.androidaop.demo.AopDemoActivity.onCreate(..)) ||"  
        +"execution(* com.androidaop.demo.AopDemoActivity.onStart(..))")  
public void logForActivity(){};  //注意,这个函数必须要有实现,否则Java编译器会报错  
   
/* 
@Before:这就是Before的advice。Before后面跟的是pointcut名字,然后其代码块由一个函数来实现。比如此处的log。 
*/  
@Before("logForActivity()")  
public void log(JoinPoint joinPoint){  
    //对于使用Annotation的AspectJ而言,JoinPoint就不能直接在代码里得到多了,而需要通过  
    //参数传递进来。  
    Log.e(TAG, joinPoint.toShortString());  
    }  
}  
(2) 示例二
//第一个@Target表示这个注解只能给函数使用  
//第二个@Retention表示注解内容需要包含的Class字节码里,属于运行时需要的。  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface SecurityCheckAnnotation {//@interface用于定义一个注解。  
    publicString declaredPermission();  //declarePermssion是一个函数,其实代表了注解里的参数  
}  

怎么使用注解呢?接着看代码:

//为checkPhoneState使用SecurityCheckAnnotation注解,并指明调用该函数的人需要声明的权限  
@SecurityCheckAnnotation(declaredPermission="android.permission.READ_PHONE_STATE")  
private void checkPhoneState(){  
    //如果不使用AOP,就得自己来检查权限  
    if(checkPermission("android.permission.READ_PHONE_STATE") ==false){  
        Log.e(TAG,"have no permission to read phone state");  
        return;  
    }  
    Log.e(TAG,"Read Phone State succeed");  
    return;  
}  

我们来看看如何在AspectJ中,充分利用这注解信息来帮助我们检查权限。

/* 
来看这个Pointcut,首先,它在选择Jpoint的时候,把@SecurityCheckAnnotation使用上了,
这表明所有那些public的,并且携带有这个注解的API都是目标JPoint。如果是带参数的pointcut,
则把参数类型和名字放到代表pointcut名字的checkPermssion中,然后在@Pointcut注解中使用参数名。
接着,由于我们希望在函数中获取注解的信息,所以这里的poincut函数有一个参数,参数类型是 
SecurityCheckAnnotation,参数名为ann。
这个参数我们需要在后面的advice里用上,所以pointcut还使用了@annotation(ann)这种方法来告诉 
AspectJ,这个ann是一个注解 
*/ 
@Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)")  
public void checkPermssion(SecurityCheckAnnotation ann){};  
   
/* 
接下来是advice,advice的真正功能由check函数来实现,这个check函数第二个参数就是我们想要 
的注解。在实际运行过程中,AspectJ会把这个信息从JPoint中提出出来并传递给check函数。 
*/  
@Before("checkPermssion(securityCheckAnnotation)")  
public void check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){  
    //从注解信息中获取声明的权限。  
    String neededPermission = securityCheckAnnotation.declaredPermission();  
    Log.e(TAG, joinPoint.toShortString());  
    Log.e(TAG, "\t needed permission is " + neededPermission);  
    return;  
}  
(3) 示例三
@Aspect
public class FragmentAspectj {
    private final static String TAG = FragmentAspectj.class.getCanonicalName();

    @Around("execution(* android.support.v4.app.Fragment.onCreateView(..))")
    public Object fragmentOnCreateViewMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        return trackFragmentView(joinPoint);
    }

    @Around("execution(* android.app.Fragment.onCreateView(..))")
    public Object fragmentOnCreateViewMethod2(ProceedingJoinPoint joinPoint) throws Throwable {
        return trackFragmentView(joinPoint);
    }

    private Object trackFragmentView(final ProceedingJoinPoint joinPoint) throws Throwable {
        // 被注解的方法在这一行代码被执行
        Object result = joinPoint.proceed();
        AopUtil.sendTrackEventToSDK3(joinPoint, "trackFragmentView", result);
        return result;
    }

    @After("execution(* android.support.v4.app.Fragment.onHiddenChanged(boolean))")
    public void onHiddenChangedMethod(JoinPoint joinPoint) throws Throwable {
        AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentHiddenChangedMethod");
    }

    @After("execution(* android.support.v4.app.Fragment.setUserVisibleHint(boolean))")
    public void setUserVisibleHintMethod(JoinPoint joinPoint) throws Throwable {
        AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentSetUserVisibleHintMethod");
    }

    @After("execution(* android.support.v4.app.Fragment.onResume())")
    public void onResumeMethod(JoinPoint joinPoint) throws Throwable {
        AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentOnResumeMethod");
    }
}

参考文献

深入理解Android之AOP

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

推荐阅读更多精彩内容

  • 里烈:2018-01-30 目标:女儿能够学会学习,喜欢学习。顺利通过国考 好种子: 1、怀着感恩,高兴愉悦的心情...
    里喻棋阅读 84评论 0 0
  • 从不知道自己不知道~知道自己不知道~不知道自己知道,是一个认知的历程。我们大多数人都在第一或第二阶段。 曾经很苦恼...
    milk76阅读 281评论 2 1
  • 如果有人要我推荐种植一种美丽的花在庭院、在阳台、在花园小径边。这种植物能开出美丽的花、一年四季枝叶浓绿、不用怎么打...
    魚兒水中游阅读 1,433评论 0 0