到達可能性メタデータ

JVMの動的言語機能(リフレクションやリソース処理など)は、フィールド、メソッド、リソースURLなどの_動的にアクセスされるプログラム要素_を実行時に計算します。HotSpotでは、すべてのクラスファイルとリソースが実行時に利用可能であり、ランタイムによってロードできるため、これが可能です。すべてのクラスとリソースの可用性、および実行時のロードには、メモリと起動時間のオーバーヘッドが伴います。

ネイティブバイナリを小さくするために、`native-image`ビルダーはビルド時に静的解析を実行し、アプリケーションの正確性に必要なプログラム要素のみを決定します。小さなバイナリはアプリケーションの高速起動と低メモリフットプリントを可能にしますが、コストがかかります。これらの要素への到達可能性は_実行時にのみ_利用可能なデータに依存するため、静的解析によって動的にアクセスされるアプリケーション要素を決定することは不可能です。

必要な動的にアクセスされる要素をネイティブバイナリに確実に含めるために、`native-image`ビルダーは**到達可能性メタデータ**(以下、_メタデータ_)を必要とします。ビルダーに正確で網羅的な到達可能性メタデータを提供することで、アプリケーションの正確性が保証され、実行時にサードパーティライブラリとの互換性が確保されます。

メタデータは、次の方法で`native-image`ビルダーに提供できます。

注:ネイティブイメージは、問題を早期に示し、簡単なデバッグを可能にする、よりユーザーフレンドリーな到達可能性メタデータの実装に移行しています。

アプリケーションで新しいユーザーフレンドリーな到達可能性メタデータモードを有効にするには、ビルド時に`--exact-reachability-metadata`オプションを渡します。具体的なパッケージに対してのみユーザーフレンドリーモードを有効にするには、`--exact-reachability-metadata=<カンマ区切りのパッケージリスト>`を渡します。

正確な動作にコミットすることなく、コード内の登録漏れが発生しているすべての場所の概要を取得するには、アプリケーションの起動時に`-XX:MissingRegistrationReportingMode=Warn`を渡します。

アプリケーションが誤って登録漏れエラーを無視する場所(`catch (Throwable t)`ブロックを使用)を検出するには、アプリケーションの起動時に`-XX:MissingRegistrationReportingMode=Exit`を渡します。すると、アプリケーションは無条件にスタックトレース付きのエラーメッセージを出力し、すぐに終了します。この動作は、すべてのメタデータが含まれていることを保証するためのアプリケーションテストを実行するのに最適です。

リフレクションのユーザーフレンドリーな実装は、GraalVMの将来のリリースでデフォルトになるため、プロジェクトの破損を避けるためにタイムリーな採用が重要です。

目次 #

コードでのメタデータの計算 #

