Android 的 AOP 面向切面编程

幻昼 2024年04月29日 29次浏览

AOP 简介

背景

在软件开发中,特别是在面向对象编程中,我们常常会面临一些横切关注点的问题。这些横切关注点可能涉及到日志记录、性能监控、安全检查等,它们通常会分散在应用程序的多个模块中,导致代码重复、耦合性增加等问题。为了解决这些问题,面向切面编程(Aspect-Oriented Programming,AOP)应运而生。

Aspectj

AspectJ 是一个基于 Java 语言的 AOP 框架,它提供了一种简洁的方式来处理横切关注点。通过 AspectJ,我们可以将横切关注点模块化,并且可以在不修改原始代码的情况下,将这些关注点织入到应用程序的特定点上。

AspectJX (不支持 AGP 8)

一个基于AspectJ并在此基础上扩展出来可应用于Android开发平台的AOP框架,可作用于java源码,class文件及jar包,同时支持kotlin的应用。

常规应用场景

AOP 可以应用于许多常见的场景,包括但不限于:

  • 网络检测:在网络请求前后记录日志、监控网络状态。
  • 登录检测:在某些方法执行前检查用户是否已登录,如果未登录则进行相应处理。
  • 多重点击检测:防止用户在短时间内多次点击按钮,导致重复操作。
  • 日志打印:在方法执行前后记录日志,用于调试和性能监控。
  • 权限检查:在方法执行前检查用户是否有权限执行该操作。

使用方法

依赖导入

在项目的根目录下 build.gradle 文件中添加 AspectJ 的依赖:

buildscript {    
    dependencies {
……
        // AOP 配置插件:https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
……
    }
}

在项目的 app 模块目录下 build.gradle 文件中添加 AspectJ 的依赖:

apply plugin: 'android-aspectjx'

android {
    // AOP 配置(exclude 和 include 二选一)
    // 需要进行配置,否则就会引发冲突,具体表现为:
    // 第一种:编译不过去,报错:java.util.zip.ZipException:Cause: zip file is empty
    // 第二种:编译能过去,但运行时报错:ClassNotFoundException: Didn't find class on path: DexPathList
    aspectjx {
        // 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突)
        // exclude 'androidx', 'com.google', 'com.squareup', 'org.apache', 'com.alipay', 'com.taobao', 'versions.9'
        // 只对以下包名做 AOP 处理
        include android.defaultConfig.applicationId
    }
    
    dependencies {
        // AOP 插件库:https://mvnrepository.com/artifact/org.aspectj/aspectjrt
        implementation 'org.aspectj:aspectjrt:1.9.6'
    }
}

定义注解

例如下面的 @Log 注解用于标记需要记录日志的方法,分别在方法执行前后做对应日志处理:

/**
 *    desc   : Debug 日志注解
 */
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER,
    AnnotationTarget.CONSTRUCTOR)
annotation class Log constructor(val value: String = "AppLog")

应用注解

直接在方法、构造方法,getter setter上应用注解即可

class MyClass {
    @Log("MyClass")
    fun myMethod() {
        // Method body
    }
}

处理注解的切面类

编写切面类,处理注解所标记的方法:

/**
 *    desc   : Debug 日志切面
 */
@Aspect
class LogAspect {

    /**
     * 构造方法切入点
     */
    @Pointcut("execution(@top.xlxs.scaffold.aop.Log *.new(..))")
    fun constructor() {}

    /**
     * 方法切入点
     */
    @Pointcut("execution(@top.xlxs.scaffold.aop.Log * *(..))")
    fun method() {}

    /**
     * 在连接点进行方法替换
     */
    @Around("(method() || constructor()) && @annotation(log)")
    @Throws(Throwable::class)
    fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, log: Log): Any? {
        enterMethod(joinPoint, log)
        val startNanos: Long = System.nanoTime()
        val result: Any? = joinPoint.proceed()
        val stopNanos: Long = System.nanoTime()
        exitMethod(joinPoint, log, result, TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos))
        return result
    }

    /**
     * 方法执行前切入
     */
    private fun enterMethod(joinPoint: ProceedingJoinPoint, log: Log) {
        val codeSignature: CodeSignature = joinPoint.signature as CodeSignature

        // 方法所在类
        val className: String = codeSignature.declaringType.name
        // 方法名
        val methodName: String = codeSignature.name
        // 方法参数名集合
        val parameterNames: Array<String?> = codeSignature.parameterNames
        // 方法参数值集合
        val parameterValues: Array<Any?> = joinPoint.args

        //记录并打印方法的信息
        val builder: StringBuilder =
            getMethodLogInfo(className, methodName, parameterNames, parameterValues)
        log(log.value, builder.toString())
        val section: String = builder.substring(2)
        Trace.beginSection(section)
    }

    /**
     * 获取方法的日志信息
     *
     * @param className         类名
     * @param methodName        方法名
     * @param parameterNames    方法参数名集合
     * @param parameterValues   方法参数值集合
     */
    private fun getMethodLogInfo(className: String, methodName: String, parameterNames: Array<String?>, parameterValues: Array<Any?>): StringBuilder {
        val builder: StringBuilder = StringBuilder("\u21E2 ")
        builder.append(className)
            .append(".")
            .append(methodName)
            .append('(')
        for (i in parameterValues.indices) {
            if (i > 0) {
                builder.append(", ")
            }
            builder.append(parameterNames[i]).append('=')
            builder.append(parameterValues[i].toString())
        }
        builder.append(')')
        if (Looper.myLooper() != Looper.getMainLooper()) {
            builder.append(" [Thread:\"").append(Thread.currentThread().name).append("\"]")
        }
        return builder
    }

    /**
     * 方法执行完毕,切出
     *
     * @param result            方法执行后的结果
     * @param lengthMillis      执行方法所需要的时间
     */
    private fun exitMethod(joinPoint: ProceedingJoinPoint, log: Log, result: Any?, lengthMillis: Long) {
        Trace.endSection()
        val signature: Signature = joinPoint.signature
        val className: String? = signature.declaringType.name
        val methodName: String? = signature.name
        val builder: StringBuilder = StringBuilder("\u21E0 ")
            .append(className)
            .append(".")
            .append(methodName)
            .append(" [")
            .append(lengthMillis)
            .append("ms]")

        //  判断方法是否有返回值
        if (signature is MethodSignature && signature.returnType != Void.TYPE) {
            builder.append(" = ")
            builder.append(result.toString())
        }
        log(log.value, builder.toString())
    }

    private fun log(tag: String?, msg: String?) {
        Timber.tag(tag)
        Timber.d(msg)
    }
}

结语

通过 AOP,我们可以更加方便地处理横切关注点,使代码更加清晰、易读,并且提高了代码的复用性和可维护性。在实际项目中,可以根据具体需求,灵活运用 AOP 来解决各种问题,提升软件开发效率和代码质量。