Kotlin Metadata JVM ライブラリ
kotlin-metadata-jvm ライブラリは、JVM 用にコンパイルされた Kotlin クラスからメタデータを読み取り、変更し、生成するためのツールを提供します。 このメタデータは .class ファイル内の @Metadata アノテーションに保存されており、kotlin-reflect などのライブラリやツールが、実行時にプロパティ、関数、クラスなどの Kotlin 固有の構造を検査するために使用されます。
kotlin-reflectライブラリは、実行時に Kotlin 固有のクラスの詳細を取得するためにメタデータに依存しています。 メタデータと実際の.classファイルの間に不整合があると、リフレクションを使用する際に正しくない動作を引き起こす可能性があります。
また、Kotlin Metadata JVM ライブラリを使用して、可視性(visibility)やモダリティ(modality)などのさまざまな宣言属性を検査したり、メタデータを生成して .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.3.0")
}// build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0'
}Maven
pom.xml ファイルに以下の依存関係を追加します。
<project>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
...
</project>メタデータの読み取りとパース
kotlin-metadata-jvm ライブラリは、コンパイル済みの Kotlin .class ファイルから、クラス名、可視性、シグネチャなどの構造化された情報を抽出します。 コンパイル済みの Kotlin 宣言を分析する必要があるプロジェクトで使用できます。 例えば、Binary Compatibility Validator (BCV) は、公開 API 宣言を出力するために kotlin-metadata-jvm に依存しています。
リフレクションを使用してコンパイル済みクラスから @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()関数は、プロジェクトで使用されている最新の Kotlin バージョンに対応するJvmMetadataVersion.LATEST_STABLE_SUPPORTEDより 1 バージョン先までのメタデータ形式をサポートしています。 例えば、プロジェクトが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 は実験的(Experimental)です。 オプトインするには、@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 バージョンでメタデータ内のアノテーションがデフォルトで有効になったときに、プロジェクトが不正または不完全なメタデータを生成する可能性があります。問題が発生した場合は、課題トラッカー(issue tracker)に報告してください。
バイトコードからのメタデータの抽出
リフレクションを使用してメタデータを取得することもできますが、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 クラスファイルから private メソッドを削除する JVM ツールを使用する場合、一貫性を維持するために Kotlin メタデータからも private 関数を削除する必要があります。
readStrict()関数を使用して@Metadataアノテーションを構造化されたKotlinClassMetadataオブジェクトにロードし、メタデータをパースします。kmClassまたはその他のメタデータ構造内で、関数のフィルタリングや属性の変更などを行って、メタデータを調整し、変更を適用します。write()関数を使用して、変更されたメタデータを新しい@Metadataアノテーションにエンコードします。
以下は、クラスのメタデータから private 関数が削除される例です。
// 必要なライブラリをインポート
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
// クラスメタデータから private 関数を削除
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()
}
}
readStrict()とwrite()を個別に呼び出す代わりに、transform()関数を使用できます。 この関数はメタデータをパースし、ラムダを通じて変換を適用し、変更されたメタデータを自動的に書き込みます。
メタデータの新規作成
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 リポジトリを参照してください。