コードでのメタデータの計算は、2つの方法で実現できます。

  1. JVMの要素に動的にアクセスする関数に_定数_引数を渡すことによって。次のコードの`Class#forName`などを参照してください。

     class ReflectiveAccess {
         public Class<Foo> fetchFoo() throws ClassNotFoundException {
             return Class.forName("Foo");
         }
     }
    

    ここで、`Class.forName("Foo")`はビルド時に定数に評価されます。ネイティブバイナリがビルドされると、この値は初期ヒープに格納されます。クラス`Foo`が存在しない場合、`Class#forName`の呼び出しは`throw ClassNotFoundException("Foo")`に変換されます。

    _定数_は次のように定義されます。

    • リテラル(たとえば、`"Foo"`または`1`)。
    • ビルド時に初期化される静的フィールドへのアクセス。
    • 実質的に最終的な変数へのアクセス。
    • 長さが一定で、すべての値が一定の配列を定義する。
    • 他の定数に対する単純な計算(たとえば、`"F"` + `"oo"`、または配列へのインデックス作成)。

    定数配列を渡す場合、配列を宣言して設定するための次のアプローチは、`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);
    

    ネイティブイメージは現在、積極的に定数を計算しているため、ビルド時に何が定数であるかを正確に指定することはできません。

  2. ビルド時にクラスを初期化し、動的にアクセスされる要素をネイティブ実行可能ファイルの初期ヒープに格納することによって。このメタデータの提供方法は、定数またはJSONでメタデータを指定できない場合に適しています。これは、次の場合に必要です。

    • ユーザーコードが新しいクラスバイトコードを生成する必要がある場合。
    • ユーザーコードがクラスパスをトラバースして、アプリケーションに必要な動的にアクセスされるプログラム要素を計算する必要がある場合。

    次の例では、

     class InitializedAtBuildTime {
         private static Class<?> aClass;
         static {
             try {
                 aClass = Class.forName(readFile("class.txt"));
             } catch (ClassNotFoundException e) {
                 throw RuntimeException(e);
             }
         }
    
         public Class<?> fetchFoo() {
             return aClass;
         }
     }
    

動的にアクセスされる要素は、ヒープのその部分が囲みメソッド(たとえば、`InitializedAtBuildTime#fetchFoo`)または静的フィールド(たとえば、`InitializedAtBuildTime.aClass`)を介して到達可能な場合にのみ、ネイティブ実行可能ファイルのヒープに含まれます。

JSONを使用したメタデータの指定 #

_META-INF/native-image/<groupId>/<artifactId>/_のクラスパスエントリのいずれかに配置されている_reachability-metadata.json_ファイルで指定されたすべてのメタデータ。到達可能性メタデータのJSONスキーマは、reachability-metadata-schema-v1.0.0.jsonで定義されています。

_reachability-metadata.json_ファイルのサンプルは、サンプルセクションにあります。 _reachability-metadata.json_設定には、メタデータのタイプごとに1つのフィールドを持つ単一のオブジェクトが含まれています。トップレベルオブジェクトの各フィールドには、_メタデータエントリ_の配列が含まれています。

{
  "reflection":[],
  "resources":[],
  "bundles":[],
  "serialization":[],
  "jni":[]
}

たとえば、Javaリフレクションメタデータは`reflection`で指定され、エントリの例は次のようになります。

{
  "reflection": [
    {
      "type": "Foo"
    }
  ]
}

条件付きメタデータエントリ #

JSONベースのメタデータの各エントリは、ネイティブバイナリサイズの不要な増加を避けるために_条件付き_にする必要があります。条件付きエントリは、次のようにエントリに`condition`フィールドを追加することによって指定されます。

{
  "condition": {
    "typeReached": "<fully-qualified-class-name>"
  },
  <metadata-entry>
}

`typeReached`条件を持つメタデータエントリは、指定された完全修飾タイプが実行時に_到達_された場合にのみ、実行時に使用可能と見なされます。それ以前は、`metadata-entry`で表される要素へのすべての動的アクセスは、`metadata-entry`が存在しないかのように動作します。つまり、これらの動的アクセスは登録漏れエラーをスローします。

タイプは、そのタイプ(クラスまたはインタフェース)のクラス初期化ルーチンが開始される直前、またはタイプのサブタイプのいずれかに到達する直前に、実行時に到達したと見なされます。次の例でメタデータエントリを保護する`"typeReached": "ConditionType"`の場合、タイプは到達したと見なされます。

class SuperType {
    static {
        // ConditionType reached (subtype reached) => metadata entry available
    }
}
class ConditionType extends SuperType {
    static {
        // ConditionType reached (before static initializer) => metadata entry available
    }
    static ConditionType singleton() {
        // ConditionType reached (already initialized) => metadata entry available
    }
}
public class App {
    public static void main(String[] args) {
        // ConditionType not reached => metadata entry not available
        ConditionType.class;
        // ConditionType not reached (ConditionType.class doesn't start class initialization) => metadata entry not available  
        ConditionType.singleton();
        // ConditionType reached (already initialized) => metadata entry available
    }
}

