9.4 Kotlin 元数据 JVM 库
原文链接: https://kotlinlang.org/docs/metadata-jvm.html
9.4 Kotlin 元数据 JVM 库
高级
kotlin-metadata-jvm 库提供了各种工具,用于读取、修改和生成已为 JVM 编译的 Kotlin 类的元数据。这些元数据存储在 .class 文件的 @Metadata 注解中,供 kotlin-reflect 等库和工具在运行时检查属性、函数和类等 Kotlin 特有构造。
警告: kotlin-reflect 库依赖元数据来在运行时获取 Kotlin 特有的类细节。元数据与实际 .class 文件之间的任何不一致都可能导致反射行为不正确。
你还可以使用 Kotlin 元数据 JVM 库来检查各种声明属性,例如可见性或修饰性,或者生成元数据并将其嵌入 .class 文件。
把该库添加到你的项目中
要把 Kotlin 元数据 JVM 库包含到项目中,请根据你的构建工具添加相应的依赖配置。
注意: Kotlin 元数据 JVM 库与 Kotlin 编译器和标准库采用相同的版本号。请确保你使用的版本与项目的 Kotlin 版本一致。
Gradle
在 build.gradle(.kts) 文件中添加以下依赖:
Kotlin
1
2
3
4
5
6
7
8
| // build.gradle.kts
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.20")
}
|
Groovy
1
2
3
4
5
6
7
8
| // build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.20'
}
|
Maven
在 pom.xml 文件中添加以下依赖。
1
2
3
4
5
6
7
8
9
10
| <project>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>2.4.20</version>
</dependency>
</dependencies>
...
</project>
|
kotlin-metadata-jvm 库从已编译的 Kotlin .class 文件中提取结构化信息,例如类名、可见性和签名。你可以在需要分析已编译 Kotlin 声明的项目中使用它。例如,二进制兼容性验证器(BCV)就依赖 kotlin-metadata-jvm 来输出公共 API 声明。
你可以先通过反射从已编译的类中获取 @Metadata 注解,开始探索 Kotlin 类的元数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| fun main() {
// 指定类的完全限定名
val clazz = Class.forName("org.example.SampleClass")
// 获取 @Metadata 注解
val metadata = clazz.getAnnotation(Metadata::class.java)
// 检查元数据是否存在
if (metadata != null) {
println("This is a Kotlin class with metadata.")
} else {
println("This is not a Kotlin class.")
}
}
|
获取 @Metadata 注解之后,请使用 KotlinClassMetadata API 中的 readLenient() 或 readStrict() 函数来解析它。这两个函数都会提取有关类或文件的详细信息,但满足不同的兼容性要求:
readLenient():使用该函数可以读取元数据,包括由更新版本 Kotlin 编译器生成的元数据。该函数不支持修改或写入元数据。readStrict():当你需要修改和写入元数据时使用该函数。readStrict() 函数只适用于由你的项目完全支持的 Kotlin 编译器版本所生成的元数据。
注意: readStrict() 函数支持的元数据格式最多比 JvmMetadataVersion.LATEST_STABLE_SUPPORTED 高一个版本,后者对应项目中使用的 Kotlin 最新版本。例如,如果你的项目依赖 kotlin-metadata-jvm:2.1.0,那么 readStrict() 可以处理到 Kotlin 2.2.x 的元数据;否则它会抛出错误,以防错误处理未知格式。更多信息请参阅 Kotlin Metadata GitHub 仓库。
解析元数据时,KotlinClassMetadata 实例会提供有关类或文件级声明的结构化信息。对于类,请使用 kmClass 属性来分析详细的类级元数据,例如类名、函数、属性以及可见性等属性。对于文件级声明,元数据由 kmPackage 属性表示,它包含 Kotlin 编译器生成的文件外观(file facade)中的顶层函数和属性。
下面的代码示例演示了如何使用 readLenient() 解析元数据、用 kmClass 分析类级细节,并用 kmPackage 获取文件级声明:
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // 导入必要的库
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() 函数解析元数据
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()
}
}
|
Kotlin 会把注解同时存储在字节码和 Kotlin 元数据中。如果你使用 kotlin-metadata-jvm 库读取或写入注解,那么你操作的是它们的元数据表示。
注意: 从 Kotlin 2.4.0 开始,Kotlin 会把注解存储在 Kotlin 元数据中。如果你检查用更早版本编译的类文件,这些注解不会出现在元数据中。
当你修改元数据中的注解时,请确保它们与存储在字节码中的注解保持一致。如果二者不同步,依赖反射或字节码分析的工具可能会报告与读取 Kotlin 元数据的工具不同的结果。
kotlin-metadata-jvm 库提供了以下用于访问注解的 API:
KmClass.annotationsKmFunction.annotationsKmProperty.annotationsKmConstructor.annotationsKmPropertyAccessorAttributes.annotationsKmValueParameter.annotationsKmFunction.extensionReceiverAnnotationsKmProperty.extensionReceiverAnnotationsKmProperty.backingFieldAnnotationsKmProperty.delegateFieldAnnotationsKmEnumEntry.annotations
下面是一个从 Kotlin 元数据中读取注解的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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"))]
}
|
除了使用反射获取元数据之外,另一种做法是使用 ASM 等字节码操作框架从字节码中提取元数据。
你可以按以下步骤进行:
- 使用 ASM 库的
ClassReader 类读取 .class 文件的字节码。该类会处理已编译的文件并填充一个表示类结构的 ClassNode 对象。 - 从
ClassNode 对象中提取 @Metadata。下面的示例为此使用了自定义扩展函数 findAnnotation()。 - 使用
KotlinClassMetadata.readLenient() 函数解析提取出的元数据。 - 用
kmClass 和 kmPackage 属性检查解析后的元数据。
下面是一个示例:
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| // 导入必要的库
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)
// 按键获取注解值
private fun List<Any>.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)
// 从 ClassNode 中提取 Kotlin 元数据
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<Int>?)?.toIntArray(),
data1 = (metadataAnnotation["d1"] as List<String>?)?.toTypedArray(),
data2 = (metadataAnnotation["d2"] as List<String>?)?.toTypedArray(),
extraString = metadataAnnotation["xs"] as String?,
packageName = metadataAnnotation["pn"] as String?,
extraInt = metadataAnnotation["xi"] as Int?
)
return KotlinClassMetadata.readLenient(metadata)
}
// 把文件转换为 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) {
// 检查解析后的元数据
val kmClass = metadata.kmClass
// 打印类的详细信息
println("Class name: ${kmClass.name}")
println("Functions:")
kmClass.functions.forEach { function ->
println("- ${function.name}, Visibility: ${function.visibility}")
}
}
}
|
当使用 ProGuard 等工具来缩减和优化字节码时,某些声明可能会从 .class 文件中被移除。ProGuard 会自动更新元数据,使其与修改后的字节码保持一致。
不过,如果你正在开发以类似方式修改 Kotlin 字节码的自定义工具,就需要确保元数据也相应地调整。借助 kotlin-metadata-jvm 库,你可以更新声明、调整属性并移除特定元素。
例如,如果你使用某个 JVM 工具删除 Java 类文件中的私有方法,那么你也必须从 Kotlin 元数据中删除私有函数,以保持一致:
- 使用
readStrict() 函数解析元数据,把 @Metadata 注解加载到结构化的 KotlinClassMetadata 对象中。 - 直接在
kmClass 或其他元数据结构中调整元数据来应用修改,例如过滤函数或更改属性。 - 使用
write() 函数把修改后的元数据编码为新的 @Metadata 注解。
下面是一个从类的元数据中删除私有函数的示例:
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
38
39
40
41
42
43
44
45
| // 导入必要的库
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() 函数解析元数据
val metadata = KotlinClassMetadata.readStrict(metadataAnnotation)
if (metadata is KotlinClassMetadata.Class) {
val kmClass = metadata.kmClass
// 从类元数据中删除私有函数
kmClass.functions.removeIf { it.visibility == Visibility.PRIVATE }
println("Removed private functions. Remaining functions: ${kmClass.functions.map { it.name }}")
// 把修改后的元数据序列化回去
val newMetadata = metadata.write()
// 修改元数据之后,你需要把它写入类文件
// 为此,你可以使用 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()
}
}
|
提示: 你可以使用 transform() 函数,而不必分别调用 readStrict() 和 write()。该函数会解析元数据、通过 lambda 应用转换,并自动写入修改后的元数据。
要使用 Kotlin 元数据 JVM 库为 Kotlin 类文件从零创建元数据:
- 根据你想生成的元数据类型,创建
KmClass、KmPackage 或 KmLambda 的实例。 - 为该实例添加属性,例如类名、可见性、构造器和函数签名。
提示: 在设置属性时,你可以使用 apply() 作用域函数来减少样板代码。
- 使用该实例创建
KotlinClassMetadata 对象,它可以生成 @Metadata 注解。 - 指定元数据版本,例如
JvmMetadataVersion.LATEST_STABLE_SUPPORTED,并设置标志(0 表示无标志,如有必要也可以从现有文件复制标志)。 - 使用 ASM 的
ClassWriter 类把 kind、data1 和 data2 等元数据字段嵌入 .class 文件。
下面的示例演示了如何为一个简单的 Kotlin 类创建元数据:
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // 导入必要的库
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("<init>", "()V")
}
functions += KmFunction("hello").apply {
visibility = Visibility.PUBLIC
returnType = KmType().apply {
classifier = KmClassifier.Class("kotlin/String")
}
signature = JvmMethodSignature("hello", "()Ljava/lang/String;")
}
}
// 把 KotlinClassMetadata.Class 实例(包括版本和标志)序列化为 @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.")
}
|
提示: 更详细的示例请参阅 Kotlin Metadata JVM GitHub 仓库。
接下来做什么?