AOP的概念很久前就接触过了,不过没有真正写过项目,甚至Demo都没有,今天把这点缺陷补上。
推荐两篇文章(本文部分图片引自这两篇文章):
1. 【翻译】Android中的AOP编程
2. 【深入理解Android之AOP】
AOP是Aspect Oriented Program的首字母缩写,译为:面向切面编程。类似的OOP,译为:面向对象编程。
术语名称 | 术语解释 |
---|---|
Cross-cutting concerns(横切关注点) | 多个模块可能添加相同附属功能的点 |
Advice(通知) | 注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。 |
join Point(连接点) | 所有可以注入代码的地方 |
PointCut(切入点) | 告诉AOP框架,我应该在哪个join point注入一段代码 |
Aspect(切面) | 由PointCut和Advice组成的公共逻辑成为切面,切面逻辑只需开发一次,多处调用 |
Weaving(织入) | 注入代码到目标位置 |
统计三个模块耗时。
简写代码如下:
/**
* OOP 登录模块
* Created by Administrator on 2017/10/13.
*/
public class LoginUtils {
private static final String TAG = "OOP";
public static boolean Login(String userName, String passWord){
long start=System.currentTimeMillis();
long end;
StringBuffer stringBuffer = new StringBuffer();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if ("张三".equals(userName) && "123456".equals(passWord)){
end = System.currentTimeMillis();
stringBuffer.append("登录成功,耗时:")
.append(end - start);
Log.e(TAG,stringBuffer.toString());
return true;
}else{
end = System.currentTimeMillis();
stringBuffer.append("登录失败,耗时:")
.append(end - start);
Log.e(TAG,stringBuffer.toString());
return false;
}
}
}
简写代码如下
/**
* OOP 文件上传模块
* Created by Administrator on 2017/10/13.
*/
public class UploadFileUtils {
private static final String TAG = "OOP";
public static boolean upload(String url, String path){
long start=System.currentTimeMillis();
long end;
StringBuffer stringBuffer1 = new StringBuffer();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("从本地上传")
.append(path)
.append("到")
.append(url);
Log.e(TAG,stringBuffer.toString());
end = System.currentTimeMillis();
stringBuffer1.append("文件上传成功,耗时:")
.append(end - start);
Log.e(TAG,stringBuffer1.toString());
return true;
}
}
日志如下:
10-12 13:28:17.300 14605-14605/com.aspectjdemo E/OOP: 登录成功,耗时:2000
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 从111112账户转出100.0到222221
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 转账成功,耗时:2001
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 从本地上传/sd/example.png到www.baidu.com
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 文件上传成功,耗时:2001
按照上面的实现方式,弊端有以下几个:
AspectJ是一个非侵入式的AOP框架,下一章专门介绍。此处只写Android Studio的实现方式,Eclipse实现方式不太一样。
在app的gradle文件中添加如下代码,作用:使用ajc代替javac编译java代码。具体说明见:【翻译】Android中的AOP编程
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
apply plugin: 'com.android.application'
repositories {
mavenCentral()
}
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
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
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
dependencies {
compile 'org.aspectj:aspectjrt:1.8.11'
}
/**
* 自定义注解
* Created by Administrator on 2017/10/13.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeTrace {
String value();
}
拿登录模块来举例,该模块中已经不包含耗时统计的逻辑。
/**
* AOP 登录模块
* Created by Administrator on 2017/10/13.
*/
public class AOPLoginUtils {
private static final String TAG = "OOP";
@TimeTrace(value = "登录")
public static boolean Login(String userName, String passWord){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if ("张三".equals(userName) && "123456".equals(passWord)){
return true;
}else{
return false;
}
}
}
@Aspect
public class TimeTraceAspect {
}
// 语法:execution(@注解 访问权限 返回值的类型 包名.函数名(参数))
// 表示:使用TimeTrace注解的任意类型返回值任意方法名(任意参数)
@Pointcut("execution(@com.aspectjdemo.aop.TimeTrace * *(..))")
public void myPointCut(){
}
具体注入的代码
// Advance比较常用的有:Before():方法执行前,After():方法执行后,Around():代替原有逻辑
@Around("myPointCut()")
public Object dealPoint(ProceedingJoinPoint point) throws Throwable {
// 方法执行前先记录时间
long start=System.currentTimeMillis();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
// 获取注解
TimeTrace annotation = methodSignature.getMethod().getAnnotation(TimeTrace.class);
String value = annotation.value();
// 执行原方法体
Object proceed = point.proceed();
// 方法执行完成后,记录时间,打印日志
long end = System.currentTimeMillis();
StringBuffer stringBuffer = new StringBuffer();
if (proceed instanceof Boolean){
// 返回的是boolean
if ((Boolean)proceed){
stringBuffer.append(value)
.append("成功,耗时:")
.append(end - start);
}else{
stringBuffer.append(value)
.append("失败,耗时:")
.append(end - start);
}
}
Log.e(TAG,stringBuffer.toString());
return proceed;
}
10-12 13:25:44.106 12332-12332/com.aspectjdemo E/AOP: 登录成功,耗时:2001
10-12 13:25:46.136 12332-12332/com.aspectjdemo E/OOP: 从111112账户转出100.0到222221
10-12 13:25:46.137 12332-12332/com.aspectjdemo E/AOP: 转账成功,耗时:2002
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/OOP: 从本地上传/sd/example.png到www.baidu.com
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/AOP: 文件上传成功,耗时:2001
不是本文重点,不深入,其实是我还没了解其他方式(~ ̄▽ ̄)~,稍微罗列一下。
建议这一部分直接去看这个文章,这个文章,这部分很详细,很多语法,各种说明:【深入理解Android之AOP】
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
使用AspectJ有两种方法:
- 完全使用 AspectJ语法(资料比较少,用AS未编译成功,下面不讲)
- 使用java开发,加上AspectJ注解(推荐使用,下面语法主要讲解注解的语法)。
官方网站:
AspectJ官方网站(下载AspectJ的jar包,更新AspectJ的adt等):http://www.eclipse.org/aspectj/
AspectJ类库参考文档:http://www.eclipse.org/aspectj/doc/released/runtime-api/index.html
AspectJ注解文档:http://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/index.html
本节只讲解AspectJ的注解语法
在Aspect的术语章节讲过,join Point(连接点):所有可以注入代码的地方,在AspectJ中是有规定的,只有在下表的几个地方才认为是join Ponit。
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的内容,稍后再说 |
|
在Aspect的术语章节讲过,PointCut(切入点):告诉AOP框架,我应该在哪个join point注入一段代码。那么Pointcuts就是筛选出来的符合条件的所有切入点。
1、 直接选择Join Point
Join Point | Ponitcut语法 | 示例 |
---|---|---|
Method execution | execution(MethodSignature) | 在Activtiy的所有生命周期执行前,注入代码:@Before(“execution(* android.app.Activity.on**(..))”) |
Method call | call(MethodSignature) | 在调用指定方法后,注入代码:@Before(“execution(* android.app.Activity.on**(..))”) |
constructor call | call(ConstructorSignature) | 在调用指定构造方法后,注入代码:@Before(“call(com.aspectjdemo.aopexample.UIUtils.new())”) |
constructor execution | execution(ConstructorSignature) | 在执行指定构造方法后,注入代码:@After(“call(com.aspectjdemo.aopexample.UIUtils.new())”) |
field get | get(FieldSignature) | 在调用指定字段get方法后,注入代码:@After(“get(String com.aspectjdemo.aopexample.AspectJActivity.userName)”) |
field set | set(FieldSignature) | 在调用指定字段get方法前,注入代码:@Before(“set(String com.aspectjdemo.aopexample.AspectJActivity.userName)”) |
Object initialization | initialization(ConstructorSignature) | 在指定的对象初始化后,注入代码:@After(“initialization(com.aspectjdemo.aopexample.UIUtils.new())”) |
MethodSignature匹配规则:
@注解 访问权限 返回值的类型 包名.函数名(参数)
1. @注解和访问权限(public/private/protect,以及static/final)属于可选项。如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、final的函数都会进行搜索。
2. 返回值类型就是普通的函数的返回值类型。如果不限定类型的话,就用*
通配符表示
3. 包名.函数名用于查找匹配的函数。可以使用通配符,包括*
和..以及+号。其中*
号用于匹配除.号之外的任意字符,而..则表示任意子package,+号表示子类。
比如:
java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase,也可以表示TestDervied
java..*:表示java任意子类
java..*Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,TreeModel
等
4. 最后来看函数的参数。参数匹配比较简单,主要是参数类型,比如:
(int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char
(String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限。在参数匹配中,
..代表任意参数个数和类型
(Object …):表示不定个数的参数,且类型都是Object,这里的…不是通配符,而是Java中代表不定参数的意思
ConstructorSignature匹配规则:
Constructorsignature和Method Signature类似,只不过构造函数没有返回值,而且函数名必须叫new。比如:
public *..TestDerived.new(..):
public:选择public访问权限
*..代表任意包名
TestDerived.new:代表TestDerived的构造函数
(..):代表参数个数和类型都是任意
FieldSignature匹配规则:
Field Signature标准格式:
@注解 访问权限 类型 类名.成员变量名
其中,@注解和访问权限是可选的类型:成员变量类型,*
代表任意类型类名.成员变量名:成员变量名可以是*,代表任意成员变量
比如,
set(*.base):表示设置所有包名下base变量时的JPoint
2、 间接选择Join Point
这一类,在demo中没有示例,有兴趣的自己在Demo中添加查看效果。
关键词 |
说明
|
示例 |
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。
|
前面例子中已经用过了,具体看下面表中说明即可。
关键词 |
说明
|
示例 |
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 |
PS:这一节已经很详细的在 3.3.1中写了,还有代码示例,往上看。
路径如下:app->build->intermediates->classes->debug->com包下即是我们使用ajc编译后的class代码。
使用Around处理后,编译出来的class文件
@TimeTrace("登录")
public static boolean Login(String userName, String passWord) {
JoinPoint var5 = Factory.makeJP(ajc$tjp_0, (Object)null, (Object)null, userName, passWord);
TimeTraceAspect var10000 = TimeTraceAspect.aspectOf();
Object[] var6 = new Object[]{userName, passWord, var5};
return Conversions.booleanValue(var10000.dealPoint((new AOPLoginUtils$AjcClosure1(var6)).linkClosureAndJoinPoint(65536)));
}
可以看出来,在这个方法的开头和结尾,都被注入了一些代码,成为我们最终运行到手机上的class文件。
本文只是对AspectJ做了一个入门的介绍,很多高级的用法都未加入进来,在实际项目使用时再进行挖掘吧。
如果有对应的AOP使用场景,建议使用AspectJ,你会感觉到很爽的。