微信号:anzhuobashi

介绍:安卓巴士——最专业的移动开发者门户,提供Android开发学习资料,分享有价值的互联网信息.

Android开发中的面向切面编程AOP

2019-01-28 07:30 慕涵盛华

目录

有什么用

  • 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分类:

类型 描述
Before 前置通知, 在目标执行之前执行通知
After 后置通知, 目标执行后执行通知
Around 环绕通知, 在目标执行中执行通知, 控制目标执行时机
AfterReturning 后置返回通知, 目标返回时执行通知
AfterThrowing 异常通知, 目标抛出异常时执行通知

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切入点指示符如下:

切入点指示符 含义
execution 用于匹配方法执行的连接点
within 用于匹配指定类型内的方法执行
this 用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
target 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
args 用于匹配当前执行的方法传入的参数为指定类型的执行方法
@within 用于匹配所以持有指定注解类型内的方法
@target 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
@args 用于匹配当前执行的方法传入的参数持有指定注解的执行
@annotation 用于匹配当前执行方法持有指定注解的方法

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.`thisas 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,使用代理对象,这样所有用到删除的方法是横向的,组成一个面。





大家都在看


搞事情,自定义LayoutInflate实现酷炫引导页

一个完美支持多进程的组件化方案

Android全新MVVM框架搭建

优秀Android开源库、工具与开源项目整理分享


欢迎前往安卓巴士博客区投稿,技术成长于分享

期待巴友留言,共同探讨学习


 
安卓巴士Android开发者门户 更多文章 Android大厂面试经验分享 Android 沉浸式状态栏 渐变颜色的实现 Android Studio用久了,磁盘越来越不够用? Android原生控件实现2048游戏-Kotlin版 Android 全新MVVM框架搭建
猜您喜欢 一种快速简洁的一致性哈希算法 区块链创新离不开一流的工程技术能力 人民银行成都分行上线支付机构分类评级系统 ❲很有料❳Cgroup - Linux 的 IO 资源隔离 雨燕集