タイプが`initialize-at-build-time`としてマークされている場合、またはそのサブタイプのいずれかが`initialize-at-build-time`としてマークされていて、クラスパスに存在する場合も、タイプは到達したと見なされます。

配列型は到達済みとしてマークされることはなく、そのため条件で使用できません。

条件付きメタデータエントリは、完全修飾タイプがビルド時に到達可能な場合にイメージに含まれます。このエントリはイメージサイズに影響し、実行時に条件が満たされた場合にのみ使用可能になります。

メタデータファイルのより多くの例は、GraalVM到達可能性メタデータリポジトリにあります。

メタデータタイプ #

ネイティブイメージは、次のタイプの到達可能性メタデータを受け入れます。

  • Javaリフレクション(`java.lang.reflect.*` API)により、Javaコードは実行時に独自のクラス、メソッド、フィールド、およびそれらのプロパティを調べることができます。
  • JNIにより、ネイティブコードは実行時にクラス、メソッド、フィールド、およびそれらのプロパティにアクセスできます。
  • リソースにより、クラスパスに存在する任意のファイルにアプリケーションで動的にアクセスできます。
  • リソースバンドル Javaローカライズサポート(`java.util.ResourceBundle`)により、JavaコードはL10Nリソースをロードできます。
  • シリアライゼーションにより、Javaオブジェクトをストリームに書き込み(および読み取り)できます。
  • (実験的)定義済みクラスは、動的に生成されたクラスのサポートを提供します。

リフレクション #

このセクションのすべてのメソッドについて、ネイティブイメージは、すべての呼び出し引数が定数である場合、ビルド時に到達可能性を計算します。コードで定数引数を指定することは、外部JSONファイルに情報を複製する必要がないため、メタデータを提供するための推奨される方法です。

Javaのリフレクションは、メソッドやフィールドなどのさらに反射的な要素を取得できる`java.lang.Class`から始まります。クラスは、`java.lang.Class`の次の静的関数を使用して反射的に取得できます。

  • java.lang.Class forName(java.lang.String) throws java.lang.ClassNotFoundException
  • java.lang.Class forName(java.lang.String, boolean, java.lang.ClassLoader) throws java.lang.ClassNotFoundException
  • java.lang.Class forName(java.lang.Module, java.lang.String)
  • java.lang.Class arrayType() - 配列型のメタデータが必要です。クラスは、java.lang.ClassLoader#loadClass(String) を使用して名前からクラスをリフレクティブにロードすることでも取得できます。

リフレクティブに Class を取得する呼び出しにメタデータを提供するには、reachability-metadata.jsonreflection 配列に以下のエントリを追加する必要があります。

{
  "type": "FullyQualifiedReflectivelyAccessedType"
}

プロキシクラスの場合、java.lang.Classjava.lang.reflect.Proxy の以下のメソッドを使用して取得されます。

  • java.lang.Class getProxyClass(java.lang.ClassLoader, java.lang.Class[]) throws java.lang.IllegalArgumentException
  • java.lang.Object newProxyInstance(java.lang.ClassLoader, java.lang.Class[], java.lang.reflect.InvocationHandler)

プロキシクラスのメタデータは、プロキシを定義するインターフェースの順序付けられたコレクションの形式です。

{
  "type": {
    "proxy": ["FullyQualifiedInterface1", "...", "FullyQualifiedInterfaceN"]
  }
}

提供されたメタデータなしで上記のメソッドを呼び出すと、java.lang.Error を拡張する MissingReflectionRegistrationError がスローされます。これは処理すべきではありません。クラスパス上に型が存在しない場合でも、上記のメソッドは MissingReflectionRegistrationError をスローすることに注意してください。

