AspectJ语法
1.Join Points
JPoints就是程序运行时的一些执行点。
最经常使用的是call和execution的执行点。
2.Pointcuts
怎么从一堆一堆的JPoints中选择自己想要的JPoints呢?恩,这就是Pointcuts的功能。一句话,Pointcuts的目标是提供一种方法使得开发者能够选择自己感兴趣的JoinPoints。
一个Pointcuts例子:
public pointcut testAll(): call(public * *.println(..)) && !within(TestAspect) ;
第一个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合起来就是:
l 选择那些调用println(而且不考虑println函数的参数是什么)的Joinpoint。
l 另外,调用者的类型不要是TestAspect的。
3.Jpoints和pointcut之间的联系
①直接针对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中代表不定参数的意思
一个Constructorsignature的完整表达式为:
Constructorsignature和Method Signature类似,只不过构造函数没有返回值,而且函数名必须叫new。比如:
public *..TestDerived.new(..):
Œ public:选择public访问权限
*..代表任意包名
Ž TestDerived.new:代表TestDerived的构造函数
(..):代表参数个数和类型都是任意
再来看Field Signature和Type Signature
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才行。
②间接针对JoinPoint的选择:
注意:this()和target()匹配的时候不能使用通配符。不是所有的AOP实现都支持本节所说的查询条件。比如Spring就不支持withincode查询条件。
4.advice和aspect
①advice
简单点说,advice就是一种Hook。
ASpectJ中有好几个Hook,主要是根据JPoint执行时机的不同而不同,比如下面的:
注意,after和before没有返回值,但是around的目标是替代原JPoint的,所以它一般会有返回值,而且返回值的类型需要匹配被选中的JPoint。
(1)往advice传递参数:
作用:比方所around advice,我可以对传入的参数进行检查,如果参数不合法,我就直接返回,根本就不需要调用proceed做处理。
往advice传参数比较简单,就是利用前面提到的this(),target(),args()等方法。具体方法如下:
Œ 先在pointcuts定义时候指定参数类型和参数名:
pointcut testAll(Test.TestDerived derived,int x):call(*Test.TestDerived.testMethod(..))
&& target(derived)&& args(x)
此处的target和args括号中用得是参数名。而参数名则是在前面pointcuts中定义好的。
或者:
pointcut testAll():call(*Test.TestDerived.testMethod(..)) && target(Test.TestDerived) &&args(int)
(2)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就必须把所有参数传进去。
}
②aspect
上面这些东西都有点像函数定义,在Java中,这些东西都是要放到一个class里的。在AspectJ中,也有类似的数据结构,叫aspect。
aspect的定义:
public aspect 名字 {//aspect关键字和class的功能一样,文件名以.aj结尾
pointcuts定义...
advice定义...
}
你看,通过这种方式,定义一个aspect类,就把相关的JPoint和advice包含起来,是不是形成了一个“关注面”?比如:
l 我们定义一个LogAspect,在LogAspect中,我们在关键JPoint上设置advice,这些advice就是打印日志
l 再定义一个SecurityCheckAspect,在这个Aspect中,我们在关键JPoint上设置advice,这些advice将检查调用app是否有权限。
通过这种方式,我们在原来的JPoint中,就不需要写log打印的代码,也不需要写权限检查的代码了。所有这些关注点都挪到对应的Aspectj文件中来控制。恩,这就是AOP的精髓。
综合前面4点,可以得知,总体的流程是这样的:
5.使用aop的例子
1.打印Log
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(){};
2.检查权限
(1)自定义注解
如果我有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用于定义一个注解。
public String 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;
}
(2)检查权限
/*
来看这个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;
}
恩,这其实是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;
}
6.AspectJ在Android studio中的配置
在Android里边,我们用得是第二种方法,即对class文件进行处理。
配置总结:
①在原工程下新建一个android library类型的moudle,然后添加AspectJ依赖至module中
compile 'org.aspectj:aspectjrt:1.8.9'
②编写moudle的gradle脚本
A。这几个配置要和原有的app下的配置一致:
compile 'com.android.support:appcompat-v7:22.2.1'
compileSdkVersion 22
buildToolsVersion '23.0.1'
B。在build的过程中,会下载jar包,要开启vpn进行下载,下载过程没有进度条提示,只有一个没有标示进度的滚动横条,如果顺利的话会在10分钟之内下载build好这个moudle,接下的app的gradle就好办了。
③编写app的gradle脚本
A。特别要注意这个,里面的名字要和你在①建立的moudle的名字一致
compile project(':aspectjlibrary')
B。这几个配置还是要和原有的app的gradle保持一致:
compileSdkVersion 21
buildToolsVersion '22.0.1'
minSdkVersion 15
targetSdkVersion 21
然后直接build app下的这个gradle就可以了,这个就很快了,20秒之内就可以build完成了。