ネイティブ・イメージでのリフレクション

Javaリフレクションがサポートされているため(java.lang.reflect.* API)、Javaコードでは、実行時に独自のクラス、メソッド、フィールドおよびそれらのプロパティを調べることができます。

ネイティブ・イメージでのリフレクションのサポートは部分的であり、リフレクティブにアクセスされるプログラム要素の事前把握が必要になります。java.lang.reflect.*を使用してプログラム要素を調べてアクセスしたり、実行時にClass.forName(String)を使用してクラスをロードするには、それらのプログラム要素の追加メタデータを準備する必要があります。(ノート: Class.forName(String)を使用したクラスのロードは、リフレクションと密接に関連しているため、ここに含まれています。)

ネイティブ・イメージでは、リフレクションAPIのコールを検出する静的分析によってターゲット要素の解決が試みられます。分析が失敗した場合、実行時にリフレクティブにアクセスされるプログラム要素は、手動構成を使用して指定する必要があります。詳細は、到達可能性メタデータおよびトレース・エージェントを使用したメタデータの収集を参照してください。

目次

自動検出

分析では、Class.forName(String)Class.forName(String, ClassLoader)Class.getDeclaredField(String)Class.getField(String)Class.getDeclaredMethod(String, Class[])Class.getMethod(String, Class[])Class.getDeclaredConstructor(Class[])およびClass.getConstructor(Class[])のコールがインターセプトされます。

これらのコールの引数を定数まで減らすことができる場合、ネイティブ・イメージによってターゲット要素の解決が試みられます。ターゲット要素を解決できる場合、コールは削除され、かわりにターゲット要素がコードに埋め込まれます。ターゲット要素を解決できない場合(クラスがクラスパスにない場合やフィールド/メソッド/コンストラクタを宣言していない場合など)、コールは実行時に適切な例外をスローするスニペットに置き換えられます。これには、2つの利点があります。まず、実行時にリフレクションAPIのコールがありません。次に、GraalVMでは、定数畳込みを使用してコードをさらに最適化できます。

コールがインターセプトおよび処理されるのは、パラメータを定数に減らすことができると明白に判断できる場合のみです。たとえば、Class.forName(String)のコールは、クラスが実際にクラスパスにあるものと想定して、String引数を定数畳込みできる場合にのみClassリテラルに置き換えられます。また、Class.getMethod(String, Class[])のコールは、Class[]引数の内容を確実に判別できる場合にのみ処理されます。この最後の制限は、Javaに不変配列がないことに起因するものです。したがって、配列が割り当てられてから引数として渡されるまでの間に配列に加えられたすべての変更を追跡する必要があります。分析は単純なルールに従います。つまり、配列へのすべての書込みがコードの線形セクションで発生する場合、つまり制御フローの分割がない場合、配列はコールの分析目的で事実上定数になります。

このため、静的フィールドがfinalであっても、その内容はいつでも変更される可能性があるため、分析では静的フィールドからのClass[]引数は受け入れられません。これは、制限が厳しすぎるように思えますが、リフレクションAPIのコールで最もよく使用されるパターンに対応しています。

定数引数ルールの唯一の例外は、Class.forName(String, ClassLoader)ClassLoader引数が定数である必要がないことです。これは無視され、かわりにクラスパス上のすべてのクラスをロードできるクラス・ローダーが使用されます。分析は固定ポイントまで実行されます。つまり、Class.forName(String).getMethod(String, Class[])のようなコールのチェーンによって最初にクラス定数が置き換えられ、次にメソッドによってそれがjava.lang.reflect.Methodまで事実上縮小されます。

次に、インターセプトして対応する要素に置き換えることができるコールの例をいくつか示します:

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")

配列を宣言および移入する次の方法は、分析の観点からは同等です:

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);

コールを処理できない場合はスキップされます。このような状況では、次に説明するような手動構成を指定できます。

手動構成

リフレクティブにアクセスされるプログラム要素を指定する構成は、次のように指定できます:

-H:ReflectionConfigurationFiles=/path/to/reflectconfig

ここで、reflectconfigは次の形式のJSONファイルです(詳細は、--expert-optionsを使用してください):

[
  {
    "name" : "java.lang.Class",
    "queryAllDeclaredConstructors" : true,
    "queryAllPublicConstructors" : true,
    "queryAllDeclaredMethods" : true,
    "queryAllPublicMethods" : true,
    "allDeclaredClasses" : true,
    "allPublicClasses" : true
  },
  {
    "name" : "java.lang.String",
    "fields" : [
      { "name" : "value" },
      { "name" : "hash" }
    ],
    "methods" : [
      { "name" : "<init>", "parameterTypes" : [] },
      { "name" : "<init>", "parameterTypes" : ["char[]"] },
      { "name" : "charAt" },
      { "name" : "format", "parameterTypes" : ["java.lang.String", "java.lang.Object[]"] }
    ]
  },
  {
    "name" : "java.lang.String$CaseInsensitiveComparator",
    "queriedMethods" : [
      { "name" : "compare" }
    ]
  }
]