指定された型のメタデータが提供されていない場合、java.lang.Class の以下のメソッドは MissingRegistrationError をスローします。

  • Constructor getConstructor(Class[]) throws NoSuchMethodException,SecurityException
  • Constructor getDeclaredConstructor(Class[]) throws NoSuchMethodException,SecurityException
  • Constructor[] getConstructors() throws SecurityException
  • Constructor[] getDeclaredConstructors() throws SecurityException
  • Method getMethod(String,Class[]) throws NoSuchMethodException,SecurityException
  • Method getDeclaredMethod(String,Class[]) throws NoSuchMethodException,SecurityException
  • Method[] getMethods() throws SecurityException
  • Method[] getDeclaredMethods() throws SecurityException
  • Field getField(String) throws NoSuchFieldException,SecurityException
  • Field getDeclaredField(String) throws NoSuchFieldException,SecurityException
  • Field[] getFields() throws SecurityException
  • Field[] getDeclaredFields() throws SecurityException
  • RecordComponent[] getRecordComponents()
  • Class[] getPermittedSubclasses()
  • Object[] getSigners()
  • Class[] getNestMembers()
  • Class[] getClasses()
  • Class[] getDeclaredClasses() throws SecurityException

さらに、java.lang.invoke.MethodHandles.Lookup を介したすべてのリフレクションルックアップも、型のメタデータが存在する必要があります。そうでない場合、MissingReflectionRegistrationError がスローされます。

ラムダプロキシクラスの場合、メタデータを提供できないことに注意してください。これは、GraalVMの将来のリリースで対処される既知の問題です。

リフレクティブなメソッド呼び出し #

メソッドをリフレクティブに呼び出すには、メソッドシグネチャを type メタデータに追加する必要があります。

{
  "type": "TypeWhoseMethodsAreInvoked",
  "methods": [
    {"name": "<methodName1>", "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"]},
    {"name": "<methodName2>", "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"]}
  ]
}

便宜上、reachability-metadata.json に以下を追加することで、メソッドのグループに対してメソッド呼び出しを許可できます。

{
  "type": "TypeWhoseMethodsAreInvoked",
  "allDeclaredConstructors": true,
  "allPublicConstructors": true,
  "allDeclaredMethods": true,
  "allPublicMethods": true
}

allDeclaredConstructors および allDeclaredMethods は、指定された型で宣言されたメソッドの呼び出しを許可します。 allPublicConstructors および allPublicMethods は、型とそのすべてのスーパータイプで定義されたすべてのパブリックメソッドの呼び出しを許可します。

メソッド呼び出しメタデータがない場合、以下のメソッドは MissingReflectionRegistrationError をスローします。

  • java.lang.reflect.Method#invoke(Object, Object...)
  • java.lang.reflect.Constructor#newInstance(Object...)
  • java.lang.invoke.MethodHandle#invokeExact(Object...)
  • java.lang.invoke.MethodHandle#invokeWithArguments (すべてのオーバーロードバージョン)

リフレクティブなフィールド値アクセス #

フィールド値にリフレクティブにアクセス(取得または設定)するには、フィールド名に関するメタデータを型に追加する必要があります。

{
  "type": "TypeWhoseFieldValuesAreAccessed",
  "fields": [{"name": "<fieldName1>"}, {"name": "<fieldNameI>"}, {"name": "<fieldNameN>"}]
}

便宜上、reachability-metadata.json に以下を追加することで、すべてのフィールドのフィールド値アクセスを許可できます。

{
  "type": "TypeWhoseFieldValuesAreAccessed",
  "allDeclaredFields": true,
  "allPublicFields": true
}

allDeclaredFields は、指定された型で宣言されたすべてのフィールドへのアクセスを許可し、allPublicFields は、指定された型とそのすべてのスーパータイプのすべてのパブリックフィールドへのアクセスを許可します。

フィールド値アクセス メタデータがない場合、以下のメソッドは MissingReflectionRegistrationError をスローします。

  • java.lang.reflect.Field#get(Object)
  • java.lang.reflect.Field#set(Object, Object)
  • java.lang.reflect.VarHandle のすべてのアクセサーメソッド。

