13.3.1 AOP
1. 简介
OOP(Object Oriented Programming)面向对象编程。在OOP的世界中,问题或者功能都被划分到一个一个的模块里边。每个模块专心干自己的事情,模块之间通过设计好的接口交互。如下图就是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合起来就是:
- 选择那些调用println(而且不考虑println函数的参数是什么)的Joinpoint。
- 调用者的类型不要是TestAspect的。
(ii) 直接针对JoinPoint的选择
pointcuts中最常用的选择条件和Joinpoint的类型密切相关:
一个==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");
}
}