一、AOP与OOP的区别
项目中遇到许多地方需要校验登录,会编写许多重复代码,针对这一类问题可以用AOP的思想进行处理,首先了解AOP与OOP的区别。
OOP:
专业术语: OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
面向对象侧重静态,名词,状态,组织,数据,载体是空间;大白话: OOP面向对象的三大特征 : 封装 , 继承 , 多态 。这些特征也说明了OOP是面向对象的,我们做什么都是考虑一个对象,我们需要完成一个任务的时候一般都想着把一些操作封装成一个类,所有的变量和操作都封装到一个类里面,那么这个类就是我们的对象,我们要实现某个特定的功能,首先也想想着在这个对象里面去实现。
面向对象也是有明显缺点的,比如我们想实现某些不是常用的功能,我们需要去在需要的对象中去一一实现这些功能,并我们要不断去维护这些功能,一旦多了我们就会很累的。
比如Android中一些按键统计、生命周期统计,特定统计都是比较琐碎的事情,要利用面向对象的思想去实现都不是很完美,这就要求去一一实现,显得很琐碎。
AOP:
专业术语: AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
大白话: AOP面向切面,我在使用的时候是关注具体的方法和功能切入点,不需要知道也不用关心所在什么类或者是什么对象,我们只关注功能的实现,具体对象是谁不用管
如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。
二 AOP是如何实现的?
AOP的Java实现方式
AOP关注的方法功能点,事先不知道所在对象是谁,当然程序的运行都是需要拿到对象在运行的,要在知道方法功能点的前提下拿到对象并执行,这就需要用到Java的动态代理。
在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),我们可以自己写代码去定义自己的动态代理,去实现AOP,但是太麻烦了。Java有很多AOP实现的库,JavaWeb里面有JBoss、SpringFramework、AspectJ等等。Android中基于AOP实现的库有ButterKnife、dagger2、EventBus3.0、Retrofit 2.0等等,不是说这些库完全基于AOP实现,只是说里面部分功能基于AOP。
Java Annotation
Java Annotation是JDK5.0引入的一种注释机制。
当然我们也可以自定义注解,只是我们自定的注解需要我们自己去反射或者动态代理来处理注解一次来实现AOP,自己处理还是比较麻烦,反射也是比较耗费性能的,不建议使用,最好是Annotation+APT来做,把注解翻译成Java代码,避免性能损耗。
AnnotatedElement
Java的Class类中有一系列支持Annotation和反射的实现,里面有一个很关键的接口:AnnotatedElement,里面是Class反射时对Annotation支持的一系列方法。
基于java的Annotation中的Target和Retention结合类似反射原理我们可以实现自己的AOP。很多Android的AOP实现也是这么做的。
这个接口(AnnotatedElement)的对象代表了在当前JVM中的一个“被注解元素”(可以是Class,Method,Field,Constructor,Package等)。
在Java语言中,所有实现了这个接口的“元素”都是可以“被注解的元素”。使用这个接口中声明的方法可以读取(通过Java的反射机制)“被注解元素”的注解。这个接口中的所有方法返回的注解都是不可变的、并且都是可序列化的。这个接口中所有方法返回的数组可以被调用者修改,而不会影响其返回给其他调用者的数组。
AOP的Android实现方式
Android是用Java写的,上面说到了的上面Java Annotation 动态代理当然都适用于Android,但是Android也有自己的AOP实现方式,但是Android的AOP实现原理跟Java的实现原理是一样的。
Android源码中也有Google官方自定义的AndroidAnnotations
根据apk的打包流程及下图所示,Android中在不同阶段有实现AOP的不同方法
apk的打包流程
- 打包资源文件,生成R.java文件
- 处理aidl文件,生成Java文件
- 将项目源代码编译生成class文件
- 将所有的calss文件,生成classes.dex文件
- 打包生成APK文件
- 对APK进行签名
- 对签名之后的APK文件进行对其处理。
1)APT
APT概述
注解处理工具APT (Annotation Processing Tool ),可以在编译时处理注解。
APT用来在编译时期扫描处理源代码中的注解信息,我们可以根据注解信息生成一些文件,比如Java文件。利用APT为我们生成的Java代码,实现冗余的代码功能,这样就减少手动的代码输入,提升了编码效率,而且使源代码看起来更清晰简洁。
从Java5开始,JDK就自带了注解处理器APT,不过从近几年开始APT才真正的流行起来,这要得益于Android上各种主流库都用了APT来实现,比如Dagger、ButterKnife、AndroidAnnotation、EventBus等。
2)AspectJ
简介
- AspectJ是一个代码生成工具(Code Generator)。
- AspectJ语法就是用来定义代码生成规则的语法。
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件,支持静态编译和动态编译。
- 优点:可以织入所有类;支持编译期和加载时代码注入;编写简单,功能强大。
- 缺点:需要使用ajc编译器编译,ajc编译器是java编译器的扩展,具有其所有功能。
使用场景
- 性能监控: 在方法调用前后记录调用时间,方法执行太长或超时报警。
- 无痕埋点: 在需要埋点的地方添加对应统计代码。
- 缓存代理: 缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
- 记录日志: 在方法执行前后记录系统日志。
- 权限验证: 方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉。
- 其它
AspectJ 语法
这里只是介绍简单的一些概念,如果想要去了解深入的用法,可以查看官网。
1. Aspect(切面)
一个切面是一个独立的功能实现,一个程序可以定义多个切面,定义切面需要新建一个类并加上@Aspect注解。
2. JointPoint(链接点)
链接点代表了程序中可以切入代码的位置,包括函数的调用和执行,类的初始化,异常处理等,链接点就是利用AspectJ可以侵入修改的地方。例如Activity中onCreate方法的调用和执行,Activity的初始化,构造方法的执行等,可以在这些JointPoint(链接点)切入自己想要的代码。
AspectJ 中可以选择的 JointPoint
JoinPoint | 说明 | 示例 |
---|---|---|
method call | 函数调用 | 例如调用 Log.e( ) |
method execution | 函数执行 | 例如 Log.e( ) 的执行内部。 method call 是调用某个函数的地方 execution 是某个函数执行的内部 |
constructor call | 构造函数调用 | 和 method call 类似 |
constructor execution | 构造函数执行 | 和 method execution 类似 |
field get | 获取某个变量 | 例如读取 MainActivity.mTest 成员 |
field set | 设置某个变量 | 例如设置 MainActivity.mTest 成员 |
pre-initialization | Object 在构造函数中做的一些工作 | |
initialization | Object 在构造函数中做的工作 | |
static initialization | 类初始化 | 例如类的 static{} |
handler | 异常处理 | 例如 try catch(xxx) 中,对应 catch 内的执行 |
advice execution | AspectJ 的内容 |
3. PointCut(切点)
切点是具体的链接点,切点定义了需要织入代码的链接点,切点的定义有专门的语法。
告诉代码注入工具,在何处注入一段特定代码的表达式。
execution:处理Join Point的类型,例如call、execution、withincode
其中call、execution类似,都是插入代码的意思,区别就是execution是在被切入的方法中,call是在调用被切入的方法前或者后。
withcode这个语法通常来进行一些切入点条件的过滤,作更加精确的切入控制
MethodPattern
这个是最重要的表达式,大致为:@注解 访问权限 返回值的类型 包名.函数名(参数)
@注解和访问权限(public/private/protect,以及static/final)属于可选项。如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、final的函数都会进行搜索。
**返回值类型:**就是普通的函数的返回值类型。如果不限定类型的话,就用*通配符表示
包名.函数名用于查找匹配的函数。可以使用通配符,包括和…以及+号。其中号用于匹配除.号之外的任意字符,而…则表示任意子package,+号表示子类。
下面我们解析例子中的匹配
* execution(*com.hengda.dsp.aopjavademo.MainActivity.test*(..))
1
第一部分:『』表示返回值,『』表示返回值为任意类型,
第二部分:就是典型的包名路径,其中可以包含『』来进行通配,几个『』没区别。同时,这里可以通过『&&、||、!』来进行条件组合。
类似【test*】的写法,表示以test开头为方法名的任意方法
第三部分:()代表这个方法的参数,你可以指定类型,例如android.os.Bundle,或者(…)这样来代表任意类型、任意个数的参数,也可以混合写法(android.os.Bundle,…)这样来代表第一个参数为bundle,后面随意。
4. Advice(通知)
通知代表对切点的监听,可以在这里编写需要织入的代码。通知包括:@Before方法执行前,@After方法执行后,@Around方法执行前后。例如:下面分别表示了切点activityOnCreate的执行前、执行后、执行前后的监听,其中@Around需要自己处理方法的执行,并且必须放在@Before和@After之前。
Advice就是我们插入的代码以何种方式插入,有Before还有After、Around。
关键词 | 说明 |
---|---|
Before | 切入点之前执行,切入点执行之前我们可以先执行我们的方法,可以拦截切入点的执行,比如拦截需要用户支付或者登陆的操作 |
after | 切入点之后执行 ,比如记录用户行为的操作 |
around | 包含切入点的前后,切入点的执行可以控制。比如需要条件判断再执行,执行完成后给用户提示的操作 |
AfterReturning | 当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。返回通知方法在目标方法执行成功后才会执行,所以,返回通知方法可以拿到目标方法(连接点方法)执行后的结果。 |
AfterThrowing | 异常通知方法只在连接点方法出现异常后才会执行,否则不执行。 |
我们可以用JoinPoint 参数来获取更多的内容:
- java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
- Signature getSignature() :获取连接点的方法签名对象;
- java.lang.Object getTarget() :获取连接点所在的目标对象;
- java.lang.Object getThis() :获取代理对象本身;
3)Javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba
(千叶 滋)所创建的。它已加入了开放源代码JBoss
应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。 – 百度百科
代表框架:热修复框架HotFix 、Savior(InstantRun)等
三、在Android项目中使用AspectJ
在项目的根目录的build.gradle文件中添加依赖
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
然后在项目或者库的build.gradle文件中添加
apply plugin: 'android-aspectjx'
使用
创建一个AspectTest的类
@Aspect
public class AspectTest {
final String TAG = AspectTest.class.getSimpleName();
@Before("execution(* *..MainActivity+.on**(..))")
public void method(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = joinPoint.getThis().getClass().getSimpleName();
Log.e(TAG, "class:" + className);
Log.e(TAG, "method:" + methodSignature.getName());
}
}
MainActivty的代码如下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
log如下
登录校验实现
// 用户登录检测
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {
}
@Aspect // 定义切面类
public class LoginCheckAspect {
private final static String TAG = "TAG";
// 1、应用中用到了哪些注解,放到当前的切入点进行处理(找到需要处理的切入点)
// execution,以方法执行时作为切点,触发Aspect类
// * *(..)) 可以处理ClickBehavior这个类所有的方法
@Pointcut("execution(@com.hengda.dsp.aopjavademo.annotation.LoginCheck * *(..))")
public void methodPointCut() {}
// 2、对切入点如何处理
@Around("methodPointCut()")
public Object jointPotin(ProceedingJoinPoint joinPoint) throws Throwable {
Context context = (Context) joinPoint.getThis();
if (false) { // 从SharedPreferences中读取
Log.e(TAG, "检测到已登录!");
return joinPoint.proceed();
} else {
Log.e(TAG, "检测到未登录!");
Toast.makeText(context, "请先登录!", Toast.LENGTH_SHORT).show();
return null; // 不再执行方法(切入点)
}
}
}
最后在需要校验登录的地方添加注解 @LoginCheck