到達可能性メタデータ
JVMの動的言語機能(リフレクションおよびリソース処理を含む)によって、実行時に呼び出されたメソッドやリソースURLなどの動的にアクセスされるプログラム要素が計算されます。native-image
ツールは、ネイティブ・バイナリをビルドしながら静的分析を実行してそれらの動的機能を判断しますが、すべての使用を常に完全に予測できるわけではありません。これらの要素をネイティブ・バイナリに確実に含めるには、native-image
ビルダーに到達可能性メタデータ(メタデータと呼ばれる追加のテキスト)を提供する必要があります。ビルダーに到達可能性メタデータを提供することで、実行時にサードパーティ・ライブラリとのシームレスな互換性も保証されます。
メタデータは、次の方法でnative-image
ビルダーに提供できます:
- ネイティブ・バイナリのビルド時にコード内のメタデータを計算し、必要な要素をネイティブ・バイナリの初期ヒープに格納する。
META-INF/native-image/<group.id>/<artifact.id>
プロジェクト・ディレクトリに格納されているJSONファイルを指定する。アプリケーションのメタデータを自動的に収集する方法の詳細は、メタデータの自動収集に関する項を参照してください。
目次
- コードでのメタデータの計算
- JSONを使用したメタデータの指定
- メタデータ・タイプ
- リフレクション
- Java Native Interface
- リソースおよびリソース・バンドル
- 動的プロキシ
- シリアライズ
- 事前定義済のクラス
コードでのメタデータの計算
コードでのメタデータの計算は、次の2つの方法で実現できます:
-
JVMの要素に動的にアクセスする関数に定数引数を指定します。このような関数の適切な例は、
Class.forName
メソッドです。次のコードを見てみます:class ReflectiveAccess { public Class<Foo> fetchFoo() throws ClassNotFoundException { return Class.forName("Foo"); } }
ネイティブ・バイナリがビルドされ、その初期ヒープに格納されると、
Class.forName("Foo")
は定数として計算されます。クラスFoo
が存在しない場合、コールはthrow ClassNotFoundException("Foo")
に変換されます。 -
ビルド時にクラスを初期化し、動的にアクセスされる要素をネイティブ実行可能ファイルの初期ヒープに格納します。たとえば:
class InitializedAtBuildTime { private static Class<?> aClass; static { try { aClass = Class.forName(readFile("class.txt")); } catch (ClassNotFoundException e) { throw RuntimeException(e); } } public Class<?> fetchClass() { return aClass; } }
メタデータがコードで計算される場合、動的にアクセスされる要素は、ヒープのその部分が包含メソッド(ReflectiveAccess#fetchFoo
など)または静的フィールド(InitializedAtBuildTime.aClass
など)を介してアクセス可能な場合にのみ、ネイティブ実行可能ファイルのヒープに含まれます。
JSONを使用したメタデータの指定
メタデータを必要とする各動的Java機能には、<feature>-config.JSON
という名前の対応するJSONファイルがあります。JSONファイルは、ネイティブ・イメージに含める要素を通知するエントリで構成されます。たとえば、Javaリフレクション・メタデータはreflect-config.json
で指定され、サンプル・エントリは次のようになります:
{
"name": "Foo"
}
json
ベースのメタデータ内の各エントリは、ネイティブ・バイナリのサイズが不必要に大きくならないように条件付きである必要があります。条件は次の方法で指定します:
{
"condition": {
"typeReachable": "<fully-qualified-class-name>"
},
<metadata-entry>
}
typeReachable
条件を持つエントリは、完全修飾クラスにアクセスできる場合にのみ考慮されます。現在、条件としてtypeReachable
のみがサポートされています。
構成ファイルのその他の例は、GraalVM到達可能性メタデータ・リポジトリを参照してください。
メタデータ・タイプ
ネイティブ・イメージは、次のタイプの到達可能性メタデータを受け入れます:
- Javaリフレクション(
java.lang.reflect.*
API)を使用すると、Javaコードでは、実行時に独自のクラス、メソッド、フィールドおよびそれらのプロパティを調べることができます。 - JNIを使用すると、ネイティブ・コードは実行時にクラス、メソッド、フィールドおよびそれらのプロパティにアクセスできます。
- リソースおよびリソース・バンドルでは、アプリケーションに存在する任意のファイルをロードできます。
- 動的JDKプロキシは、指定されたインタフェースのリストを実装するクラスをオンデマンドで作成します。
- シリアライズでは、ストリームとの間でJavaオブジェクトの書込みおよび読取りが可能です。
- 事前定義済クラスは、動的に生成されたクラスをサポートします。
リフレクション
コードでのリフレクション・メタデータの計算
一部のリフレクション・メソッドは特別に処理され、定数引数を指定するとビルド時に評価されます。リストされているクラスごとに、次のメソッドがあります:
java.lang.Class
:getField
、getMethod
、getConstructor
、getDeclaredField
、getDeclaredMethod
、getDeclaredConstructor
、forName
、getClassLoader
java.lang.invoke.MethodHandles
:publicLookup
、privateLookupIn
、arrayConstructor
、arrayLength
、arrayElementGetter
、arrayElementSetter
、arrayElementVarHandle
、byteArrayViewVarHandle
、byteBufferViewVarHandle
、lookup
java.lang.invoke.MethodHandles.Lookup
:in
、findStatic
、findVirtual
、findConstructor
、findClass
、accessClass
、findSpecial
、findGetter
、findSetter
、findVarHandle
、findStaticGetter
、findStaticSetter
、findStaticVarHandle
、unreflect
、unreflectSpecial
、unreflectConstructor
、unreflectGetter
、unreflectSetter
、unreflectVarHandle
java.lang.invoke.MethodType
:methodType
、genericMethodType
、changeParameterType
、insertParameterTypes
、appendParameterTypes
、replaceParameterTypes
、dropParameterTypes
、changeReturnType
、erase
、generic
、wrap
、unwrap
、parameterType
、parameterCount
、returnType
、lastParameterType
次に、対応するメタデータ要素に置き換えられるコールの例を示します:
Class.forName("java.lang.Integer")
Class.forName("java.lang.Integer", true, ClassLoader.getSystemClassLoader())
Class.forName("java.lang.Integer").getMethod("equals", Object.class)
Integer.class.getDeclaredMethod("bitCount", int.class)
Integer.class.getConstructor(String.class)
Integer.class.getDeclaredConstructor(int.class)
Integer.class.getField("MAX_VALUE")
Integer.class.getDeclaredField("value")
定数配列を渡す場合、配列を宣言および移入する次のアプローチは、native-image
ビルダーの観点からは同等です:
Class<?>[] params0 = new Class<?>[]{String.class, int.class};
Integer.class.getMethod("parseInt", params0);
Class<?>[] params1 = new Class<?>[2];
params1[0] = Class.forName("java.lang.String");
params1[1] = int.class;
Integer.class.getMethod("parseInt", params1);
Class<?>[] params2 = {String.class, int.class};
Integer.class.getMethod("parseInt", params2);
JSONでのリフレクション・メタデータの指定
リフレクション・メタデータは、reflect-config.jsonファイルで指定し、reflect-config-schema-v1.0.0.jsonで定義されているJSONスキーマに準拠する必要があります。スキーマには、この構成がどのように機能するかの詳細および説明も含まれます。reflect-config.jsonの例を次に示します。
[
{
"condition": {
"typeReachable": "<condition-class>"
},
"name": "<class>",
"methods": [
{"name": "<methodName>", "parameterTypes": ["<param-one-type>"]}
],
"queriedMethods": [
{"name": "<methodName>", "parameterTypes": ["<param-one-type>"]}
],
"fields": [
{"name": "<fieldName>"}
],
"allDeclaredClasses": true,
"allDeclaredMethods": true,
"allDeclaredFields": true,
"allDeclaredConstructors": true,
"allPublicClasses": true,
"allPublicMethods": true,
"allPublicFields": true,
"allPublicConstructors": true,
"allRecordComponents": true,
"allNestMembers": true,
"allSigners": true,
"allPermittedSubclasses": true,
"queryAllDeclaredMethods": true,
"queryAllDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllPublicConstructors": true,
"unsafeAllocated": true
}
]
Java Native Interface
Java Native Interface (JNI)を使用すると、ネイティブ・コードは任意のJavaタイプおよびタイプ・メンバーにアクセスできます。ネイティブ・イメージでは、このようなネイティブ・コードのルックアップ、書込みまたは呼出しを予測できません。JNIを使用するJavaアプリケーションのネイティブ・バイナリをビルドするには、JNIメタデータが必要になる可能性があります。たとえば、指定されたC
コードの場合:
jclass clazz = FindClass(env, "java/lang/String");
java.lang.String
クラスをルックアップします。このクラスを使用すると、たとえば、異なるString
メソッドを呼び出すことができます。前述のコールに対して生成されたメタデータ・エントリは次のようになります:
{
"name": "java.lang.String"
}
コードでのJNIメタデータ
コードにJNIメタデータを指定することはできません。
JSONでのJNIメタデータ
JNIメタデータは、jni-config.jsonファイルで指定し、jni-config-schema-v1.0.0.jsonで定義されたJSONスキーマに準拠する必要があります。スキーマには、この構成がどのように機能するかの詳細および説明も含まれます。jni-config.jsonの例は、前述のreflect-config.jsonの例と同じです。
リソースおよびリソース・バンドル
Javaは、アプリケーション・クラスパスの任意のリソース、またはリクエスト元のコードがアクセス権限を持つモジュール・パスにアクセスできます。リソース・メタデータは、指定されたリソースおよびリソース・バンドルを生成されたバイナリに含めるようにnative-image
ビルダーに指示します。このアプローチの結果、構成にリソースを使用するアプリケーションの一部(ロギングなど)は、ビルド時に効果的に構成されます。
コードでのリソース・メタデータ
ネイティブ・イメージは、次のようなjava.lang.Class#getResource
およびjava.lang.Class#getResourceAsStream
へのコールを検出します:
- これらのメソッドがコールされるクラスは定数です
- 最初のパラメータ
name
は定数で、そのようなリソースが自動的に登録されます。
次のコードは、何も設定しなくてもそのまま機能します。理由は次のとおりです:
- クラス・リテラル(
Example.class
)を使用しています name
パラメータとして文字列リテラルを使用していますclass Example { public void conquerTheWorld() { ... InputStream plan = Example.class.getResourceAsStream("plans/v2/conquer_the_world.txt"); ... } }
JSONでのリソース・メタデータ
リソース・メタデータは、resource-config.jsonファイルで指定し、resource-config-schema-v1.0.0.jsonで定義されたJSONスキーマに準拠する必要があります。スキーマには、この構成がどのように機能するかの詳細および説明も含まれます。resource-config.jsonの例を次に示します。
{
"resources": {
"includes": [
{
"condition": {
"typeReachable": "<condition-class>"
},
"pattern": ".*\\.txt"
}
],
"excludes": [
{
"condition": {
"typeReachable": "<condition-class>"
},
"pattern": ".*\\.txt"
}
]
},
"bundles": [
{
"condition": {
"typeReachable": "<condition-class>"
},
"name": "fully.qualified.bundle.name",
"locales": ["en", "de", "sk"]
},
{
"condition": {
"typeReachable": "<condition-class>"
},
"name": "fully.qualified.bundle.name",
"classNames": [
"fully.qualified.bundle.name_en",
"fully.qualified.bundle.name_de"
]
}
]
}
動的プロキシ
JDKでは、特定のインタフェース・リストに対するプロキシ・クラスの生成がサポートされています。ネイティブ・イメージでは、実行時に新しいクラスの生成がサポートされないため、これらのプロキシを使用するコードを適切に実行するにはメタデータが必要です。
ノート: プロキシの作成に使用されるインタフェース・リスト内のインタフェースの順序は重要です。インタフェースの順序が異なる2つの同一のインタフェース・リストを使用してプロキシを作成すると、2つの異なるプロキシ・クラスが作成されます。
コード例
次のコードは、2つの異なるプロキシを作成します:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
interface IA {
}
interface IB {
}
class Example {
public void doWork() {
InvocationHandler handler;
...
Object proxyOne = Proxy.newProxyInstance(Example.class.getClassLoader(), new Class[]{IA.class, IB.class}, handler);
Object proxyTwo = Proxy.newProxyInstance(Example.class.getClassLoader(), new Class[]{IB.class, IA.class}, handler);
...
}
}
コードでの動的プロキシ・メタデータ
次のメソッドは、定数引数を使用してコールされると、ビルド時に評価されます:
java.lang.reflect.Proxy.getProxyClass
java.lang.reflect.Proxy.newProxyInstance
JSONでの動的プロキシ・メタデータ
動的プロキシ・メタデータは、proxy-config.jsonファイルで指定し、proxy-config-schema-v1.0.0.jsonで定義されたJSONスキーマに準拠する必要があります。スキーマには、この構成がどのように機能するかの詳細および説明も含まれます。次に、proxy-config.jsonの例を示します。
[
{
"condition": {
"typeReachable": "<condition-class>"
},
"interfaces": [
"IA",
"IB"
]
}
]
シリアライズ
Javaでは、Serializable
インタフェースを実装する任意のクラスをシリアライズできます。ネイティブ・イメージは、適切なシリアライズ・メタデータ登録によるシリアライズをサポートします。これは、シリアライズでは通常、シリアライズされるオブジェクトのクラスにリフレクティブにアクセスする必要があるため必要です。
コードでのシリアライズ・メタデータの登録
ネイティブ・イメージはObjectInputFilter.Config#createFilter(String pattern)
へのコールを検出し、pattern
引数が定数である場合、パターンで指定されたクラスがシリアライズ用に登録されます。たとえば、次のパターンは、シリアライズ用にクラスpkg.SerializableClass
を登録します:
var filter = ObjectInputFilter.Config.createFilter("pkg.SerializableClass;!*;")
objectInputStream.setObjectInputFilter(proof);
このパターンを使用すると、objectInputStream
で受け取ることができるのはpkg.SerializableClass
のみになるため、JVMのセキュリティが向上するというプラスの効果があります。
ワイルドカード・パターンは、エンクロージング・クラスのラムダ・プロキシ・クラスに対してのみシリアライズ登録を行います。たとえば、エンクロージング・クラスpkg.LambdaHolder
でラムダ・シリアライズを登録するには、次を使用します:
ObjectInputFilter.Config.createFilter("pkg.LambdaHolder$$Lambda*;")
"pkg.**"
や"pkg.Prefix*"
などのパターンは、あまりにも一般的で、イメージ・サイズが大きくなるため、シリアライズ登録は実行されません。
sun.reflect.ReflectionFactory#newConstructorForSerialization(java.lang.Class)
およびsun.reflect.ReflectionFactory#newConstructorForSerialization(java.lang.Class)
のコールの場合、ネイティブ・イメージは、すべての引数とレシーバが定数の場合にこれらの関数のコールを検出します。たとえば、次のコールはシリアライズ用にSerializlableClass
を登録します:
ReflectionFactory.getReflectionFactory().newConstructorForSerialization(SerializableClass.class);
シリアライズ用のカスタム・コンストラクタを作成するには、次を使用します:
var constructor = SuperSuperClass.class.getDeclaredConstructor();
var newConstructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(BaseClass.class, constructor);
プロキシ・クラスは、JSONファイルを介してのみシリアライズに登録できます。
JSONでのシリアライズ・メタデータ
シリアライズ・メタデータは、serialization-config.jsonファイルで指定し、serialization-config-schema-v1.0.0.jsonで定義されたJSONスキーマに準拠する必要があります。スキーマには、この構成がどのように機能するかの詳細および説明も含まれます。次に、serialization-config.jsonの例を示します。
{
"types": [
{
"condition": {
"typeReachable": "<condition-class>"
},
"name": "<fully-qualified-class-name>",
"customTargetConstructorClass": "<custom-target-constructor-class>"
}
],
"lambdaCapturingTypes": [
{
"condition": {
"typeReachable": "<condition-class>"
},
"name": "<fully-qualified-class-name>"
}
],
"proxies": [
{
"condition": {
"typeReachable": "<condition-class>"
},
"interfaces": ["<fully-qualified-interface-name-1>", "<fully-qualified-interface-name-n>"]
}
]
}
事前定義済クラス
ネイティブ・イメージでは、ビルド時にすべてのクラスが認識される必要があります(閉世界仮説)。
ただし、Javaでは、実行時の新しいクラスのロードをサポートしています。クラス・ロードをエミュレートするために、エージェントは、動的にロードされたクラスをトレースし、そのバイトコードを保存して、後でnative-image
ビルダーで使用できるようにします。実行時に、トレース中に検出されたクラスの1つと同じ名前およびバイトコードを持つクラスをロードしようとすると、事前定義済のクラスがアプリケーションに提供されます。
ノート: 事前定義済クラスのメタデータは、手動で記述することを意図していません。
コードでの事前定義済クラス・メタデータ
コードに事前定義済クラスを指定することはできません。
JSONでの事前定義済クラス・メタデータ
事前定義済のクラス・メタデータは、predefined-classes-config.jsonファイルで指定し、predefined-classes-config-schema-v1.0.0.jsonで定義されているJSONスキーマに準拠する必要があります。スキーマには、この構成がどのように機能するかの詳細および説明も含まれます。次に、predefined-classes-config.jsonの例を示します。
[
{
"type": "agent-extracted",
"classes": [
{
"hash": "<class-bytecodes-hash>",
"nameInfo": "<class-name"
}
]
}
]