概念
AOP(Aspect Oriented Programming)简而言之就是面向切面编程。它所要实现的目标就是解耦,提供代码的灵活性和可扩展性。
与OOP的区别
OOP(Object Oriented Programming)面对对象编程。
这其实是两种不同的设计思想:
- OOP是把同一对象的属性放到一个对象里,把同一类别的类放到一个模块里;这里同一的评判标准可以按照业务来划分,也可以通过行为来划分
- AOP则是提取相同的功能和方法,针对这样的横切面来归类。
用图来弥补下言语的匮乏:
AOP的应用领域
在Android开发过程中,我们时长遇到需要统计事件,性能监测,权限检查等需求。而这些需求是独立业务开发之外的。在业务开发过程中,开发人员不想被这些需求打扰而中断了业务逻辑的梳理。这个时候,就可以用到AOP的思想来解决问题。
- 打印日志:独立的日志模块,在业务开发后期嵌入到各个业务模块中,在代码里不存在日志相关代码。
- 性能监测:时长需要对生命周期函数,view绘制函数监测其运行时长来监测性能。但如果在代码开发阶段考虑就要,实现每个生命周期函数,在绘制view的函数中加入时间统计代码,这样不仅会导致冗余代码还会影响方法本身的性能。
- 权限检查:使方法功能单一,剥离权限检查部分。
其实,总结起来,最终的目的就是为了解耦,尽量将业务无关的,且同一类方法,功能中需要做的重复动作,提取出来。
AspectJ
AOP的实现有很多中,AspectJ只是其中一种,在Java中用得比较多。AspectJ可以说是一种语言,它完全兼容Java,使用原生的Java来开发的话,只需要加上AspectJ的注解就可以。因此两种方式:
- 通过AspectJ的关键字来实现
- 原生java+AspectJ的相关注解来开发
但是无论是通过何种方式实现,其编译都必须要通过AspectJ的编译工具ajc来编译。
AspectJ的语法
在介绍AspectJ的语法之前,先介绍几个概念,也是AspectJ中的关键字,了解他们的含义,对于开发至关重要。
- aspect(切面) 针对切面的模块。也就说独立于业务,需要被插入的部分。
- joinpoint(连接点) 顾名思义就是连接切面模块和业务模块的地方;也可以理解为就是业务模块中需要被嵌入代码的地方。
- pointcut 这个理解起来跟joinpoint应该是一个意思,只不过它可以添加一些附加条件
- advice(处理逻辑) 说逻辑处理有点牵强,它表示的意思应该是被插入的代码,以及插入的时机,如:Before,After,Around等。
常用的切入点
切入点一般是通过joinpoint和advice的组合来实现的,常用的可以看下表:
joinpoint | advice | 切入点 |
---|---|---|
execution | before | 方法执行之前,切入点在方法内 |
execution | after | 方法执行之后,切入点在方法内 |
execution | around | 方法执行前后,可以替换原方法,切入点在方法内 |
call | before | 方法调用之前,切入点在方法外 |
call | after | 方法调用之后,切入点在方法外 |
call | around | 方法调用前后,可以替换原方法,切入点在方法外 |
PS:以上是常用的一些切入点,还有通过cflow来切入每一行字节码。这个控制较难,控制不好会产生StackOverFlow,这个以后再说。
PSS: Advice的各个类型是可以组合使用的,但是切记Around与After是不可以同时使用的,会发生重复调用的问题。
JoinPoint的匹配规则
通过call 和 execution 我们可以知道切入点的时机是在方法调用还是在方法执行。但是如何才能找到方法呢,这就需要一定的匹配规则去找到需要切入的方法。
举个例子:
cn.test.fwl.Test.main() 这样的表达式,可以指定到包名为<cn.test.fwl>,类名为<Test>中无参数的main方法;那么如果我们需要匹配到这个类里所有的main方法,又或者我们需要匹配到这个包里所有类的main方法,再或者我们需要匹配到包含main字符的方法该如何来写表达式呢?
通配符
AspectJ中提供了一些通配符来方便我们找到满足规则的方法。
通配符 | 含义 |
---|---|
* | 匹配除了[.]之外的所有字符,用在路径中表示任意包名字符串,用在类名中标识任意类名字符串,方法中表示任意方法名字符串 |
.. | 表示任意的子package,或者任意的参数 |
+ | 表示子类 |
举例:
- java.*.Date : 可以表示java.sql.Date,也可以表示java.util.Date
- Test* : 表示以Test开头的任意字符串
- java..* : 表示java包中的任意类
- java..*Model+ : 表示java包中以Model结尾的类的子类
- test(..) : 表示方法名为test,任意的参数,没有参数,有一个,两个都可以匹配
- test(int,char) : 表示方法名为test,有且仅有两个参数,类型为int,char
- test(String,..) : 表示方法名为test,至少有一个参数,第一个类型为String,其他任意
- test(String ...) : 表示方法名为test,参数个数不定,但必须都是String类型,这里的[...] 不是通配符,而是java中不定参数的意思。
JoinPoint的约束
除了上面的匹配规则外,AspectJ还提供了一些其他方法来更加精确的选择JoinPoint,比如某个类中的JoinPoint或者某个函数执行流程中的JoinPoint。
关键词 | 说明 | 举个栗子 |
---|---|---|
within(pattern) | pattern可以通过通配符表示,代表某个包或者类 | 满足pattern适配的JoinpPoint。比如说within(Test)就标识在Test类中(包括内部类)所有的JoinPoint。 |
withinCode(Constructor Signature/Method Signature) | 表示某个构造函数或其他函数执行过程中涉及到的 JoinPoint | 比如:withinCode(* Test.testMethod(..))表示testMethod涉及的JoinPoint; withinCode(*.Test.new(..))表示Test构造函数涉及的JoinPoint |
cflow(pointcuts) | cflow的条件是一个pointcut,表示某个流程中涉及的JoinPoint | 比如cflow(call Test.testMethod):表示调用Test.testMethod函数时所包含的JoinPoint,包括testMethod的call这个JoinPoint本身 |
cflowbelow(pointcuts) | 比如:cflowbelow(call Test.testMethod):表示调用Test.testMethod函数时所包含的JoinPont,不包含testMethod的call这个JoinPont本身 | |
this(Type) | JoinPoint的this对象是Type类型。包括其子类 | JPoint所在的这个类的类型是Type标示的类型或是其子类,则和它相关的JPoint将全部被选中。比如:Animal中的Move方法,则Bird,cat中的Move方法都会被选中 |
target(Type) | JoinPoint的target对象是Type类型 | target一般用在call的情况。call一个函数,这个函数可能定义在其他类。比如Bird的move方法在调用时被选中,那么其他的Move的方法则不会。 |
args(Type) | 用来对JoinPoint的参数进行条件约束 | 比如args(int,..),表示第一个参数是int,后面参数个数和类型不限 |
Advice的注意点
关于Advice前面已经说过了,他其实就是被嵌入的部分,而嵌入的时机,也在切入点的表格里提到过。这里主要讲下注意点:
- After:表示函数执行或者调用完成后运行被嵌入的代码部分。但是函数可能执行结束可能有两种退出方式:一个正常的Return,或者抛出异常,因此After也做了区分: after():return(type) 和
after():throwing(Throwable) - Around: 除了之前说Around和After不能同时使用之外,Around因为是可以替代原函数执行的,因此,要特别注意被嵌入的代码的返回值一定要和原来的方法一致。
环境配置
Eclipse
Android现在很少有用Eclipse开发的了,但是Eclipse的插件却是对AspectJ开发支持最友好的。基础的AOP实例,打算用Eclipse来开发AspectJ对Java的横切,因此,这里也介绍下,Eclipse的搭建。
Help -> Install New Software
然后一直下一步就好
Android Studio
首先在工程目录中导入相关的编译工具:
buuildscript{
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
然后在Aspect的module的build.gradle中添加依赖库:
compile 'org.aspectj:aspectjrt:1.8.9'
添加aspect编译的脚本:
def variants = android.libraryVariants
variants.all{ variant ->
LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast{
String[] args = [
"-showWeaveInfo",
"-1.5",
"-inpath",
javaCompile.destinationDir.toString(),
"-aspectpath",
javaCompile.classpath.asPath,
"-d",
javaCompile.destinationDir.toString(),
"-classpath",
javaCompile.classpath.asPath,
"-bootclasspath",
project.android.bootClasspath.join(
File.pathSeparator
)
]
MessageHandler handler = new MessageHandler(true);
new Main().run(args,handler)
def log = project.logger
for(IMessage msg: handler.getMessages(null,true)){
switch(msg.getKind()){
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error msg.message, msg.thrown
break;
case IMessage.WARNING:
log.warn msg.message, msg.thrown
break;
case IMessage.INFO:
log.info msg.message,msg.thrown
break;
case IMessage.DEBUG:
log.debug msg.message,msg.thrown
break;
}
}
}
}
最后在app的module里添加对aspect module的依赖,同时添加上对aspectJ编译的脚本:
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.5",
"-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 msg: handler.getMessages(null,true)){
switch (msg.getKind()){
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error msg.message,msg.thrown
break;
case IMessage.WARNING:
log.warn msg.message, msg.thrown
break;
case IMessage.INFO:
log.info msg.message,msg.thrown
break;
case IMessage.DEBUG:
log.debug msg.message,msg.thrown
break;
}
}
}
}
举个栗子
先通过Eclipse上创建AJ的工程来熟悉下AspectJ的相关语法。
首先,创建一个AspectJ的工程,在已经完成AJDT的插件的前提下,在新建工程的时候,就可以看到可以创建AspectJ Project
这样的工程,如图:
之后创建两个不同的包,来区分java文件和aj文件
创建Test1.java文件
public class Test1 {
public static void main(String[] args) {
test();
}
public static void test(){
System.out.println("this is test method!");
}
}
现在我们要在test()
方法执行打印之前,插入我们的操作(这里也插入一句打印)
注意我们这里创建文件的时候,不再是java文件,而是.aj的文件
创建AspectJ.aj文件
public aspect AspectJ{
public pointcut aspect1(): execution(* test(..));
before():aspect1(){
System.out.println("this is before test method: execution");
}
}
完成这个文件之后,就会发现之前Test1.java
中test()
这个方法里上多了箭头的标志。这就表明插入成功了。
可以运行看下结果:
AspectJ.aj中的注入的打印已经被打印出来了。那么被注入之后的
Test1.class
是样的:
可以看到在打印System.out.println("this is test method!");
之前被插入了一段代码,而这段正是before():aspect1()
方法中所执行的内容。
上面以execution
和before
的组合举了一个简单的例子,主要是阐述了下如何创建Aspecj的工程,以及相应的文件。下面的例子会包含call
,execution
和before
,after
的两两组合。
Test.java
public class Test1 {
public static void main(String[] args) {
testBeforeExecution();
testBeforeCall();
testAfterExecution();
testAfterCall();
testAfterReturn();
testAfterThrowable();
}
public static void testBeforeExecution(){
System.out.println("this is test before-execution!");
}
public static void testBeforeCall(){
System.out.println("this is test before-call");
}
public static void testAfterExecution(){
System.out.println("this is test after-execution");
}
public static void testAfterCall(){
System.out.println("this is test after-call");
}
public static String testAfterReturn(){
String a = "test parameter";
System.out.println("this is test after-return");
return a;
}
public static String testAfterThrowable(){
String a = null;
System.out.println("this is test after-throwable");
a.equals("test");
return a;
}
}
AspectJ.aj
public aspect AspectJ{
public pointcut aspect1(): execution(* testBeforeExecution(..));
public pointcut aspect2(): call(* testBeforeCall(..));
public pointcut aspect3(): execution(* testAfterExecution(..));
public pointcut aspect4(): call(* testAfterCall(..));
public pointcut aspect5(): execution(* testAfterReturn(..));
public pointcut aspect6(): execution(* testAfterThrowable(..));
before():aspect1(){
System.out.println("this is before test : execution");
}
before():aspect2(){
System.out.println("this is before test: call");
}
after():aspect3(){
System.out.println("this is after test:execution");
}
after():aspect4(){
System.out.println("this is after test: call");
}
after() returning(String s):aspect5(){
System.out.println("this is after test : return->"+s);
}
after() throwing(Exception e):aspect6(){
System.out.println("this is after test: throwable->"+e.getMessage());
}
}
可以看到运行结果:
同时也可以看到编译后的class文件:
接下来再举个关于Around的用法的例子:
Test2.java
public class Test2 {
public static void main(String[] args) {
testAroundCall();
testAroundExecution();
testAroundReplace();
System.out.println(testAroundRetrun());
}
public static void testAroundCall(){
System.out.println("this is testAroundCall method");
}
public static void testAroundExecution(){
System.out.println("this is testAroundExecution method");
}
public static void testAroundReplace(){
System.out.println("this is testAroundReplace");
}
public static String testAroundRetrun(){
String a = "the return value";
System.out.println("this is test around return");
return a;
}
}
AspectJ1.aj
public aspect AspectJ1{
public pointcut test1():execution(* testAroundCall(..));
public pointcut test2():call(* testAroundExecution(..));
public pointcut test3():call(* testAroundReplace(..));
public pointcut test4():call(* testAroundRetrun(..));
void around():test1(){
System.out.println("around-execution test before");
proceed();
System.out.println("around-execution test after");
}
void around():test2(){
System.out.println("around-call test before");
proceed();
System.out.println("around-call test after");
}
void around():test3(){
System.out.println("do replace ... ");
}
String around():test4(){
String a = "string in aspect";
System.out.println("replace return value");
proceed();
return a;
}
}
运行结果如下:
class 文件
以上都是AspectJ语言写的,那么如果使用纯Java的方式该如何来实现呢,看下面的例子:
Aspectj2.java
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Aspectj2 {
@Pointcut("execution(* testBeforeExecution(..))")
public void test1(){
}
@Pointcut("call(* testBeforeCall(..))")
public void test2(){
}
@Pointcut("execution(* testAfterExecution(..))")
public void test3(){
}
@Pointcut("call(* testAfterCall(..))")
public void test4(){
}
@Pointcut("execution(* testAfterReturn(..))")
public void test5(){
}
@Pointcut("execution(* testAfterThrowable(..))")
public void test6(){
}
@Before("test1()")
public void execute1(){
System.out.println("before-execution aspectj");
}
@Before("test2()")
public void execute2(){
System.out.println("before-call aspectj");
}
@After("test3()")
public void execute3(){
System.out.println("after-execution aspectj");
}
@After("test4()")
public void execute4(){
System.out.println("after-call aspectj");
}
@AfterReturning("test5()")
public void execute5(){
System.out.println("after-return aspectj");
}
@AfterThrowing("test6()")
public void execute6(){
System.out.println("after-throw aspectj");
}
}
特别提醒下:类的注释@Aspect千万不能少,在这入坑了好几次
运行结果如下:
再看下编译后的文件:
栗子就先吃这么多~~~后面会再补一篇关于带参数,返回值处理的栗子。
AspectJ在Android中的应用
后续会在github上传一个关于权限检查的库,有时间也会写个文档介绍下这个库。
总结
AOP的知识接触得还不多,写了些demo和Android的库,总结下来,重点还是在JoinPoint的适配,如何才能精确得适配到自己想要的切入点,还需要将JoinPoint和Advice结合多加练习。
Eclipse上对的AJDT的插件对Aspect的语法还有错误检查,但是Android Studio上还没有,所以写的时候,要特别仔细。
TODO
- 带参数,返回值的栗子
- 权限检查的工程和分析文档