目录
有什么用
- App中很多跳转的地方都需要登入校验,无非就是if-else,但是如果这样的判断有很多,我们就得重复很多次,或者有一天需求变动,有可能就会更改多个地方。类似的还有网络判断,权限管理,Log日志的统一管理这样的问题,如果更优雅的实现这些功能呢?
- App 调试时,如果一眼无法看出错误在哪里,有时会把一些关键信息打印出来,如何快速将方法的入参和出参都打印出来?
- 如何安全地执行方法,不用考虑异常情况?
- …….
什么是AOP?
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP 解决 OOP 中遇到的一些问题.是 对OOP 的延续和扩展
AOP中的术语
Joinpoint(连接点):所谓连接点是指那些被拦截到的点。
Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
Advice(通知/增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类 动态地添加一些方法或 Field。
Target(目标对象):代理的目标对象。
Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程. AspectJ 采用编译期织入和类装在期织入 。
Proxy(代理):一个类被 AOP 织入增强后,就产生一个结果代理类 。
Aspect(切面):是切入点和通知(引介)的结合 。
Advice分类:
Android中如何使用AOP?
AspectJ 介绍
AspectJ是一个面向切面编程的框架。AspectJ是对java的扩展,而且是完全兼容java的,AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。AspectJ还支持原生的Java,只需要加上AspectJ提供的注解即可。在Android开发中,一般就用它提供的注解和一些简单的语法就可以实现绝大部分功能上的需求了。
下面通过一个例子来说明一下AOP中各个术语的含义:
public class UserDao {
public void save(){}
public void find(){}
public void update(){}
public void delete(){}
}
假设UserDao 中的四个方法均已实现,现在需要对delete()方法加入权限校验。那么我们就需要对UserDao 这个类进行增强,那么UserDao这个类就是Target(目标对象),而该类中有四个方法,我们现在只对delete()方法进行改造,所以delete()就是Pointcut(切入点);其他方法都是Joinpoint(连接点);新增的权限校验方法就是Advice(通知);**Introduction(引介)**是对类方面的增强;将通知应用到目标的过程就是 Weaving(织入)
切入点表达式语法
语法如下:
切入点指示符([访问修饰符] 方法的返回值类型 包名.类名.方法名(参数))
AspectJ类型匹配的通配符:
*:匹配任何数量字符;
…:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
AspectJ切入点支持的切入点指示符还有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode。
切入点语法详细说明
Android 中使用Gradle集成 AspectJ
在Android中集成AspectJ,主要思想就是hook Apk打包过程,使用AspectJ提供的工具来编译.class文件。自己手动接入AspectJ的话,比较繁琐。目前有一些在Android中集成AspectJ的比较火的框架,如JakeWharton的 gradle_plugin_android_aspectjx。该框架支持kotlin。这里就使用该框架做演示,不再自己手动接入。
在项目根目录build.gradle下引入aspectjtools插件:
buildscript {
ext.kotlin_version = '1.2.30'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//aspectjtools插件
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.1.1'
classpath 'org.aspectj:aspectjtools:1.8.9'
}
}
在module目录下的build.gradle中引入插件和依赖:(注释部分)
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
//引入aspectj插件
apply plugin: 'com.hujiang.android-aspectjx'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.gfd.aop"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
//引入aspectj的依赖
implementation 'org.aspectj:aspectjrt:1.8.9'
}
至此Aspectj的环境已经搭建好了,下面通过一个登入检查的例子来说明它如果使用。
1.创建Target(目标类)
@Retention(AnnotationRetention.RUNTIME)//存储在编译后的 Class 文件,反射可见。
@Target(AnnotationTarget.FUNCTION)//方法(不包括构造函数)
annotation class CheckLogin
这里创建的是一个注解,所有被该注解标识的方法都是接入点
关于Target和Retention参数的说明:
AnnotationTarget参数说明:
/**
* AnnotationTarget.CLASS:类,接口或对象,注解类也包括在内。
* AnnotationTarget.ANNOTATION_CLASS:只有注解类。
* AnnotationTarget.TYPE_PARAMETER:Generic type parameter (unsupported yet)通用类型参数(还不支持)。
* AnnotationTarget.PROPERTY:属性。
* AnnotationTarget.FIELD:字段,包括属性的支持字段。
* AnnotationTarget.LOCAL_VARIABLE:局部变量。
* AnnotationTarget.VALUE_PARAMETER:函数或构造函数的值参数。
* AnnotationTarget.CONSTRUCTOR:仅构造函数(主函数或者第二函数)。
* AnnotationTarget.FUNCTION:方法(不包括构造函数)。
* AnnotationTarget.PROPERTY_GETTER:只有属性的 getter。
* AnnotationTarget.PROPERTY_SETTER:只有属性的 setter。
* AnnotationTarget.TYPE:类型使用。
* AnnotationTarget.EXPRESSION:任何表达式。
* AnnotationTarget.FILE:文件。
* AnnotationTarget.TYPEALIAS:@SinceKotlin("1.1") 类型别名,Kotlin1.1已可用。
*/
AnnotationRetention参数说明:
/**
* AnnotationRetention.SOURCE:不存储在编译后的 Class 文件。
* AnnotationRetention.BINARY:存储在编译后的 Class 文件,但是反射不可见。
* AnnotationRetention.RUNTIME:存储在编译后的 Class 文件,反射可见。
*/
2.创建切面AspectJ
@Aspect//标识切面
class CheckLoginAspect {
private var isLogin = false
//切入点
@Pointcut("execution(@com.gfd.aop.CheckLogin * *(..))")
fun checkLogin(){
}
@Around("checkLogin()")//环绕通知,先执行通知
@Throws(Throwable::class)//可能抛出的异常
fun aroundJoinPoint(joinPoint: ProceedingJoinPoint){
val methodSignature = joinPoint.signature as MethodSignature
val checkLogin : CheckLogin? = methodSignature.method.getAnnotation(CheckLogin::class.java)
if(checkLogin != null){
val context = joinPoint.`this` as Context
if(isLogin){//如果已经登入再去执行对应的内容
joinPoint.proceed()//执行标注的方法中的内容
}else{
Toast.makeText(context,"请先登入",Toast.LENGTH_SHORT).show()
}
}
}
}
这是方便演示判断登入的方法省略直接用一个变量isLogin 来代替了,只要明白意思即可。
3.创建测试类
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mBtnJust.setOnClickListener {
test()
}
}
@CheckLogin
private fun test() {
Toast.makeText(this@MainActivity,"跳转成功",Toast.LENGTH_SHORT).show()
}
}
test方法被@CheckLogin修饰,会先判断是否登入,如果登入了就会执行text方法中的代码,上面我们模拟的是没有登入isLogin = false,所以程序的运行结果就是提示:请先登入,而不会执行test方法中的代码。这样在需要检测登入的操作方法上添加上@CheckLogin即可实现登入校验的操作,当登入校验的逻辑发生改变的时候,我们也不需要改动调用的地方。
当然我们可以使用通配符“*”对项目中所有的的某个方法进行增强操作。
总结
AOP是对OOP的扩展,OOP强调的是纵向的,而AOP是横向的,假如项目中有很多个删除的方法,现在都需要对删除方法加上校验的操作,一种是:定义一个基类,在基类中实现权限校验的功能,然后去集成它,这样所有用到的地方都得修改,继承就是纵向的;第二种就是利用AOP,使用代理对象,这样所有用到删除的方法是横向的,组成一个面。