この構成では、Method.invoke(Object, Object...)またはConstructor.newInstance(Object...)を介した実行中に呼び出すことができるメソッドとコンストラクタと、呼び出すことができないメソッドとコンストラクタは区別されます。呼び出す機能のない関数を構成に含めると、静的な分析が、到達可能性ステータスを正しく評価するために役立ち、結果としてバイナリ・サイズが縮小されます。その後、関数のメタデータには、他の登録済リフレクション・メソッドまたはコンストラクタのように実行時にアクセスできます。ただし、関数をコールしようとするとランタイム・エラーになります。queryまたはqueriedという接頭辞が付いた構成フィールドのみにメタデータが含まれます。一方、その他のフィールド(methodsなど)ではランタイム呼出しが有効になります。

ネイティブ・イメージ・ビルダーでは、そのファイルで参照されるすべてのクラス、メソッドおよびフィールドのリフレクション・メタデータが生成されます。queryAllPublicConstructorsqueryAllDeclaredConstructorsqueryAllPublicMethodsqueryAllDeclaredMethodsallPublicConstructorsallDeclaredConstructorsallPublicMethodsallDeclaredMethodsallPublicFieldsallDeclaredFieldsallPublicClassesおよびallDeclaredClasses属性を使用すると、クラスのメンバー・セット全体を自動的に含めることができます。

ただし、allPublicClassesおよびallDeclaredClassesでは、リフレクティブ・アクセス用の内部クラスが自動的に登録されることはありません。単に、宣言クラスでコールされたときに、Class.getClasses()およびClass.getDeclaredClasses()を介して使用可能になります。コードでこの例のString.valueのような非static finalフィールドを記述することもできますが、他のコードでは、最適化が理由で非finalフィールドと同じようにはfinalフィールド値の変更が監視されないこともあります。static finalフィールドを記述することはできません。

ReflectionConfigurationFilesに複数のパスを指定し、それらを,で区切ることで、複数の構成を使用できます。また、-H:ReflectionConfigurationResourcesを指定すると、JARファイルなど、ビルドのクラスパスから1つ以上の構成ファイルをロードできます。

条件付き構成

条件付き構成では、指定されたconditionが満たされる場合にのみクラス構成エントリが適用されます。現在サポートされている条件はtypeReachableのみです。指定タイプが他のコードを介して到達可能な場合に構成エントリが有効になります。たとえば、io.netty.util.internal.PlatformDependent0に到達可能な場合にsun.misc.Unsafe.theUnsafeへのリフレクティブ・アクセスをサポートするには、構成は次のようになります:

{
  "condition" : { "typeReachable" : "io.netty.util.internal.PlatformDependent0" },
  "name" : "sun.misc.Unsafe",
  "fields" : [
    { "name" : "theUnsafe" }
  ]
}

条件付き構成は、リフレクション構成を指定する推奨方法です: リフレクティブ・アクセスを実行するコードに到達できない場合、対応するリフレクション・エントリを含める必要はありません。conditionを一貫して使用することで、バイナリが小さくなりビルド時間が短縮されます。イメージ・ビルダはリフレクティブ・アクセスのコードを選択して組み込むことができるためです。

conditionを省略すると、要素は常に組み込まれます。同じconditionが2つの構成エントリの2つの異なる要素に使用される場合、条件が満たされると両方の要素が含まれます。複数のタイプのいずれかが到達可能な場合に構成エントリを有効にする必要があるときは、2つの構成エントリ(条件ごとに1つのエントリ)を追加する必要があります。

構成支援とともに使用した場合、既存構成の条件付きエントリはエージェント収集エントリでは使用されません。

機能を含む構成

または、カスタムのFeature実装では、RuntimeReflectionクラスを使用して、ビルドの分析フェーズの前および最中にプログラム要素を登録できます。たとえば:

class RuntimeReflectionRegistrationFeature implements Feature {
  public void beforeAnalysis(BeforeAnalysisAccess access) {
    try {
      RuntimeReflection.register(String.class);
      RuntimeReflection.register(String.class.getDeclaredField("value"));
      RuntimeReflection.register(String.class.getDeclaredField("hash"));
      RuntimeReflection.register(String.class.getDeclaredConstructor(char[].class));
      RuntimeReflection.register(String.class.getDeclaredMethod("charAt", int.class));
      RuntimeReflection.register(String.class.getDeclaredMethod("format", String.class, Object[].class));
      RuntimeReflection.register(String.CaseInsensitiveComparator.class);
      RuntimeReflection.register(String.CaseInsensitiveComparator.class.getDeclaredMethod("compare", String.class, String.class));
    } catch (NoSuchMethodException | NoSuchFieldException e) { ... }
  }
}

カスタム機能をアクティブ化するには、--features=<fully qualified name of RuntimeReflectionRegistrationFeature class>をnative-imageに渡す必要があります。ネイティブ・イメージ・ビルド構成では、META-INF/native-imagenative-image.propertiesファイルでこれを自動化する方法について説明します。

ビルド時のリフレクションの使用

リフレクションは、ネイティブ・バイナリの生成時に静的イニシャライザなどで制限なく使用できます。この時点で、コードによってメソッドおよびフィールドに関する情報を収集し、独自のデータ構造に格納できます。これにより、実行時のリフレクションが不要になります。

安全でないアクセス

非推奨ながら、Unsafeクラスを使用すると、Javaオブジェクトのメモリーへの直接アクセスが可能になります。Unsafe.objectFieldOffset()メソッドを通じて、Javaオブジェクト内のフィールドのオフセットを指定します。ネイティブ・バイナリのビルド時に問合せが行われるオフセットは、実行時のオフセットとは異なる場合があります。