# Kotlin Metadata JVM 库
[kotlin-metadata-jvm](https://github.com/JetBrains/kotlin/tree/master/libraries/kotlinx-metadata/jvm) 库提供了很多工具, 用于对 JVM 编译上的 Kotlin 类, 读取, 修改, 以及生成 metadata. 这些 metadata, 保存在 `.class` 文件内的 [@Metadata](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-metadata/) 注解中, 由 [kotlin-reflect](reflection.html) 等等库和工具使用, 用于在运行期检查 Kotlin 专有的结构, 例如属性, 函数, 以及类.
> **Warning:**
> `kotlin-reflect` 库依赖 metadata, 以便在运行期获取 Kotlin 专有的类详细信息. metadata 与实际的 `.class` 文件之间的任何不一致, 都可能导致在使用反射时出现不正确的行为.
你也可以使用 Kotlin Metadata JVM 库, 检查各种声明属性, 例如可见度或模态(modality), 或者生成 metadata, 并将它嵌入到 `.class` 文件中.
## 向你的项目添加这个库
要在你的项目中包含 Kotlin Metadata JVM 库, 请根据你的构建工具, 添加对应的依赖项配置.
> **Note:**
> Kotlin Metadata JVM 库遵循与 Kotlin 编译器和标准库相同的版本. 请确认你使用的版本与你的项目的 Kotlin 版本相同.
### Gradle
向你的 `build.gradle(.kts)` 文件添加以下依赖项:
Kotlin:
```KOTLIN
// build.gradle.kts
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.20")
}
```
Groovy:
```GROOVY
// build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.20'
}
```
### Maven
向你的 `pom.xml` 文件文件添加以下依赖项.
```XML
org.jetbrains.kotlin
kotlin-metadata-jvm
2.4.20
...
```
## 读取并解析 metadata
`kotlin-metadata-jvm` 库从编译后的 Kotlin `.class` 文件提取结构化信息, 例如类名称, 可见度, 以及签名. 你可以在需要分析编译后的 Kotlin 声明的项目中使用它. 例如, [二进制兼容性验证器(Binary Compatibility Validator, BCV)](https://github.com/Kotlin/binary-compatibility-validator) 依赖 `kotlin-metadata-jvm` 来打印 public API 声明.
你可以从使用反射从编译后的类获取 `@Metadata` 注解开始探索 Kotlin 类的 metadata:
```KOTLIN
fun main() {
// 指定类的完全限定名称
val clazz = Class.forName("org.example.SampleClass")
// 获取 @Metadata 注解
val metadata = clazz.getAnnotation(Metadata::class.java)
// 检查 metadata 是否存在
if (metadata != null) {
println("This is a Kotlin class with metadata.")
} else {
println("This is not a Kotlin class.")
}
}
```
获取 `@Metadata` 注解之后, 请使用 [KotlinClassMetadata](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-kotlin-class-metadata/) API 的 [readLenient()](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-kotlin-class-metadata/-companion/read-lenient.html) 或 [readStrict()](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-kotlin-class-metadata/-companion/read-strict.html) 函数来解析它. 这些函数提取类或文件的详细信息, 同时满足不同的兼容性要求:
* `readLenient()`: 使用这个函数读取 metadata, 包括由比较新的 Kotlin 编译器版本生成的 metadata. 这个函数不支持修改或写入 metadata.
* `readStrict()`: 当你需要修改和写入 metadata 时, 请使用这个函数. `readStrict()` 函数只支持由你的项目完全支持的 Kotlin 编译器版本生成的 metadata. > **Note:** > `readStrict()` 函数支持的 metadata 格式最高是 [JvmMetadataVersion.LATEST_STABLE_SUPPORTED](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-jvm-metadata-version/-companion/-l-a-t-e-s-t_-s-t-a-b-l-e_-s-u-p-p-o-r-t-e-d.html) 之后的一个版本, 这个版本对应于项目中使用的最新 Kotlin 版本. 例如, 如果你的项目依赖于 `kotlin-metadata-jvm:2.1.0`, `readStrict()` 最高能够处理 Kotlin `2.2.x` 的 metadata; 否则, 它会抛出错误, 以防错误处理未知的格式. > > > > 详情请参见 [Kotlin Metadata 的 GitHub 代码仓库](https://github.com/JetBrains/kotlin/blob/master/libraries/kotlinx-metadata/jvm/ReadMe.md#detailed-explanation).
在解析 metadata 时, `KotlinClassMetadata` 实例会提供关于类或文件级声明的结构化信息. 对于类, 请使用 [kmClass](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-kotlin-class-metadata/-class/km-class.html) 属性来分析详细的类级 metadata, 例如类名称, 函数, 属性, 以及特性, 例如可见度. 对于文件级声明, metadata 由 `kmPackage` 属性表示, 其中包含由 Kotlin 编译器生成的文件 facade 中的定层函数和属性.
下面的代码示例演示如何使用 `readLenient()` 来解析 metadata, 使用 `kmClass` 分析类级详细信息, 以及使用 `kmPackage` 获取文件级声明:
```KOTLIN
// 导入需要的库
import kotlin.metadata.jvm.*
import kotlin.metadata.*
fun main() {
// 指定类的完全限定名称
val className = "org.example.SampleClass"
try {
// 获取指定名称的类对象
val clazz = Class.forName(className)
// 获取 @Metadata 注解
val metadataAnnotation = clazz.getAnnotation(Metadata::class.java)
if (metadataAnnotation != null) {
println("Kotlin Metadata found for class: $className")
// 使用 readLenient() 函数解析 metadata
val metadata = KotlinClassMetadata.readLenient(metadataAnnotation)
when (metadata) {
is KotlinClassMetadata.Class -> {
val kmClass = metadata.kmClass
println("Class name: ${kmClass.name}")
// 遍历所有函数, 检查可见度
kmClass.functions.forEach { function ->
val visibility = function.visibility
println("Function: ${function.name}, Visibility: $visibility")
}
}
is KotlinClassMetadata.FileFacade -> {
val kmPackage = metadata.kmPackage
// 遍历所有函数, 检查可见度
kmPackage.functions.forEach { function ->
val visibility = function.visibility
println("Function: ${function.name}, Visibility: $visibility")
}
}
else -> {
println("Unsupported metadata type: $metadata")
}
}
} else {
println("No Kotlin Metadata found for class: $className")
}
} catch (e: ClassNotFoundException) {
println("Class not found: $className")
} catch (e: Exception) {
println("Error processing metadata: ${e.message}")
e.printStackTrace()
}
}
```
### 在 metadata 中写入和读取注解
Kotlin 会将注解同时保存到字节码和 Kotlin metadata 中. 如果你使用 `kotlin-metadata-jvm` 库来读取或写入注解, 你操作的是它们的 metadata 表达.
> **Note:**
> Kotlin 从 2.4.0 版开始将注解保存到 Kotlin metadata 中. 如果你检查使用 Kotlin 更早版本编译的类文件, 注解不会出现在 metadata 中.
如果你修改 metadata 中的注解, 请确保它们与字节码中保存的注解保持一致. 如果这两处的注解没有同步, 依赖于反射或字节码分析的工具, 与读取 Kotlin metadata 的工具, 二者可能会报告不同的结果.
`kotlin-metadata-jvm` 库提供了以下 API 来访问注解:
* `KmClass.annotations`
* `KmFunction.annotations`
* `KmProperty.annotations`
* `KmConstructor.annotations`
* `KmPropertyAccessorAttributes.annotations`
* `KmValueParameter.annotations`
* `KmFunction.extensionReceiverAnnotations`
* `KmProperty.extensionReceiverAnnotations`
* `KmProperty.backingFieldAnnotations`
* `KmProperty.delegateFieldAnnotations`
* `KmEnumEntry.annotations`
下面是一个从 Kotlin metadata 读取注解的示例:
```KOTLIN
import kotlin.metadata.ExperimentalAnnotationsInMetadata
import kotlin.metadata.jvm.KotlinClassMetadata
annotation class Label(val value: String)
@Label("Message class")
class Message
fun main() {
val metadata = Message::class.java.getAnnotation(Metadata::class.java)
val kmClass = (KotlinClassMetadata.readStrict(metadata) as KotlinClassMetadata.Class).kmClass
println(kmClass.annotations)
// 输出结果为: [@Label(value = StringValue("Message class"))]
}
```
### 从字节码提取 metadata
你可以使用反射获取 metadata, 另一种方式是使用字节码操作框架, 例如 [ASM](https://asm.ow2.io/), 从字节码提取 metadata.
你可以通过以下步骤实现:
1. 使用 ASM 库的 `ClassReader` 类, 读取一个 `.class` 文件的字节码. 这个类会处理编译后的文件, 并生成一个 `ClassNode` 对象, 它代表类的结构.
2. 从 `ClassNode` 对象提取 `@Metadata`. 下面的示例使用一个自定义的扩展函数 `findAnnotation()` 来实现.
3. 使用 `KotlinClassMetadata.readLenient()` 函数, 解析提取的 metadata.
4. 使用 `kmClass` 和 `kmPackage` 属性, 检查解析后的 metadata.
示例代码如下:
```KOTLIN
// 导入需要的库
import kotlin.metadata.jvm.*
import kotlin.metadata.*
import org.objectweb.asm.*
import org.objectweb.asm.tree.*
import java.io.File
// 检查一个注解是否引用指定的名称
fun AnnotationNode.refersToName(name: String) =
desc.startsWith('L') && desc.endsWith(';') && desc.regionMatches(1, name, 0, name.length)
// 通过 key 获取注解值
private fun List.annotationValue(key: String): Any? {
for (index in (0 until size / 2)) {
if (this[index * 2] == key) {
return this[index * 2 + 1]
}
}
return null
}
// 自定义扩展函数, 在 ClassNode 中通过注解名称查找注解
fun ClassNode.findAnnotation(annotationName: String, includeInvisible: Boolean = false): AnnotationNode? {
val visible = visibleAnnotations?.firstOrNull { it.refersToName(annotationName) }
if (!includeInvisible) return visible
return visible ?: invisibleAnnotations?.firstOrNull { it.refersToName(annotationName) }
}
// 操作符函数, 用于简化注解值的获取
operator fun AnnotationNode.get(key: String): Any? = values.annotationValue(key)
// 从一个 class node 提取 Kotlin metadata
fun ClassNode.readMetadataLenient(): KotlinClassMetadata? {
val metadataAnnotation = findAnnotation("kotlin/Metadata", false) ?: return null
@Suppress("UNCHECKED_CAST")
val metadata = Metadata(
kind = metadataAnnotation["k"] as Int?,
metadataVersion = (metadataAnnotation["mv"] as List?)?.toIntArray(),
data1 = (metadataAnnotation["d1"] as List?)?.toTypedArray(),
data2 = (metadataAnnotation["d2"] as List?)?.toTypedArray(),
extraString = metadataAnnotation["xs"] as String?,
packageName = metadataAnnotation["pn"] as String?,
extraInt = metadataAnnotation["xi"] as Int?
)
return KotlinClassMetadata.readLenient(metadata)
}
// 将一个文件转换为a ClassNode, 用于检查字节码
fun File.toClassNode(): ClassNode {
val node = ClassNode()
this.inputStream().use { ClassReader(it).accept(node, ClassReader.SKIP_CODE) }
return node
}
fun main() {
val classFilePath = "build/classes/kotlin/main/org/example/SampleClass.class"
val classFile = File(classFilePath)
// 读取字节码, 并将它处理为一个 ClassNode 对象
val classNode = classFile.toClassNode()
// 查找 @Metadata 注解, 并以宽松模式读取它
val metadata = classNode.readMetadataLenient()
if (metadata != null && metadata is KotlinClassMetadata.Class) {
// 检查解析后的 metadata
val kmClass = metadata.kmClass
// 打印输出类的详细信息
println("Class name: ${kmClass.name}")
println("Functions:")
kmClass.functions.forEach { function ->
println("- ${function.name}, Visibility: ${function.visibility}")
}
}
}
```
## 修改 metadata
在使用 [ProGuard](https://github.com/Guardsquare/proguard) 之类的工具缩减和优化字节码时, 有些声明可能会被从 `.class` 文件中删除. ProGuard 会自动更新 metadata, 使它与修改后的字节码保持一致.
但是, 如果你在开发一个自定义工具, 以类似的方式修改 Kotlin 字节码, 你就需要确保 metadata 也进行了相应的调整. 使用 `kotlin-metadata-jvm` 库, 你可以更新声明, 调整特性, 以及删除特定的元素.
例如, 如果你使用一个 JVM tool 从 Java 类文件删除 private 方法, 你还必须从 Kotlin metadata 中删除 private 函数, 以保证一致:
1. 使用 `readStrict()` 函数解析 metadata, 将 `@Metadata` 注解载入到结构化的 `KotlinClassMetadata` 对象中.
2. 调整 metadata , 例如直接在 `kmClass` 或其他 metadata 结构中过滤函数, 或改变特性, 完成修改.
3. 使用 [write()](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-kotlin-class-metadata/write.html) 函数, 将修改后的 metadata 编码为一个新的 `@Metadata` 注解.
下面是一段示例代码, private 函数会从类的 metadata 中删除:
```KOTLIN
// 导入需要的库
import kotlin.metadata.jvm.*
import kotlin.metadata.*
fun main() {
// 指定类的完全限定名称
val className = "org.example.SampleClass"
try {
// 获取指定名称的类对象
val clazz = Class.forName(className)
// 获取 @Metadata 注解
val metadataAnnotation = clazz.getAnnotation(Metadata::class.java)
if (metadataAnnotation != null) {
println("Kotlin Metadata found for class: $className")
// 使用 readStrict() 函数解析 metadata
val metadata = KotlinClassMetadata.readStrict(metadataAnnotation)
if (metadata is KotlinClassMetadata.Class) {
val kmClass = metadata.kmClass
// 从类的 metadata 中删除 private 函数
kmClass.functions.removeIf { it.visibility == Visibility.PRIVATE }
println("Removed private functions. Remaining functions: ${kmClass.functions.map { it.name }}")
// 将修改后的 metadata 序列化回去
val newMetadata = metadata.write()
// 修改 metadata 之后, 你需要将它写回到类文件中
// 你可以使用字节码操作框架来实现, 例如 ASM
println("Modified metadata: ${newMetadata}")
} else {
println("The metadata is not a class.")
}
} else {
println("No Kotlin Metadata found for class: $className")
}
} catch (e: ClassNotFoundException) {
println("Class not found: $className")
} catch (e: Exception) {
println("Error processing metadata: ${e.message}")
e.printStackTrace()
}
}
```
> **Tip:**
> 不需要分别调用 `readStrict()` 和 `write()`, 你可以使用 [transform()](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-kotlin-class-metadata/-companion/transform.html) 函数. 这个函数解析 metadata, 通过 Lambda 表达式完成变换, 并自动将修改后的写回.
## 从头开始创建 metadata
使用 Kotlin Metadata JVM 库, 为一个 Kotlin 类文件从头开始创建 metadata 的步骤如下:
1. 根据你想要生成的 metadata 类型, 创建一个 `KmClass`, `KmPackage`, 或 `KmLambda` 实例.
2. 向实例添加特性, 例如类名称, 可见度, 构造器, 以及函数签名.
> **Tip:**
> 在设置属性时, 你可以使用 [作用域函数](scope-functions.html) `apply()`, 来减少样板代码.
3. 使用实例创建一个 `KotlinClassMetadata` 对象, 它可以生成 `@Metadata` 注解.
4. 指定 metadata 版本, 例如 `JvmMetadataVersion.LATEST_STABLE_SUPPORTED`, 并设置 flag (`0` 表示没有 flag, 或者如果需要, 可以从既有的文件复制 flag).
5. 使用 [ASM](https://asm.ow2.io/) 的 `ClassWriter` 类, 将 metadata field, 例如 `kind`, `data1` 和 `data2` 嵌入到 `.class` 文件中.
下面的示例代码演示如何为一个简单的 Kotlin 类创建 metadata:
```KOTLIN
// 导入需要的库
import kotlin.metadata.*
import kotlin.metadata.jvm.*
import org.objectweb.asm.*
fun main() {
// 创建 KmClass 实例
val klass = KmClass().apply {
name = "Hello"
visibility = Visibility.PUBLIC
constructors += KmConstructor().apply {
visibility = Visibility.PUBLIC
signature = JvmMethodSignature("", "()V")
}
functions += KmFunction("hello").apply {
visibility = Visibility.PUBLIC
returnType = KmType().apply {
classifier = KmClassifier.Class("kotlin/String")
}
signature = JvmMethodSignature("hello", "()Ljava/lang/String;")
}
}
// 将一个 KotlinClassMetadata.Class 实例, 包括版本和 flag, 序列化到 @kotlin.Metadata 注解中
val annotationData = KotlinClassMetadata.Class(
klass, JvmMetadataVersion.LATEST_STABLE_SUPPORTED, 0
).write()
// 使用 ASM 生成 .class 文件
val classBytes = ClassWriter(0).apply {
visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Hello", null, "java/lang/Object", null)
// 将 @kotlin.Metadata 实例写入到 .class 文件
visitAnnotation("Lkotlin/Metadata;", true).apply {
visit("mv", annotationData.metadataVersion)
visit("k", annotationData.kind)
visitArray("d1").apply {
annotationData.data1.forEach { visit(null, it) }
visitEnd()
}
visitArray("d2").apply {
annotationData.data2.forEach { visit(null, it) }
visitEnd()
}
visitEnd()
}
visitEnd()
}.toByteArray()
// 将生成的 .class 文件写入磁盘
java.io.File("Hello.class").writeBytes(classBytes)
println("Metadata and .class file created successfully.")
}
```
> **Tip:**
> 更多详细示例, 请参见 [Kotlin Metadata JVM 的 GitHub 代码仓库](https://github.com/JetBrains/kotlin/blob/50331fb1496378c82c862db04af597e4198ec645/libraries/kotlinx-metadata/jvm/test/kotlin/metadata/test/MetadataSmokeTest.kt#L43).
## 下一步做什么?
* [查看 Kotlin Metadata JVM 库的 API 参考文档](https://kotlinlang.org/api/kotlinx-metadata-jvm/).
* [查看 Kotlin Metadata JVM 的 GitHub 代码仓库](https://github.com/JetBrains/kotlin/tree/master/libraries/kotlinx-metadata/jvm).
* [了解模块的 metadata, 以及如何使用 .kotlin_module 文件](https://github.com/JetBrains/kotlin/blob/master/libraries/kotlinx-metadata/jvm/ReadMe.md#module-metadata).