型の安全でない割り当て #

sun.misc.Unsafe#allocateInstance(Class<?>) を介した型の安全でない割り当て、または AllocObject(jClass) を介したネイティブコードからの安全でない割り当ての場合、以下のメタデータを提供する必要があります。

{
  "type": "FullyQualifiedUnsafeAllocatedType",
  "unsafeAllocated": true
}

そうでない場合、これらのメソッドは MissingReflectionRegistrationError をスローします。

リフレクションメタデータの概要 #

JSON での型の全体的な定義には、以下の値を含めることができます。

{
  "condition": {
    "typeReached": "<condition-class>"
  },
  "type": "<class>|<proxy-interface-list>",
  "fields": [
    {"name": "<fieldName>"}
  ],
  "methods": [
    {"name": "<methodName>", "parameterTypes": ["<param-type>"]}
  ],
  "allDeclaredConstructors": true,
  "allPublicConstructors": true,
  "allDeclaredMethods": true,
  "allPublicMethods": true,
  "allDeclaredFields": true,
  "allPublicFields": true,
  "unsafeAllocated": true
}

Java Native Interface #

Java Native Interface (JNI) を使用すると、ネイティブコードは任意の Java 型と型メンバーにアクセスできます。 Native Image は、そのようなネイティブコードが何をルックアップ、書き込み、または呼び出すかを予測できません。 Java 値にアクセスするために JNI を使用する Java アプリケーションのネイティブバイナリをビルドするには、JNI メタデータが必要です。

たとえば、以下の C コードは、

jclass clazz = FindClass(env, "jni/accessed/Type");

jni.accessed.Type クラスをルックアップします。これは、jni.accessed.Type をインスタンス化したり、そのメソッドを呼び出したり、そのフィールドにアクセスしたりするために使用できます。

上記の呼び出しのメタデータエントリは、reachability-metadata.json を介して*のみ*提供できます。 jni フィールドに type エントリを指定します。

{
  "jni":[
    {
      "type": "jni.accessed.Type"
    }
  ]
}

型のメタデータを追加しても、GetFieldIDGetStaticFieldIDGetStaticMethodID、および GetMethodID を使用してすべてのフィールドとメソッドを取得することはできません。

フィールド値にアクセスするには、フィールド名を提供する必要があります。

{
  "type": "jni.accessed.Type",
  "fields": [{"name": "value"}]
}

すべてのフィールドにアクセスするには、以下の属性を使用できます。

{
  "type": "jni.accessed.Type",
  "allDeclaredFields": true,
  "allPublicFields": true
}

allDeclaredFields は、指定された型で宣言されたすべてのフィールドへのアクセスを許可し、allPublicFields は、指定された型とそのすべてのスーパータイプのすべてのパブリックフィールドへのアクセスを許可します。

JNI から Java メソッドを呼び出すには、メソッドシグネチャのメタデータを提供する必要があります。

{
  "type": "jni.accessed.Type",
  "methods": [
    {"name": "<methodName1>", "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"]},
    {"name": "<methodName2>", "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"]}
  ]
}

便宜上、以下を追加することで、メソッドのグループに対してメソッド呼び出しを許可できます。

{
  "type": "jni.accessed.Type",
  "allDeclaredConstructors": true,
  "allPublicConstructors": true,
  "allDeclaredMethods": true,
  "allPublicMethods": true
}

allDeclaredConstructors および allDeclaredMethods は、指定された型で宣言されたメソッドの呼び出しを許可します。 allPublicConstructors および allPublicMethods は、型とそのすべてのスーパータイプで定義されたすべてのパブリックメソッドの呼び出しを許可します。

AllocObject を使用して型のオブジェクトを割り当てるには、メタデータを reflection セクションに格納する必要があります。

{
  "reflection": [
    {
      "type": "jni.accessed.Type",
      "unsafeAllocated": true
    }
  ]
}

