Android中AOP(⾯向切向编程)的深⼊讲解
⼀、闲谈AOP
⼤家都知道OOP,即ObjectOriented Programming,⾯向对象编程。⽽本⽂要介绍的是AOP。AOP是Aspect Oriented Programming的缩写,中译⽂为⾯向切向编程。OOP和AOP是什么关系呢?
⾸先:
l OOP和AOP都是⽅法论。我记得在刚学习C++的时候,最难学的并不是C++的语法,⽽是C++所代表的那种看问题的⽅法,即OOP。同样,今天在AOP中,我发现其难度并不在利⽤AOP⼲活,⽽是从AOP的⾓度来看待问题,设计解决⽅法。这就是为什么我特意强调AOP是⼀种⽅法论的原因!
l 在OOP的世界中,问题或者功能都被划分到⼀个⼀个的模块⾥边。每个模块专⼼⼲⾃⼰的事情,模块之间通过设计好的接⼝交互。从图⽰来看,OOP世界中,最常见的表⽰⽐如:
图1 Android Framework中的模块
图1中所⽰为AndroidFramework中的模块。OOP世界中,⼤家画的模块图基本上是这样的,每个功能都放在⼀个模块⾥。⾮常好理解,⽽且确实简化了我们所处理问题的难度。
OOP的精髓是把功能或问题模块化,每个模块处理⾃⼰的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单⽽⼜常见的例⼦:现在想为每个模块加上⽇志功能,要求模块运⾏时候能输出⽇志。在不知道AOP的情况下,⼀般的处理都是:先设计⼀个⽇志输出模块,这个模块提供⽇志输出API,⽐如Android中的Log类。然后,其他模块需要输出⽇志的时候调⽤Log类的⼏个函数,⽐如e(TAG,...),w(TAG,...),d(TAG,...),i(TAG,...)等。
在没有接触AOP之前,包括我在内,想到的解决⽅案就是上⾯这样的。但是,从OOP⾓度看,除了⽇志模块本⾝,其
他模块的家务事绝⼤部分情况下应该都不会包含⽇志输出功能。什么意思?以ActivityManagerService为例,你能说它的家务事⾥包含⽇志输出吗?显然,ActivityManagerService的功能点中不包含输出⽇志这⼀项。但实际上,软件中的众多模块确实⼜需要打印⽇志。这个⽇志输出功能,从整体来看,都是⼀个⾯上的。⽽这个⾯的范围,就不局限在单个模块⾥了,⽽是横跨多个模块。
在没有AOP之前,各个模块要打印⽇志,就是⾃⼰处理。反正⽇志模块的那⼏个API都已经写好了,你在其他模块的任何地⽅,任何时候都可以调⽤。功能是得到了满⾜,但是好像没有Oriented的感觉了。是的,随意加⽇志输出功能,使得其他模块的代码和⽇志模块耦合⾮常紧密。⽽且,将来要是⽇志模块修改了API,则使⽤它们的地⽅都得改。这种搞法,⼀点也不酷。
AOP的⽬标就是解决上⾯提到的不cool的问题。在AOP中:
第⼀,我们要认识到OOP世界中,有些功能是横跨并嵌⼊众多模块⾥的,⽐如打印⽇志,⽐如统计某个模块中某些函数的执⾏时间等。这些功能在各个模块⾥分散得很厉害,可能到处都能见到。
第⼆,AOP的⽬标是把这些功能集中起来,放到⼀个统⼀的地⽅来控制和管理。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某⼀类问题进⾏统⼀管理。⽐如我们可以设计两个Aspects,⼀个是管理某个软件中所有模块的⽇志输出的功能,另外⼀个是管理该软件中⼀些特殊函数调⽤的权限检查。
讲了这么多,还是先来看个例⼦。在这个例⼦中,我们要:
Activity的⽣命周期的⼏个函数运⾏时,要输出⽇志。
⼏个重要函数调⽤的时候,要检查有没有权限。
⼆、没有AOP的例⼦
先来看没有AOP的情况下,代码怎么写。主要代码都在AopDemoActivity中
[-->AopDemoActivity.java]
public class AopDemoActivity extends Activity {
private static final String TAG = "AopDemoActivity";
onCreate,onStart,onRestart,onPause,onResume,onStop,onDestory返回前,都输出⼀⾏⽇志
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.layout_main);
Log.e(TAG,"onCreate");
}
protected void onStart() {
Log.e(TAG, "onStart");
}
protected void onRestart() {
Log.e(TAG, "onRestart");
}
protectedvoid onResume() {
Log.e(TAG, "onResume");
checkPhoneState会检查app是否申明了android.permission.READ_PHONE_STATE权限
checkPhoneState();
}
protected void onPause() {
Log.e(TAG, "onPause");
}
protected void onStop() {
Log.e(TAG, "onStop");
}
protected void onDestroy() {
Log.e(TAG, "onDestroy");
}
private void checkPhoneState(){
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;
}
private boolean checkPermission(String permissionName){
try{
PackageManager pm = getPackageManager();
//调⽤PackageMangaer的checkPermission函数,检查⾃⼰是否申明使⽤某权限
int nret = pm.checkPermission(permissionName,getPackageName());
return nret == PackageManager.PERMISSION_GRANTED;
}......
}
}
代码很简单。但是从这个⼩例⼦中,你也会发现要是这个程序⽐较复杂的话,到处都加Log,或者在某些特殊函数加权限检查的代码,真的是⼀件挺繁琐的事情。
三、AspectJ介绍
3.1 AspectJ极简介
AOP虽然是⽅法论,但就好像OOP中的Java⼀样,⼀些先⾏者也开发了⼀套语⾔来⽀持AOP。⽬前⽤得⽐较⽕的就是AspectJ 了,它是⼀种⼏乎和Java完全⼀样的语⾔,⽽且完全兼容Java(AspectJ应该就是⼀种扩展Java,但它不是像Groovy[1]那样的拓展。)。当然,除了使⽤AspectJ特殊的语⾔外,AspectJ还⽀持原⽣的Java,只要加上对应的AspectJ注解就好。所以,使⽤AspectJ有两种⽅法:
完全使⽤AspectJ的语⾔。这语⾔⼀点也不难,和Java⼏乎⼀样,也能在AspectJ中调⽤Java的任何类库。AspectJ只是多了⼀些关键词罢了。
或者使⽤纯Java语⾔开发,然后使⽤AspectJ注解,简称@AspectJ。
Anyway,不论哪种⽅法,最后都需要AspectJ的编译⼯具ajc来编译。由于AspectJ实际上脱胎于Java,所以ajc⼯具也能编译java 源码。
AspectJ现在托管于Eclipse项⽬中,官⽅⽹站是:
/aspectj/ <=AspectJ官⽅⽹站
/aspectj/doc/released/runtime-api/index.html <=AspectJ类库参考⽂档,内容⾮常少
springboot实现aop
/aspectj/doc/released/aspectj5rt-api/index.html <=@AspectJ⽂档,以后我们⽤Annotation的⽅式最多。
3.2 AspectJ语法
题外话:AspectJ东西⽐较多,但是AOP做为⽅法论,它的学习和体会是需要⼀步⼀步,并且⼀定要结合实际来的。如果⼀下⼦讲太多,反⽽会疲倦。更可怕的是,有些胆肥的同学要是⼀股脑把所有⾼级玩法全弄上去,反⽽得不偿失。这就是是⽅法论学习和其他知识学习不⼀样的地⽅。请⼤家切记。
3.2.1 Join Points介绍
Join Points(以后简称JPoints)是AspectJ中最关键的⼀个概念。什么是JPoints呢?JPoints就是程序运⾏时的⼀些执⾏点。那么,⼀个程序中,哪些执⾏点是JPoints呢?⽐如:
⼀个函数的调⽤可以是⼀个JPoint。⽐如Log.e()这个函数。e的执⾏可以是⼀个JPoint,⽽调⽤e的函数也可以认为是⼀个
JPoint。
设置⼀个变量,或者读取⼀个变量,也可以是⼀个JPoint。⽐如Demo类中有⼀个debug的boolean变量。设置它的地⽅或者读取它的地⽅都可以看做是JPoints。
for循环可以看做是JPoint。
理论上说,⼀个程序中很多地⽅都可以被看做是JPoint,但是AspectJ中,只有如表1所⽰的⼏种执⾏点被认为是JPoints:
表1 AspectJ中的Join Point
Join Points 说明
⽰例
method call函数调⽤⽐如调⽤Log.e(),这是⼀处JPoint
method
execution函数执⾏⽐如Log.e()的执⾏内部,是⼀处JPoint。注意它和method call的区别。method call是调⽤某个函数的地⽅。⽽execution是某个函数执⾏的内部。
constructor
call构造函数调⽤和method call类似constructor
execution构造函数执⾏和method execution类似
field get获取某个变量⽐如读取DemoActivity.debug成员field set设置某个变量⽐如设置DemoActivity.debug成员
pre-initialization Object在构造函数中做
得⼀些⼯作。
很少使⽤,详情见下⾯的例⼦
initialization Object在构造函数中做
得⼯作
详情见下⾯的例⼦
static
initialization类初始化⽐如类的static{}
handler异常处理⽐如try catch(xxx)中,对应catch内的执⾏
advice execution 这个是AspectJ的内容,稍后再说
表1列出了AspectJ所认可的JoinPoints的类型。下⾯我们来看个例⼦以直观体会⼀把。
图2 ⽰例代码
图2是⼀个Java⽰例代码,下⾯我们将打印出其中所有的join points。图3所⽰为打印出来的join points:
图3 所有的join points
图3中的输出为从左到右,我们来解释红框中的内容。先来看左图的第⼀个红框:
staticinitialization(test.Test.<clinit>):表⽰当前是哪种类型的JPoint,括号中代表⽬标对象是谁(此处是指Test class的类初始化)。由于Test类没有指定static block,所以后⾯的at:Test.java:0 表⽰代码在第0⾏(其实就是没有到源代码的意思)。
Test类初始化完后,就该执⾏main函数了。所以,下⼀个JPoint就是execution(voidtest.Test.main(String[]))。括号中表⽰此JPoint对应的是test.Test.main函数。at:Test.java:30表⽰这个JPoint在源代码的第30⾏。⼤家可以对⽐图2的源码,很准确!
main函数⾥⾸先是执⾏System.out.println。⽽这⼀⾏代码实际包括两个JPoint。⼀个是get(PrintStream
java.lang.System.out),get表⽰Field get,它表⽰从System中获取out对象。另外⼀个是call(void
java.io.PrintStream.println(String)),这是⼀个call类型的JPoint,表⽰执⾏out.println函数。
再来看左图第⼆个红框,它表⽰TestBase的类的初始化,由于源码中为TestBase定义了static块,所以这个JPoint清晰指出了源码的位置是at:Test.java:5
接着看左图第三个红框,它和对象的初始化有关。在源码中,我们只是构造了⼀个TestDerived对象。它会先触发TestDerived Preinitialization JPoint,然后触发基类TestBase的PreInitialization JPoint。注意红框中的before和after 。在TestDerived和TestBase所对应的PreInitialization before和after中都没有包含其他JPoint。所以,Pre-Initialization应该是构造函数中⼀个⽐较基础的Phase。这个阶段不包括类中成员变量定义时就赋值的操作,也不包括构造函数中对某些成员变量进⾏的赋值操作。
⽽成员变量的初始化(包括成员变量定义时就赋值的操作,⽐如源码中的int base = 0,以及在构造函数中所做的赋值操作,⽐如源码中的this.derived = 1000)都被囊括到initialization阶段。请读者对应图三第⼆个红框到第三个红框(包括第3个红框的内容)看看是不是这样的。
最后来看第5个红框。它包括三个JPoint:
testMethod的call类型JPoint
testMethod的execution类型JPonint
以及对异常捕获的Handler类型JPoint
好了。JPoint的介绍就先到此。现在⼤家对JoinPoint应该有了⼀个很直观的体会,简单直⽩粗暴点说,JoinPoint就是⼀个程序中的关键函数(包括构造函数)和代码段(staticblock)。
为什么AspectJ⾸先要定义好JoinPoint呢?⼤家仔细想想就能明⽩,以打印log的AopDemo为例,log在哪⾥打印?⾃然是在⼀些关键点去打印。⽽谁是关键点?AspectJ定义的这些类型的JPoint就能满⾜我们绝⼤部分需求。
注意,要是想在⼀个for循环中打印⼀些⽇志,⽽AspectJ没有这样的JPoint,所以这个需求我们是⽆法利⽤AspectJ来实现了。另外,不同的软件框架对表1中的JPoint类型⽀持也不同。⽐如Spring中,不是所有AspectJ⽀持的JPoint都有。
3.2.2 Pointcuts介绍
pointcuts这个单词不好翻译,此处直接⽤英⽂好了。那么,Pointcuts是什么呢?前⾯介绍的内容可知,⼀个程序会有很多的JPoints,即使是同⼀个函数(⽐如testMethod这个函数),还分为call类型和execution类型的JPoint。显然,不是所有的JPoint,也不是所有类型的JPoint都是我们关注的。再次以A
opDemo为例,我们只要求在Activity的⼏个⽣命周期函数中打印⽇志,只有这⼏个⽣命周期函数才是我们业务需要的JPoint,⽽其他的什么JPoint我不需要关注。
怎么从⼀堆⼀堆的JPoints中选择⾃⼰想要的JPoints呢?恩,这就是Pointcuts的功能。⼀句话,Pointcuts的⽬标是提供⼀种⽅法使得开发者能够选择⾃⼰感兴趣的JoinPoints。
在图2的例⼦中,怎么把Test.java中所有的Joinpoint选择出来呢?⽤到的pointcut格式为:
pointcuttestAll():within(Test)。
AspectJ中,pointcut有⼀套标准语法,涉及的东西很多,还有⼀些⽐较⾼级的玩法。我⾃⼰琢磨了半天,需不需要把这些内容⼀股脑都搬到此⽂呢?回想我⾃⼰学习AOP的经历,好像看了⼏本书,记得⽐较清楚的都是简单的case,⽽那些复杂的case则是到实践中,确实有需求了,才回过头来,重新查阅⽂档来实施的。恩,那就⼀步⼀步来吧。
(1)⼀个Pointcuts例⼦
直接来看⼀个例⼦,现在我想把图2中的⽰例代码中,那些调⽤println的地⽅到,该怎么弄?代码该这么写:
public pointcut testAll(): call(public * *.println(..)) && !within(TestAspect) ;
注意,aspectj的语法和Java⼀样,只不过多了⼀些关键词
我们来看看上述代码
第⼀个public:表⽰这个pointcut是public访问。这主要和aspect的继承关系有关,属于AspectJ的⾼级玩法,本⽂不考虑。
pointcut:关键词,表⽰这⾥定义的是⼀个pointcut。pointcut定义有点像函数定义。总之,在AspectJ中,你得定义⼀个pointcut。 testAll():pointcut的名字。在AspectJ中,定义Pointcut可分为有名和匿名两种办法。个⼈建议使⽤named⽅法。因为在后⾯,我们要使⽤⼀个pointcut的话,就可以直接使⽤它的名字就好。
testAll后⾯有⼀个冒号,这是pointcut定义名字后,必须加上。冒号后⾯是这个pointcut怎么选择Joinpoint的条件。
本例中,call(public * *.println(..))是⼀种选择条件。call表⽰我们选择的Joinpoint类型为call类型。
public **.println(..):这⼩⾏代码使⽤了通配符。由于我们这⾥选择的JoinPoint类型为call类型,它对应的⽬标JPoint⼀定是某个函数。所以我们要到这个/些函数。public 表⽰⽬标JPoint的访问类型(public/private/protect)。第⼀个*表⽰返回值的类型是任意类型。第⼆个*⽤来指明包名。此处不限定
包名。紧接其后的println是函数名。这表明我们选择的函数是任何包中定义的名字叫println的函数。当然,唯⼀确定⼀个函数除了包名外,还有它的参数。在(..)中,就指明了⽬标函数的参数应该是什么样⼦的。⽐如这⾥使⽤了通配符..,代表任意个数的参数,任意类型的参数。
再来看call后⾯的&&:AspectJ可以把⼏个条件组合起来,⽬前⽀持 &&,||,以及!这三个条件。这三个条件的意思不⽤我说了吧?和Java中的是⼀样的。
来看最后⼀个!within(TestAspectJ):前⾯的!表⽰不满⾜某个条件。within是另外⼀种类型选择⽅法,特别注意,这种类型和前⾯讲到的joinpoint的那⼏种类型不同。within的类型是数据类型,⽽joinpoint的类型更像是动态的,执⾏时的类型。
上例中的pointcut合起来就是:
选择那些调⽤println(⽽且不考虑println函数的参数是什么)的Joinpoint。
另外,调⽤者的类型不要是TestAspect的。
图4展⽰了执⾏结果:
图4 新pointcut执⾏结果
我在图2所⽰的源码中,为Test类定义了⼀个public static void println()函数,所以图4的执⾏结果就把这个println给匹配上了。
看完例⼦,我们来讲点理论。
(2)直接针对JoinPoint的选择
pointcuts中最常⽤的选择条件和Joinpoint的类型密切相关,⽐如图5:
图5 不同类型的JPoint对应的pointcuts查询⽅法
以图5为例,如果我们想选择类型为methodexecution的JPoint,那么pointcuts的写法就得包括execution(XXX)来限定。
除了指定JPoint类型外,我们还要更进⼀步选择⽬标函数。选择的根据就是图5中列出的什么
MethodSignature,ConstructorSignature,TypeSinature,FieldSignature等。名字听起来陌⽣得很,其实就是指定JPoint对应的函数(包括构造函数),Static block的信息。⽐如图4中的那个println例⼦,⾸先它的JPoint类型是call,所以它的查询条件是根据MethodSignature来表达。⼀个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中代表不定参数的意思
是不是很简单呢?
Constructorsignature和Method Signature类似,只不过构造函数没有返回值,⽽且函数名必须叫new。⽐如:
public *..w(..):
public:选择public访问权限
*..代表任意包名
(..):代表参数个数和类型都是任意
再来看Field Signature和Type Signature,⽤它们的地⽅见图5。下⾯直接上⼏个例⼦:
Field Signature标准格式:
@注解访问权限类型类名.成员变量名
其中,@注解和访问权限是可选的
类型:成员变量类型,*代表任意类型
类名.成员变量名:成员变量名可以是*,代表任意成员变量
⽐如,
set(inttest..TestBase.base):表⽰设置TestBase.base变量时的JPoint
Type Signature:直接上例⼦
staticinitialization(test..TestBase):表⽰TestBase类的static block
handler(NullPointerException):表⽰catch到NullPointerException的JPoint。注意,图2的源码第23⾏截获的其实是
Exception,其真实类型是NullPointerException。但是由于JPointer的查询匹配是静态的,即编译过程中进⾏的匹配,所以handler(NullPointerException)在运⾏时并不能真正被截获。只有改成handler(Exception),或者把源码第23⾏改成NullPointerException才⾏。
以上例⼦,读者都可以在aspectj-test例⼦中⾃⼰都试试。
(3)间接针对JPoint的选择
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论