跳至主要內容

Sword - 为 Kotlin 函数增加代理功能(一)

guodongAndroid大约 5 分钟

Sword - 为 Kotlin 函数增加代理功能(一)

简介

Sword:一个可以给 Kotlin 函数增加代理的第三方库,基于 KCP 实现。

前言

为什么写这个库呢?笔者在学习 Retrofit 时,写过一篇文章 Retrofit学习日记-动态代理open in new window,在这时候笔者就萌生了为函数/方法也增加代理功能的想法。

正好笔者之前就有把某个函数/方法标记为开发调试方法的需求:在开发调试阶段可以执行,正式发布之后就不可执行。要实现这种需求,笔者可以列举下自己想到的方案:

  1. 在函数/方法中通过 BuildConfig#DEBUG 判断是否可以执行:此方案最简单,在编码期依赖开发人员手动实现,但是编码过程比较繁琐且索然无味,
  2. 为函数/方法增加某个注解,通过修改函数/方法的字节码增加判断是否可以执行逻辑:此方案实现起来较为复杂,需要了解一些字节码信息,但是实现后使用起来比较方便且一劳永逸。

仔细思考一下,其实可以发现:为函数/方法增加代理功能 可以实现 把某个函数/方法标记为开发调试方法的需求

经过一番思想斗争,笔者选择了第二种方案,然后结合 为函数/方法也增加代理功能的想法 ,所以最终选型为:注解 + KCP + ASM。

又正好笔者之前写过几篇关于 KCP 的文章:

  1. Kotlin-KCP的应用-第一篇open in new window
  2. Kotlin-KCP的应用-第二篇open in new window
  3. Kotlin-KCP的应用-修改SDK版本号open in new window

在上面的 KCP 文章中,笔者记录了搭建 KCP 开发环境的过程,通过阅读上面的文章可以快速搭建一个 KCP 开发环境。

本文记录下 Sword 的项目结构及前期的开发环境搭建,后续文章再分析 Sword 的核心代码部分,下面让我们先看看项目结构吧。

项目结构

project

  • api-kotlin:API 模块,定义相关的注解 API 类,
  • compiler:ksp 模块,辅助 Sword 生成常量类,后续文章再讲,
  • kcp:kcp 模块,实现 Gradle Plugin 和 Kotlin Compiler Plugin。

API

Proxy

首先创建 API 模块并定义 Proxy 注解:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
annotation class Proxy(
    /**
     * 是否启用, 默认True
     */
    val enable: Boolean = true,

    /**
     * [InvocationHandler]实现类的全限定名, 实现类必须有无参构造方法
     *
     * e.g. com.example.ProxyTestInvocationHandler
     */
    val handler: String = "",
)

Proxy 注解中有两个参数:

  1. enable:表示是否为函数启用代理,默认 True
  2. handler:表示代理函数处理类的全限定名此处理类必须实现 InvocationHandler 接口,且必须有无参构造方法

InvocationHandler

先看下 Java 动态代理接口 java.lang.reflect.InvocationHandler

// java
public interface InvocationHandler {
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

然后我们仿照 Java 的动态代理接口定义我们的接口:

// kt
interface InvocationHandler {

    fun invoke(className: String, methodName: String, args: Array<Any?>): Any?
}

两者是不是比较相似呢?但是后者的接口方法参数有所不同:

  1. className:表示当前代理方法所在的类名,
  2. methodName:表示当前代理方法的名称,
  3. args:表示当前代理方法的参数数组。

在实现方式上也有所不同:

  1. Java 动态代理是在运行时动态创建并加载 Class,然后通过反射调用,有一定的性能开销,
  2. Sword 勉强属于静态代理吧,在编译期修改字节码,没有经过反射调用,基本没有性能开销。

API 相关定义完成,接下来开始编写 KCP 吧。

KCP

对 KCP 不太了解的读者可以先看下上面列举的几篇文章。这里再贴下 KCP 的架构图吧:

kcp-architecture

Gradle Plugin

class SwordGradlePlugin : KotlinCompilerPluginSupportPlugin {

    override fun apply(target: Project) = with(target) {
        logger.error("Welcome to guodongAndroid sword kcp gradle plugin.")
    }

    override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true

    override fun getCompilerPluginId(): String = BuildConfig.KOTLIN_PLUGIN_ID

    override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(
        groupId = BuildConfig.KOTLIN_PLUGIN_GROUP,
        artifactId = BuildConfig.KOTLIN_PLUGIN_NAME,
        version = BuildConfig.KOTLIN_PLUGIN_VERSION,
    )

    override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
        val project = kotlinCompilation.target.project
        return project.provider { emptyList() }
    }
}

对目前的 Sword 来说,它不需要任何参数和配置项。所以 Gradle Plugin 就一个 SwordGradlePlugin 类,比较简单,在其中配置下 PluginId 和 KCP 的 SubpluginArtifact 即可。

Kotlin Compiler Plugin

CommandLineProcessor

@AutoService(CommandLineProcessor::class)
class SwordCommandLineProcessor : CommandLineProcessor {

    override val pluginId: String = BuildConfig.KOTLIN_PLUGIN_ID

    override val pluginOptions: Collection<AbstractCliOption> = emptyList()
}

CommandLineProcessor 也非常简单,只需配置 PluginId,没有任何参数和配置项。

ComponentRegistrar

@AutoService(ComponentRegistrar::class)
class SwordComponentRegistrar : ComponentRegistrar {

    override fun registerProjectComponents(
        project: MockProject,
        configuration: CompilerConfiguration
    ) {
        val messageCollector =
            configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)

        messageCollector.report(
            CompilerMessageSeverity.WARNING,
            "Welcome to guodongAndroid sword kcp kotlin plugin"
        )

        ClassBuilderInterceptorExtension.registerExtension(
            project,
            SwordClassGenerationInterceptor(messageCollector)
        )
    }
}

ComponentRegistrar 的逻辑也很简单:首先获取 MessageCollector 用于日志输出,然后注册一个 ClassBuilder 拦截器用于拦截类的生成。

ClassBuilderInterceptorExtension

class SwordClassGenerationInterceptor(
    private val messageCollector: MessageCollector,
) : ClassBuilderInterceptorExtension {

    override fun interceptClassBuilderFactory(
        interceptedFactory: ClassBuilderFactory,
        bindingContext: BindingContext,
        diagnostics: DiagnosticSink
    ): ClassBuilderFactory = object : ClassBuilderFactory by interceptedFactory {
        override fun newClassBuilder(origin: JvmDeclarationOrigin): ClassBuilder {
            return SwordClassBuilder(messageCollector, interceptedFactory.newClassBuilder(origin))
        }
    }
}

实现 ClassBuilderInterceptorExtension 接口方法 interceptClassBuilderFactory,返回一个 ClassBuilderFactory,在 newClassBuilder 中做拦截,加入自己的处理逻辑,所以接下来的 ClassBuilder 算是 Sword 的核心代码了。

总结

本文只是记录了 Sword 的项目结构及前期的开发环境搭建过程,可以看出 KCP 的开发环境搭建是有迹可循,有模板可依,此次的搭建过程与之前的 Mask 和 修改 SDK 版本号大同小异,笔者后续会提供一个 KCP 开发环境的模板工程供大家参考。

在学习或工作中有好的想法一定要及时记录下来,不要着急去实现你的想法,认真思考几种实现方案,仔细衡量方案中的利弊,选择一个你认为好的方案后再开始做实现。

哦,对了,有想法一定要去实践,不要只是记录下来!

下篇再见,happy~