ネイティブコードから動的にアクセスされる要素のメタデータを提供しないと、例外 (MissingJNIRegistrationError) が発生します。

JNI を使用するほとんどのライブラリは例外を正しく処理しないため、不足している要素を確認するには、--exact-reachability-metadata-XX:MissingRegistrationReportingMode=Warn を組み合わせて使用する必要があります。

リソース #

Java は、アプリケーションクラスパス、または要求コードがアクセス許可を持つモジュールパスの任意のリソースにアクセスできます。リソースメタデータは、native-image ビルダに、指定されたリソースとリソースバンドルを生成されるバイナリに含めるように指示します。このアプローチの結果、構成にリソースを使用するアプリケーションの一部(ロギングなど)は、ビルド時に効果的に構成されます。

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 でのリソースメタデータ #

リソースメタデータは、*reachability-metadata.json* ファイルの resources フィールドで指定されます。リソースメタデータの例を以下に示します。

{
  "resources": [
    {
      "glob": "path1/level*/**"
    }
  ]
}

glob フィールドは、リソースを指定するための glob パターン ルールのサブセットを使用します。リソースパスを指定する際に observanceるべきルールがいくつかあります。

  • native-image ツールは、*スター* (*) および *globstar* (**) ワイルドカードパターンのみをサポートします。
    • 定義上、*スター* は 1 つのレベルで任意の数の任意の文字に一致させることができ、*globstar* は任意のレベルで任意の数の文字に一致させることができます。
    • スターを文字通り(特別な意味なしに)扱う必要がある場合は、\ を使用してエスケープできます(たとえば、\*)。
  • glob では、*レベル* は / で区切られたパターンの部分を 나타냅니다。
  • glob パターンを記述する際には、以下のルールに従う必要があります。
    • Glob は空にすることはできません(たとえば、"")。
    • Glob は末尾のスラッシュ (/) で終わることはできません(たとえば、"foo/bar/")。
    • Glob は、1 つのレベルに 2 つ以上の連続した(エスケープされていない)* 文字を含めることはできません(たとえば、"foo/***/")。
    • Glob は空のレベルを含めることはできません(たとえば、"foo//bar")。
    • Glob は 2 つの連続した globstar ワイルドカードを含めることはできません(例:"foo/**/**")。
    • Glob は、globstar ワイルドカードと同じレベルに他のコンテンツを含めることはできません(例:"foo/**bar/x")。

以下のプロジェクト構造が与えられた場合、

app-root
└── src
    └── main
        └── resources
            ├── Resource0.txt
            └── Resource1.txt

以下のことができます。

  • glob **/Resource*.txtですべてのリソースを含めます ({ "glob":})
  • glob **/Resource0.txtで *Resource0.txt* を含めます。
  • glob **/Resource0.txt および **/Resource1.txt で *Resource0.txt* と *Resource1.txt* を含めます。

Java モジュール内のリソース #

すべてのリソースまたはリソースバンドルについて、リソースまたはリソースバンドルを取得するモジュールを指定できます。各エントリの個別の module フィールドにモジュール名を指定できます。例えば、

{
   "resources": [
      {
        "module:": "library.module",
        "glob": "resource-file.txt" 
      }
   ]
}

これにより、native-image ツールは Java モジュール library.module から *resource-file.txt* のみを含めます。パターン *resource-file.txt* に一致するリソースを含む他のモジュールまたはクラスパスがある場合、*library-module* 内のものがネイティブ実行可能ファイルに含めるように登録されます。 Native Image は、モジュールが実行時にアクセス可能であることも保証します。

以下のコードパターンを考えてみましょう。

InputStream resource = ModuleLayer.boot().findModule("library.module").getResourceAsStream(resourcePath);

上記のように登録されたリソースについては、常に期待どおりに動作します(モジュールに静的分析で到達可能と見なされるコードが含まれていない場合でも)。

埋め込みリソース情報 #

