周末在家正刷着 GitHub 呢,微信收到一条消息:“森哥,像 ksp , allopen 这些 Kotlin 的编译器插件,它们是怎么 run 起来的,看了半天一头雾水”,我心想,不应该呀,十有八九是通过 SPI 来实现插件的加载的“,于是,我赶紧瞅了一眼 JetBrains/Kotlin 的代码,找到了 KotlinGradleSubplugin.kt,于是,假装很懂的样子,发了一个 KotlinGradleSubplugin.kt 的代码截图给他。

“这个我看过了,我想知道 all-open 这个插件究竟是在什么时候修改类的修饰符的”

呃。。。,看来,是编不下去了,只好 cloneJetBrains/Kotlin 的代码下来开始仔细研究。

PluginCliParser

经过一番连蒙带猜,在 PluginCliParsers.kt 中发现了这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object PluginCliParser {

@JvmStatic
fun loadPlugins(pluginClasspaths: Iterable<String>?, pluginOptions: Iterable<String>?, configuration: CompilerConfiguration) {
val classLoader = URLClassLoader(
pluginClasspaths
?.map { File(it).toURI().toURL() }
?.toTypedArray()
?: emptyArray(),
this::class.java.classLoader
)

val componentRegistrars = ServiceLoaderLite.loadImplementations(ComponentRegistrar::class.java, classLoader)
configuration.addAll(ComponentRegistrar.PLUGIN_COMPONENT_REGISTRARS, componentRegistrars)

processPluginOptions(pluginOptions, configuration, classLoader)
}

}

果然不出所料,是通过 SPI 来加载插件的,只不过没有直接用 ServiceLoader ,而是用的 ServiceLoaderLite.kt,来看看它跟 JDK 提供的 ServiceLoader 有什么不一样。

ServiceLoaderLite

代码注释写得很清楚,原来是因为 JDK 8 的 bug — ServiceLoader 文件句柄泄露 🤣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* ServiceLoader has a file handle leak in JDK8: https://bugs.openjdk.java.net/browse/JDK-8156014.
* This class, hopefully, doesn't. :)
*/
object ServiceLoaderLite {
private const val SERVICE_DIRECTORY_LOCATION = "META-INF/services/"

...

fun <Service> loadImplementations(service: Class<out Service>, classLoader: URLClassLoader): List<Service> {
val files = classLoader.urLs.map { url ->
try {
Paths.get(url.toURI()).toFile()
} catch (e: FileSystemNotFoundException) {
throw IllegalArgumentException("Only local URLs are supported, got ${url.protocol}")
} catch (e: UnsupportedOperationException) {
throw IllegalArgumentException("Only local URLs are supported, got ${url.protocol}")
}
}

return loadImplementations(service, files, classLoader)
}

...

private fun findImplementationsInJar(classId: String, file: File): Set<String> {
ZipFile(file).use { zipFile ->
val entry = zipFile.getEntry(SERVICE_DIRECTORY_LOCATION + classId) ?: return emptySet()
zipFile.getInputStream(entry).use { inputStream ->
return inputStream.bufferedReader().useLines { parseLines(file, it) }
}
}
}

....

}

从实现来看,ServiceLoaderLite.kt 是直接从 URLClassLoaderclasspath 来遍历所有的 JAR 文件中的 SPI 配置文件。

Kotlin Compiler 架构

整个 Kotlin 编译器分为 front-endback-endback-end 主要工作是生成平台相关的代码,平台无关的工作基本上都是由 front-end 来完成,其结构如下图所示:

Kotlin 的编译器有三种启动方式:

  1. Kotlin Gradle Plugin
  2. JPS (Jetbrains Project System)JetBrains 基于 Gant 开发的一款构建框架,主要用在 JetBrainsIDEA 全家桶中
  3. kotlinc 命令

Kotlin Gradle Plugin

平常我们使用 Kotlin 基本上都是在 Gradle 环境中使用,而 KotlinGradle 插件启动流程如下图所示:

Kotlin Compiler Plugin

Kotlin 编译器本身提供了一些扩展接口,允许开发者基于 Kotlin 编译器开发一些插件,像官方提供的插件有:

  1. all-open
  2. no-arg
  3. SAM-with-receiver
  4. Parcelable implementations generator

除此之外,还有 Google 推出的 KSP (Kotlin Symbol Processing API)Kotlin 编译器提供的扩展接口有:

  1. KotlinGradleSubplugin

    主要是给 Kotlin Gradle 插件用,因为 Compiler 插件是不依赖于 Gradle 的,所以,需要由 Gradle 插件将 Compiler 插件加载进来,KotlinGradleSubplugin 就是用来配置 Compiler 对应的依赖,以及一些 Compiler 要用到的编译选项。

  2. ComponentRegistrar

    主要是向 Compiler 注册一些 Compiler Extension (不是 Android Gradle Plugin 的那种 Extension),Compiler Extension 既有 front-end 的,也有 back-end 的。

    Front-EndExtension 有:

    1. AnnotationBasedExtension
    2. CollectAdditionalSourcesExtension
    3. CompilerConfigurationExtension
    4. DeclarationAttributeAltererExtension
    5. PreprocessedVirtualFileFactoryExtension

    Back-EndExtension 有:

    1. ClassBuilderInterceptorExtension
    2. ExpressionCodegenExtension
  3. CommandLineProcessor

    主要是用来处理通过命令行传递给插件的参数,格式为:-P plugin:<plugin-id>:<key>=<value>

kotlinc

kotlinc 的大致的启动过程如下图所示,由于过程太过复杂,省略了一些细节,以便于帮忙大家更快的理解 kotlinc 是如何工作的:

结语

了解了 Kotlin 编译器的整体架构,我们就可以基于 Kotlin 编译器来开发自己的插件了,而且,Kotlin 从语法上就具备了语言间的互操作性,加上 Kotlin 编译器的可扩展能力,这给了我们无限的想像空间。