你们都知道OOP,即ObjectOriented Programming,面向对象编程。而本文要介绍的是AOP。AOP是Aspect Oriented Programming的缩写,中译文为面向切向编程。OOP和AOP是什么关系呢?首先:html
图1中所示为AndroidFramework中的模块。OOP世界中,你们画的模块图基本上是这样的,每一个功能都放在一个模块里。很是好理解,并且确实简化了咱们所处理问题的难度。java
OOP的精髓是把功能或问题模块化,每一个模块处理本身的家务事。但在现实世界中,并非全部问题都能完美得划分到模块中。举个最简单而又常见的例子:如今想为每一个模块加上日志功能,要求模块运行时候能输出日志。在不知道AOP的状况下,通常的处理都是:先设计一个日志输出模块,这个模块提供日志输出API,好比Android中的Log类。而后,其余模块须要输出日志的时候调用Log类的几个函数,好比e(TAG,...),w(TAG,...),d(TAG,...),i(TAG,...)等。android
在没有接触AOP以前,包括我在内,想到的解决方案就是上面这样的。可是,从OOP角度看,除了日志模块自己,其余模块的家务事绝大部分状况下应该都不会包含日志输出功能。什么意思?以ActivityManagerService为例,你能说它的家务事里包含日志输出吗?显然,ActivityManagerService的功能点中不包含输出日志这一项。但实际上,软件中的众多模块确实又须要打印日志。这个日志输出功能,从总体来看,都是一个面上的。而这个面的范围,就不局限在单个模块里了,而是横跨多个模块。git
AOP的目标就是解决上面提到的不cool的问题。在AOP中:sql
讲了这么多,仍是先来看个例子。在这个例子中,咱们要:编程
注意,本文的例子代码在https://code.csdn.net/Innost/androidaopdemo上。ubuntu
先来看没有AOP的状况下,代码怎么写。主要代码都在AopDemoActivity中api
[-->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) { super.onCreate(savedInstanceState); setContentView(R.layout.layout_main); Log.e(TAG,"onCreate"); } protected void onStart() { super.onStart(); Log.e(TAG, "onStart"); } protected void onRestart() { super.onRestart(); Log.e(TAG, "onRestart"); } protectedvoid onResume() { super.onResume(); Log.e(TAG, "onResume"); checkPhoneState会检查app是否申明了android.permission.READ_PHONE_STATE权限 checkPhoneState(); } protected void onPause() { super.onPause(); Log.e(TAG, "onPause"); } protected void onStop() { super.onStop(); Log.e(TAG, "onStop"); } protected void onDestroy() { super.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,或者在某些特殊函数加权限检查的代码,真的是一件挺繁琐的事情。数据结构
AOP虽然是方法论,但就好像OOP中的Java同样,一些先行者也开发了一套语言来支持AOP。目前用得比较火的就是AspectJ了,它是一种几乎和Java彻底同样的语言,并且彻底兼容Java(AspectJ应该就是一种扩展Java,但它不是像Groovy[1]那样的拓展。)。固然,除了使用AspectJ特殊的语言外,AspectJ还支持原生的Java,只要加上对应的AspectJ注解就好。因此,使用AspectJ有两种方法:
l 彻底使用AspectJ的语言。这语言一点也不难,和Java几乎同样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
l 或者使用纯Java语言开发,而后使用AspectJ注解,简称@AspectJ。
Anyway,不论哪一种方法,最后都须要AspectJ的编译工具ajc来编译。因为AspectJ实际上脱胎于Java,因此ajc工具也能编译java源码。
AspectJ如今托管于Eclipse项目中,官方网站是:
题外话:AspectJ东西比较多,可是AOP作为方法论,它的学习和体会是须要一步一步,而且必定要结合实际来的。若是一会儿讲太多,反而会疲倦。更可怕的是,有些胆肥的同窗要是一股脑把全部高级玩法全弄上去,反而得不偿失。这就是是方法论学习和其余知识学习不同的地方。请你们切记。
Join Points(之后简称JPoints)是AspectJ中最关键的一个概念。什么是JPoints呢?JPoints就是程序运行时的一些执行点。那么,一个程序中,哪些执行点是JPoints呢?好比:
理论上说,一个程序中不少地方均可以被看作是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中的输出为从左到右,咱们来解释红框中的内容。先来看左图的第一个红框:
再来看左图第二个红框,它表示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:
图2的示例代码我也放到了https://code.csdn.net/Innost/androidaopdemo上。请小伙伴们本身下载玩耍。具体的操做方法是:
我已经编译并提交了一个test.jar到git上,小伙伴们能够执行一把玩玩!
pointcuts这个单词很差翻译,此处直接用英文好了。那么,Pointcuts是什么呢?前面介绍的内容可知,一个程序会有不少的JPoints,即便是同一个函数(好比testMethod这个函数),还分为call类型和execution类型的JPoint。显然,不是全部的JPoint,也不是全部类型的JPoint都是咱们关注的。再次以AopDemo为例,咱们只要求在Activity的几个生命周期函数中打印日志,只有这几个生命周期函数才是咱们业务须要的JPoint,而其余的什么JPoint我不须要关注。
怎么从一堆一堆的JPoints中选择本身想要的JPoints呢?恩,这就是Pointcuts的功能。一句话,Pointcuts的目标是提供一种方法使得开发者可以选择本身感兴趣的JoinPoints。
在图2的例子中,怎么把Test.java中全部的Joinpoint选择出来呢?用到的pointcut格式为:
pointcuttestAll():within(Test)。
AspectJ中,pointcut有一套标准语法,涉及的东西不少,还有一些比较高级的玩法。我本身琢磨了半天,需不须要把这些内容一股脑都搬到此文呢?回想我本身学习AOP的经历,好像看了几本书,记得比较清楚的都是简单的case,而那些复杂的case则是到实践中,确实有需求了,才回过头来,从新查阅文档来实施的。恩,那就一步一步来吧。
好了。JPoint的介绍就先到此。如今你们对JoinPoint应该有了一个很直观的体会,简单直白粗暴点说,JoinPoint就是一个程序中的关键函数(包括构造函数)和代码段(staticblock)。
为何AspectJ首先要定义好JoinPoint呢?你们仔细想一想就能明白,以打印log的AopDemo为例,log在哪里打印?天然是在一些关键点去打印。而谁是关键点?AspectJ定义的这些类型的JPoint就能知足咱们绝大部分需求。
注意,要是想在一个for循环中打印一些日志,而AspectJ没有这样的JPoint,因此这个需求咱们是没法利用AspectJ来实现了。另外,不一样的软件框架对表1中的JPoint类型支持也不一样。好比Spring中,不是全部AspectJ支持的JPoint都有。
直接来看一个例子,如今我想把图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合起来就是:
图4展现了执行结果:
图4 新pointcut执行结果 |
我在图2所示的源码中,为Test类定义了一个public static void println()函数,因此图4的执行结果就把这个println给匹配上了。
看完例子,咱们来说点理论。
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 *..TestDerived.new(..): public:选择public访问权限 *..表明任意包名 TestDerived.new:表明TestDerived的构造函数 (..):表明参数个数和类型都是任意 再来看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例子中本身都试试。
除了根据前面提到的Signature信息来匹配JPoint外,AspectJ还提供其余一些选择方法来选择JPoint。好比某个类中的全部JPoint,每个函数执行流程中所包含的JPoint。
特别强调,不论什么选择方法,最终都是为了找到目标的JPoint。
表2列出了一些经常使用的非JPoint选择方法:
表2 其它经常使用选择方法
关键词 |
说明
|
示例 |
within(TypePattern) |
TypePattern标示package或者类。TypePatter可使用通配符 |
表示某个Package或者类中的全部JPoint。好比 within(Test):Test类中(包括内部类)全部JPoint。图2所示的例子就是用这个方法。 |
withincode(Constructor Signature|Method Signature) |
表示某个构造函数或其余函数执行过程当中涉及到的JPoint |
好比 withinCode(* TestDerived.testMethod(..)) 表示testMethod涉及的JPoint withinCode( *.Test.new(..)) 表示Test构造函数涉及的JPoint |
cflow(pointcuts) |
cflow是call flow的意思 cflow的条件是一个pointcut |
好比 cflow(call TestDerived.testMethod):表示调用TestDerived.testMethod函数时所包含的JPoint,包括testMethod的call这个JPoint自己 |
cflowbelow(pointcuts) |
cflow是call flow的意思。 |
好比 cflowblow(call TestDerived.testMethod):表示调用TestDerived.testMethod函数时所包含的JPoint,不包括testMethod的call这个JPoint自己 |
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。
|
上面这些东西,建议读者:
注意:this()和target()匹配的时候不能使用通配符。
图6给出了修改示例和输出:
图6 示例代码和输出结果
注意,不是全部的AOP实现都支持本节所说的查询条件。好比Spring就不支持withincode查询条件。
恭喜,看到这个地方来,AspectJ的核心部分就掌握一大部分了。如今,咱们知道如何经过pointcuts来选择合适的JPoint。那么,下一步工做就很明确了,选择这些JPoint后,咱们确定是须要干一些事情的。好比前面例子中的输出都有before,after之类的。这其实JPoint在执行前,执行后,都执行了一些咱们设置的代码。在AspectJ中,这段代码叫advice。简单点说,advice就是一种Hook。
ASpectJ中有好几个Hook,主要是根据JPoint执行时机的不一样而不一样,好比下面的:
before():testAll(){ System.out.println("before calling: " + thisJoinPoint);//打印这个JPoint的信息 System.out.println(" at:" + thisJoinPoint.getSourceLocation());//打印这个JPoint对应的源代码位置 }
testAll()是前面定义的pointcuts,而before()定义了在这个pointcuts选中的JPoint执行前咱们要干的事情。
表3列出了AspectJ所支持的Advice的类型:
表3 advice的类型
关键词 |
说明
|
示例 |
before() |
before advice |
表示在JPoint执行以前,须要干的事情 |
after() |
after advice |
表示JPoint本身执行完了后,须要干的事情。 |
after():returning(返回值类型) after():throwing(异常类型) |
returning和throwing后面均可以指定具体的类型,若是不指定的话则匹配的时候不限定类型 |
假设JPoint是一个函数调用的话,那么函数调用执行完有两种方式退出,一个是正常的return,另一个是抛异常。 注意,after()默认包括returning和throwing两种状况 |
返回值类型 around() |
before和around是指JPoint执行前或执行后备触发,而around就替代了原JPoint |
around是替代了原JPoint,若是要执行原JPoint的话,须要调用proceed |
注意,after和before没有返回值,可是around的目标是替代原JPoint的,因此它通常会有返回值,并且返回值的类型须要匹配被选中的JPoint。咱们来看个例子,见图7。
图7 advice示例和结果
图7中:
注意:从技术上说,around是彻底能够替代before和after的。图7中第二个红框还把after给注释掉了。若是不注释掉,编译时候报错,[error]circular advice precedence: can't determine precedence between two or morepieces of advice that apply to the same join point: method-execution(voidtest.Test$TestDerived.testMethod())(你们能够本身试试)。我猜想其中的缘由是around和after冲突了。around本质上表明了目标JPoint,好比此处的testMethod。而after是testMethod以后执行。那么这个testMethod究竟是around仍是原testMethod呢?真是傻傻分不清楚!
(我以为再加一些限制条件给after是能够避免这个问题的,可是没搞成功...)
advice讲完了。如今回顾下3.2节从开始到如今咱们学到了哪些内容:
上面这些东西都有点像函数定义,在Java中,这些东西都是要放到一个class里的。在AspectJ中,也有相似的数据结构,叫aspect。
public aspect 名字 {//aspect关键字和class的功能同样,文件名以.aj结尾 pointcuts定义... advice定义... }
你看,经过这种方式,定义一个aspect类,就把相关的JPoint和advice包含起来,是否是造成了一个“关注面”?好比:
经过这种方式,咱们在原来的JPoint中,就不须要写log打印的代码,也不须要写权限检查的代码了。全部这些关注点都挪到对应的Aspectj文件中来控制。恩,这就是AOP的精髓。
注意,读者在把玩代码时候,必定会碰到AspectJ语法不熟悉的问题。因此请读者记得随时参考官网的文档。这里有一个官方的语法大全:
http://www.eclipse.org/aspectj/doc/released/quick5.pdf 或者官方的另一个文档也能够:
http://www.eclipse.org/aspectj/doc/released/progguide/semantics.html
到此,AspectJ最基本的东西其实讲差很少了,可是在实际使用AspectJ的时候,你会发现前面的内容还欠缺一点,尤为是advice的地方:
l 前面介绍的advice都是没有参数信息的,而JPoint确定是或多或少有参数的。并且advice既然是对JPoint的截获或者hook也好,确定须要利用传入给JPoint的参数干点什么事情。比方所around advice,我能够对传入的参数进行检查,若是参数不合法,我就直接返回,根本就不须要调用proceed作处理。
往advice传参数比较简单,就是利用前面提到的this(),target(),args()等方法。另外,整个pointcuts和advice编写的语法也有一些区别。具体方法以下:
先在pointcuts定义时候指定参数类型和名字
pointcut testAll(Test.TestDerived derived,int x):call(*Test.TestDerived.testMethod(..)) && target(derived)&& args(x)
注意上述pointcuts的写法,首先在testAll中定义参数类型和参数名。这一点和定义一个函数彻底同样
接着看target和args。此处的target和args括号中用得是参数名。而参数名则是在前面pointcuts中定义好的。这属于target和args的另一种用法。
注意,增长参数并不会影响pointcuts对JPoint的匹配,上面的pointcuts选择和
pointcut testAll():call(*Test.TestDerived.testMethod(..)) && target(Test.TestDerived) &&args(int)是同样的
只不过咱们须要把参数传入advice,才须要改造
接下来是修改advice:
Object around(Test.TestDerived derived,int x):testAll(derived,x){ System.out.println(" arg1=" + derived); System.out.println(" arg2=" + x); return proceed(derived,x); //注意,proceed就必须把全部参数传进去。 }
advice的定义如今也和函数定义同样,把参数类型和参数名传进来。
接着把参数名传给pointcuts,此处是testAll。注意,advice必须和使用的pointcuts在参数类型和名字上保持一致。
而后在advice的代码中,你就能够引用参数了,好比derived和x,均可以打印出来。
总结,参数传递其实并不复杂,关键是得记住语法:
咱们前面示例中都打印出了JPoint的信息,好比当前调用的是哪一个函数,JPoint位于哪一行代码。这些都属于JPoint的信息。AspectJ为咱们提供以下信息:
关于thisJoinpoint,建议你们直接查看API文档,很是简单。其地址位于http://www.eclipse.org/aspectj/doc/released/runtime-api/index.html。
如今正式回到咱们的AndroidAopDemo这个例子来。咱们的目标是为AopDemoActivity的几个Activity生命周期函数加上log,另外为checkPhoneState加上权限检查。一切都用AOP来集中控制。
前面提到说AspectJ须要编写aj文件,而后把AOP代码放到aj文件中。可是在Android开发中,我建议不要使用aj文件。由于aj文件只有AspectJ编译器才认识,而Android编译器不认识这种文件。因此当更新了aj文件后,编译器认为源码没有发生变化,因此不会编译它。
固然,这种问题在其余不认识aj文件的java编译环境中也存在。因此,AspectJ提供了一种基于注解的方法来把AOP实现到一个普通的Java文件中。这样咱们就把AOP当作一个普通的Java文件来编写、编译就好。
立刻来看AopDemoActivity对应的DemoAspect.java文件吧。先看输出日志初版本:
[-->初版本]
package com.androidaop.demo; import android.util.Log; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.JoinPoint; @Aspect //必须使用@AspectJ标注,这样class DemoAspect就等同于 aspect DemoAspect了 public class DemoAspect { staticfinal 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,对于after,after -returning,和after-throwing。对于的注解格式为 @After,@AfterReturning,@AfterThrowing。Before后面跟的是pointcut名字,而后其代码块由一个函数来实现。好比此处的log。 */ @Before("logForActivity()") public void log(JoinPoint joinPoint){ //对于使用Annotation的AspectJ而言,JoinPoint就不能直接在代码里获得多了,而须要经过 //参数传递进来。 Log.e(TAG, joinPoint.toShortString()); } }
提示:若是开发者已经切到AndroidStudio的话,AspectJ注解是能够被识别并能自动补齐。
上面的例子仅仅是列出了onCreate和onStart两个函数的日志,若是想在全部的onXXX这样的函数里加上log,该怎么改呢?
@Pointcut("execution(* *..AopDemoActivity.on*(..))") public void logForActivity(){};
图8给出这个例子的执行结果:
图8 AopDemoActivity执行结果
检查权限这个功能的实现也能够采用刚才打印log那样,可是这样就没有太多意思了。咱们玩点高级的。不过这个高级的玩法也是来源于现实需求:
若是我有10个API,10个不一样的权限,那么在10个函数的注释里都要写,太麻烦了。怎么办?这个时候我想到了注解。注解的本质是源代码的描述。权限声明,从语义上来讲,实际上是属于API定义的一部分,两者是一个统一体,而不是分离的。
Java提供了一些默认的注解,不过此处咱们要使用本身定义的注解:
package com.androidaop.demo; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; //第一个@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 接着,因为咱们但愿在函数中获取注解的信息,全部这里的poincut函数有一个参数,参数类型是 SecurityCheckAnnotation,参数名为ann 这个参数咱们须要在后面的advice里用上,因此pointcut还使用了@annotation(ann)这种方法来告诉 AspectJ,这个ann是一个注解 */ @Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)") publicvoid checkPermssion(SecurityCheckAnnotationann){}; /* 接下来是advice,advice的真正功能由check函数来实现,这个check函数第二个参数就是咱们想要 的注解。在实际运行过程当中,AspectJ会把这个信息从JPoint中提出出来并传递给check函数。 */ @Before("checkPermssion(securityCheckAnnotation)") publicvoid check(JoinPoint joinPoint,SecurityCheckAnnotationsecurityCheckAnnotation){ //从注解信息中获取声明的权限。 String neededPermission = securityCheckAnnotation.declaredPermission(); Log.e(TAG, joinPoint.toShortString()); Log.e(TAG, "\tneeded permission is " + neededPermission); return; }
如此这般,咱们在API源码中使用的注解信息,如今就能够在AspectJ中使用了。这样,咱们在源码中定义注释,而后利用AspectJ来检查。图9展现了执行的结果
图9 权限检查的例子
事情这样就完了?很明显没有。为何?刚才权限检查只是简单得打出了日志,可是并无真正去作权限检查。如何处理?这就涉及到AOP如何与一个程序中其余模块交互的问题了。初看起来容易,其实有难度。
好比,DemoAspect虽然是一个类,可是没有构造函数。并且,咱们也没有在代码中主动去构造它。根据AsepctJ的说明,DemoAspect不须要咱们本身去构造,AspectJ在编译的时候会把构造函数给你自动加上。具体在程序什么位置加上,实际上是有规律的,可是咱们并不知道,也不要去知道。
这样的话,DemoAspect岂不是除了打打log就没什么做用了?非也!以此例的权限检查为例,咱们须要:
恩,这实际上是Aspect的真正做用,它负责收集Jpoint,设置advice。一些简单的功能可在Aspect中来完成,而一些复杂的功能,则只是有Aspect来统一收集信息,并交给专业模块来处理。
最终代码:
@Before("checkPermssion(securityCheckAnnotation)") publicvoid check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){ String neededPermission = securityCheckAnnotation.declaredPermission(); Log.e(TAG, "\tneeded permission is " + neededPermission); SecurityCheckManager manager =SecurityCheckManager.getInstanc(); if(manager.checkPermission(neededPermission) == false){ throw new SecurityException("Need to declare permission:" + neededPermission); } return; }
图10所示为最终的执行结果。
图10 执行真正的权限检查
注意,
最后咱们来说讲其余一些内容。首先是AspectJ的编译。
在Android里边,咱们用得是第二种方法,即对class文件进行处理。来看看代码:
//AndroidAopDemo.build.gradle //此处是编译一个App,因此使用的applicationVariants变量,不然使用libraryVariants变量 //这是由Android插件引入的。因此,须要import com.android.build.gradle.AppPlugin; android.applicationVariants.all { variant -> /* 这段代码之意是: 当app编译个每一个variant以后,在javaCompile任务的最后添加一个action。此action 调用ajc函数,对上一步生成的class文件进行aspectj处理。 */ AppPluginplugin = project.plugins.getPlugin(AppPlugin) JavaCompile javaCompile = variant.javaCompile javaCompile.doLast{ String bootclasspath =plugin.project.android.bootClasspath.join(File.pathSeparator) //ajc是一个函数,位于utils.gradle中 ajc(bootclasspath,javaCompile) } }
ajc函数其实和咱们手动试玩aspectj-test目标同样,只是咱们没有直接调用ajc命令,而是利用AspectJ提供的API作了和ajc命令同样的事情。
import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main def ajc(String androidbootClassFiles,JavaCompile javaCompile){ String[] args = ["-showWeaveInfo", "-1.8", //1.8是为了兼容java 8。请根据本身java的版本合理设置它 "-inpath",javaCompile.destinationDir.toString(), "-aspectpath",javaCompile.classpath.asPath, "-d",javaCompile.destinationDir.toString(), "-classpath",javaCompile.classpath.asPath, "-bootclasspath", androidbootClassFiles] MessageHandlerhandler = new MessageHandler(true); new Main().run(args,handler) deflog = project.logger for(IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT: case IMessage.ERROR: case IMessage.FAIL: log.error message.message, message.thrown throw message.thrown break; case IMessage.WARNING: case IMessage.INFO: log.info message.message, message.thrown break; case IMessage.DEBUG: log.debug message.message, message.thrown break; } } }
主要利用了https://eclipse.org/aspectj/doc/released/devguide/ajc-ref.html中TheAspectJ compiler API一节的内容。因为代码已经在csdn git上,你们下载过来直接用便可。
除了hook以外,AspectJ还能够为目标类添加变量。另外,AspectJ也有抽象,继承等各类更高级的玩法。根据本文前面的介绍,这些高级玩法必定要靠需求来驱动。AspectJ确定对原程序是有影响的,如若贸然使用高级用法,则可能带来一些未知的后果。关于这些内容,读者根据状况自行阅读文后所列的参考文献。
最后再来看一个图。
图11 未使用AOP的状况
图11中,左边是一个程序的三个基于OOP而划分的模块(也就是concern)。安全、业务逻辑、交易管理。这三个模块在设计图上必定是互相独立,互不干扰的。
可是在右图实现的时候,这三个模块就搅在一块儿了。这和咱们在AndroidAopDemo中检查权限的例子中彻底同样。在业务逻辑的时候,须要显示调用安全检查模块。
自从有了AOP,咱们就能够去掉业务逻辑中显示调用安全检查的内容,使得代码归于干净,各个模块又能各司其职。而这之中千丝万缕的联系,都由AOP来链接和管理,岂不美哉?!
[1] Manning.AspectJ.in.Action第二版
看书仍是要挑简单易懂的,AOP概念并不复杂,而AspectJ也有不少书,可是真正写得通俗易懂的就是这本,虽然它本意是介绍Spring中的AOP,但对AspectJ的解释真得是很是到位,并且还有对@AspectJ注解的介绍。本文除第一个图外,其余参考用图全是来自于此书。
[2] http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
Android中如何使用AspectJ,最重要的是它教会咱们怎么使用aspectj编译工具API。
[1] 关于Groovy更多的故事,请阅读《深刻理解Android之Gradle》http://blog.csdn.net/innost/article/details/48228651