ネイティブ実行可能ファイルにどのリソースが含まれていたかを確認するには、2 つの方法があります。

  1. ネイティブ実行ファイルのビルドレポートを生成するには、--emit build-report オプションを使用します。生成されたレポートの Resources タブで、含まれるすべてのリソースに関する情報を確認できます。
  2. -H:+GenerateEmbeddedResourcesFile オプションを使用すると、含まれるすべてのリソースをリストした JSON ファイル *embedded-resources.json* が生成されます。

登録された各リソースについて、以下の情報が得られます。

  • モジュール (リソースがどのモジュールにも属していない場合は unnamed)
  • 名前 (リソースパス)
  • 起点 (システム上のリソースの場所)
  • 種類 (リソースがファイル、ディレクトリ、または欠落しているかどうか)
  • サイズ (実際のリソースサイズ)

注: リソースディレクトリのサイズは、すべてのディレクトリエントリの名前のサイズのみを表します (コンテンツサイズの合計ではありません)。

リソースバンドル #

Java のローカライズサポート (java.util.ResourceBundle) を使用すると、L10N リソースを読み込み、特定の *ロケール* にローカライズされたメッセージを表示できます。 Native Image は、アプリケーションが使用するリソースバンドルを認識して、適切なリソースとプログラム要素をアプリケーションに含める必要があります。

単純なバンドルは、*reachability-metadata.json* の bundles セクションで指定できます。

{
  "bundles": [
    {
      "name":"your.pkg.Bundle"
    }
  ]
}

特定のモジュールからバンドルを要求するには、以下のようにします。

{
  "bundles": [
    {
      "name":"app.module:module.pkg.Bundle"
    }
  ]
}

デフォルトでは、イメージに含まれるすべてのロケールについてリソースバンドルが含まれます。以下は、バンドルに特定のロケールのみを含める方法の例です。

{
  "bundles": [
    {
      "name": "specific.locales.Bundle",
      "locales": ["en", "de", "sk"]
    }
  ]
}

ロケール #

ネイティブ実行ファイルに含めるロケールとデフォルトのロケールを指定することもできます。たとえば、デフォルトのロケールをスイスドイツ語に切り替え、フランス語と英語も含めるには、次のオプションを使用します。

native-image -Duser.country=CH -Duser.language=de -H:IncludeLocales=fr,en

ロケールは、言語タグを使用して指定します。 -H:+IncludeAllLocales を使用してすべてのロケールを含めることができますが、結果の実行ファイルのサイズが大きくなることに注意してください。

シリアライゼーション #

Java は、Serializable インターフェースを実装する任意のクラスをシリアライズ (またはデシリアライズ) できます。 Native Image は、適切なシリアライゼーションメタデータの登録により、シリアライゼーション (またはデシリアライズ) をサポートします。これは、シリアライゼーションでは通常、シリアライズされるオブジェクトへのリフレクティブアクセスが必要になるためです。

コードでのシリアライゼーションメタデータの登録 #

Native Image は、ObjectInputFilter.Config#createFilter(String pattern) への呼び出しを検出し、pattern 引数が定数の場合、パターンで指定されたクラスがシリアライゼーション用に登録されます。たとえば、次のパターンは、クラス pkg.SerializableClass をシリアライゼーション用に登録します。

  var filter = ObjectInputFilter.Config.createFilter("pkg.SerializableClass;!*;")
  objectInputStream.setObjectInputFilter(proof);

このパターンを使用すると、objectInputStreampkg.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 でのシリアライゼーションメタデータ #

シリアライゼーションメタデータは、*reachability-metadata.json* の serialization セクションで指定します。

通常の serialized.Type を指定するには、以下を使用します。

{
  "serialization": [
    {
      "type": "serialized.Type"
    }
  ]
}

シリアライゼーション用のプロキシクラスを指定するには、次のエントリを使用します。

{
  "serialization": [
    {
      "type": {
        "proxy": ["FullyQualifiedInterface1", "...", "FullyQualifiedInterfaceN"]
      }
    }
  ]
}

