Kotlin Metadata JVM 函式庫
kotlin-metadata-jvm 函式庫提供了用於讀取、修改及產生為 JVM 編譯的 Kotlin 類別中繼資料的工具。此中繼資料儲存在 .class 檔案內的 @Metadata 註解中,並由 kotlin-reflect 等函式庫和工具用於在執行時檢查 Kotlin 特有的建構,例如屬性、函數和類別。
kotlin-reflect函式庫依賴於中繼資料以在執行時檢索 Kotlin 特有的類別詳細資訊。 中繼資料與實際.class檔案之間任何不一致都可能導致使用反射時出現不正確的行為。
您也可以使用 Kotlin Metadata JVM 函式庫來檢查各種宣告屬性,例如可見性或模組性,或者產生中繼資料並將其嵌入 .class 檔案中。
將函式庫加入您的專案
若要將 Kotlin Metadata JVM 函式庫加入您的專案,請根據您的建構工具新增對應的依賴配置。
Kotlin Metadata JVM 函式庫遵循與 Kotlin 編譯器和標準函式庫相同的版本控制。 請確保您使用的版本與您專案的 Kotlin 版本相符。
Gradle
將以下依賴項加入您的 build.gradle(.kts) 檔案:
// build.gradle.kts
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.2.21")
}// build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-metadata-jvm:2.2.21'
}Maven
將以下依賴項加入您的 pom.xml 檔案。
<project>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>2.2.21</version>
</dependency>
</dependencies>
...
</project>讀取和解析中繼資料
kotlin-metadata-jvm 函式庫從已編譯的 Kotlin .class 檔案中提取結構化資訊,例如類別名稱、可見性和簽章。您可以在需要分析已編譯 Kotlin 宣告的專案中使用它。例如,二進位相容性驗證器 (BCV) 依賴於 kotlin-metadata-jvm 來列印公開 API 宣告。
您可以透過使用反射從已編譯的類別中檢索 @Metadata 註解來開始探索 Kotlin 類別中繼資料:
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()可以處理 Kotlin2.2.x之前的中繼資料;否則,它會拋出錯誤以防止處理未知格式不當。如需更多資訊,請參閱 Kotlin Metadata GitHub 儲存庫。
解析中繼資料時,KotlinClassMetadata 實例會提供有關類別或檔案層級宣告的結構化資訊。對於類別,請使用 kmClass 屬性來分析詳細的類別層級中繼資料,例如類別名稱、函數、屬性以及可見性等屬性。對於檔案層級宣告,中繼資料由 kmPackage 屬性表示,其中包含 Kotlin 編譯器產生之檔案門面中的頂層函數和屬性。
以下程式碼範例演示如何使用 readLenient() 解析中繼資料,使用 kmClass 分析類別層級詳細資訊,以及使用 kmPackage 檢索檔案層級宣告:
// 匯入必要的函式庫
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-metadata-jvm 函式庫來存取它們。這消除了透過簽章匹配註解的需求,使多載宣告的存取更加可靠。
若要使註解在您編譯檔案的中繼資料中可用,請新增以下編譯器選項:
-Xannotations-in-metadata或者,將其加入您的 Gradle 建構檔案的 compilerOptions {} 區塊:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xannotations-in-metadata")
}
}啟用此選項後,Kotlin 編譯器會將註解寫入中繼資料以及 JVM 位元碼中,使 kotlin-metadata-jvm 函式庫可以存取它們。
該函式庫提供了以下用於存取註解的 API:
KmClass.annotationsKmFunction.annotationsKmProperty.annotationsKmConstructor.annotationsKmPropertyAccessorAttributes.annotationsKmValueParameter.annotationsKmFunction.extensionReceiverAnnotationsKmProperty.extensionReceiverAnnotationsKmProperty.backingFieldAnnotationsKmProperty.delegateFieldAnnotationsKmEnumEntry.annotations
這些 API 為 實驗性。若要啟用,請使用 @OptIn(ExperimentalAnnotationsInMetadata::class) 註解。
以下是從 Kotlin 中繼資料讀取註解的範例:
@file:OptIn(ExperimentalAnnotationsInMetadata::class)
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"))]
}如果您在專案中使用
kotlin-metadata-jvm函式庫,我們建議更新並測試您的程式碼以支援註解。 否則,當中繼資料中的註解在未來的 Kotlin 版本中預設啟用時, 您的專案可能會產生無效或不完整的中繼資料。如果您遇到任何問題,請在我們的問題追蹤器中回報。
從位元碼中提取中繼資料
儘管您可以使用反射檢索中繼資料,但另一種方法是使用 ASM 等位元碼操作框架從位元碼中提取中繼資料。
您可以按照以下步驟操作:
- 使用 ASM 函式庫的
ClassReader類別讀取.class檔案的位元碼。 此類別處理已編譯的檔案並填充ClassNode物件,該物件表示類別結構。 - 從
ClassNode物件中提取@Metadata。下面的範例為此使用了自訂擴展函數findAnnotation()。 - 使用
KotlinClassMetadata.readLenient()函數解析提取的中繼資料。 - 使用
kmClass和kmPackage屬性檢查已解析的中繼資料。
以下是範例:
// 匯入必要的函式庫
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)
// 從類別節點中提取 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 函式庫,您可以更新宣告、調整屬性並移除特定元素。
例如,如果您使用一個刪除 Java 類別檔案中私有方法的 JVM 工具,您也必須從 Kotlin 中繼資料中刪除私有函數以維持一致性:
- 透過使用
readStrict()函數將@Metadata註解載入到結構化的KotlinClassMetadata物件中來解析中繼資料。 - 透過直接在
kmClass或其他中繼資料結構內調整中繼資料來應用修改,例如篩選函數或更改屬性。 - 使用
write()函數將修改後的中繼資料編碼成新的@Metadata註解。
以下是一個從類別中繼資料中移除私有函數的範例:
// 匯入必要的函式庫
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 Metadata JVM 函式庫從頭開始為 Kotlin 類別檔案建立中繼資料:
建立
KmClass、KmPackage或KmLambda實例,具體取決於您要產生的中繼資料類型。為該實例新增屬性,例如類別名稱、可見性、建構函數和函數簽章。
您可以使用
apply()作用域函數來減少樣板程式碼,同時設定屬性。使用該實例建立
KotlinClassMetadata物件,該物件可以產生@Metadata註解。指定中繼資料版本,例如
JvmMetadataVersion.LATEST_STABLE_SUPPORTED,並設定旗標(0表示無旗標,或必要時從現有檔案複製旗標)。使用 ASM 中的
ClassWriter類別將中繼資料欄位,例如kind、data1和data2嵌入到.class檔案中。
以下範例演示如何為一個簡單的 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("<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 儲存庫。