まれに、アプリケーションが明示的に以下の呼び出しを行う場合があります。

    ReflectionFactory.newConstructorForSerialization(Class<?> cl, Constructor<?> constructorToCall);

渡された constructorToCall は、cl の通常のシリアライゼーションが使用された場合に自動的に使用されるものとは異なります。

このようなシリアライゼーションユースケースもサポートするために、カスタム constructorToCall を使用してクラスのシリアライゼーションを登録できます。たとえば、org.apache.spark.SparkContext$$anonfun$hadoopFile$1 のシリアライゼーションを許可するには、java.lang.Object の宣言済みコンストラクターをカスタム targetConstructor として使用し、以下を使用します。

{
  "serialization": [
    {
      "type": "<fully-qualified-class-name>",
      "customTargetConstructorClass": "<custom-target-constructor-class>"
    }
  ]
}

到達可能性メタデータのサンプル #

*reachabilty-metadata.json* で使用できる到達可能性メタデータ設定のサンプルを以下に示します。

{
  "reflection": [
    {
      "type": "reflectively.accessed.Type",
      "fields": [
        {
          "name": "field1"
        }
      ],
      "methods": [
        {
          "name": "method1",
          "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"] 
        }
      ],
      "allDeclaredConstructors": true,
      "allPublicConstructors": true,
      "allDeclaredFields": true,
      "allPublicFields": true,
      "allDeclaredMethods": true,
      "allPublicMethods": true,
      "unsafeAllocated": true
    }
  ],
  "jni": [
    {
      "type": "jni.accessed.Type",
      "fields": [
        {
          "name": "field1"
        }
      ],
      "methods": [
        {
          "name": "method1",
          "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"]
        }
      ],
      "allDeclaredConstructors": true,
      "allPublicConstructors": true,
      "allDeclaredFields": true,
      "allPublicFields": true,
      "allDeclaredMethods": true,
      "allPublicMethods": true
    }
  ],
  "resources": [
    {
      "module": "optional.module.of.a.resource",
      "glob": "path1/level*/**"
    }
  ],
  "bundles": [
    {
      "name": "fully.qualified.bundle.name",
      "locales": ["en", "de", "other_optional_locales"]
    }
  ],
  "serialization": [
    {
      "type": "serialized.Type",
      "customTargetConstructorClass": "optional.serialized.super.Type"
    }
  ]
}

実行時にクラスを定義する #

Java は、実行時にバイトコードから新しいクラスを読み込むことをサポートしていますが、Native Image ではすべてのクラスがビルド時に既知である必要があるため (「閉じた世界の前提」)、これは不可能です。この問題を克服するには、次のオプションがあります。

  1. アプリケーション (またはサードパーティライブラリ) を変更または再構成して、実行時にクラスを生成したり、組み込み以外のクラスローダーを介してクラスを読み込んだりしないようにします。
  2. クラスを生成する必要がある場合は、専用クラスの静的初期化子でビルド時に生成してみてください。生成された java.lang.Class オブジェクトは静的フィールドに格納し、ビルド引数として --initialize-at-build-time=<class_name> を渡して専用クラスを初期化する必要があります。
  3. 上記のいずれも該当しない場合は、Native Image Agent を使用してアプリケーションを実行し、 java -agentlib:native-image-agent=config-output-dir=<config-dir>,experimental-class-define-support <application-arguments> で定義済みクラスを収集します。実行時に、トレース中に検出されたクラスのいずれかと同じ名前とバイトコードを持つクラスを読み込もうとすると、定義済みクラスがアプリケーションに提供されます。

定義済みクラスのメタデータは、*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"
      }
    ]
  }
]

注: 定義済みクラスのメタデータは、手動で記述することを意図したものではありません。注: 定義済みクラスは、レガシープロジェクト向けのベストエフォートのアプローチであり、動作を保証するものではありません。

参考文献 #

お問い合わせ