ノート:

意見とGraalVM Native Imageについて

イントロダクション

このラボは、GraalVM Native Imageにおけるリフレクションの動作について詳しく理解したい開発者向けです。

GraalVM Native Imageを使用すると、Javaアプリケーションを自己完結型ネイティブ実行可能ファイルに事前にコンパイルできます。GraalVM Native Imageでは、実行時にアプリケーションで必要とされるコードのみがネイティブの実行可能ファイルに追加されます。

これらのネイティブの実行可能ファイルには、次のような多くの重要な利点があります。

業界をリードするマイクロサービス・フレームワークの多くは、Micronaut、Spring、Helidon、Quarkusなど、GraalVM Native Imageのコンパイルを事前にサポートしています。

さらに、ネイティブ・イメージ用のMavenおよびGradleプラグインがあり、Javaアプリケーションのビルド、テストおよび実行をネイティブ実行可能ファイルとして容易にします。

注意: Oracle Cloud Infrastructure (OCI)は、追加コストなしでGraalVM Enterpriseを提供します。

予想実習時間: 30分

ラボの目的

この演習では、次のタスクを実行します。

注:ラップトップアイコンが表示された場合は常に、このようにする必要があります。これらに注意しなさい。

# This is where we you will need to do something

開発環境は、リモート・ホスト(Oracle Linux 8、1 CPUおよび32GBのメモリーを備えたOCIコンピュート・インスタンス)によって提供されます。
Luna Labsデスクトップ環境は、リモート・ホストの準備が完了する前に表示され、最大2分かかる場合があります。

ステップ1:仮想ホストへの接続および開発環境の確認

リモート・ホストへの接続は、Lunaデスクトップ環境でセットアップ・スクリプトを実行します。このスクリプトは、「リソース」タブで使用できます

  1. デスクトップで、「Luna-Lab.html」アイコンをダブルクリックします。ページが開き、ラボに固有のOracle Cloud Infrastructure資格証明および情報が表示されます。

  2. 「リソース」タブが表示されます。コンピュート・インスタンスが10分のクラウドでプロビジョニングされている間は、「リソース」タイトルの横に表示された歯車が回転することに注意してください。

  3. インスタンスのプロビジョニング時に最大2分かかる場合、「リソース」タブに次が表示されます

    Lunaリソース・タブ

  4. 「リソース」タブから、VSコード環境を設定する構成スクリプトをコピーします。「詳細の表示」リンクをクリックして、構成を表示します。次のスクリーンショットに示すように、これをコピーします。

    構成スクリプトのコピー

  5. 次のスクリーンショットに示すように、ターミナルを開きます。

    ターミナルを開く

  6. 構成コードを端末に貼り付け、VSコードを開きます。

    端末1を貼り付け

    端末2を貼り付け

完了しました。これで、Oracle Cloudのリモート・ホストに正常に接続されました。

ステップ2:クローズ済ワールド仮定

GraalVMとともに提供されるnative-imageツールでスタンドアロン実行可能ファイルを構築することは、Javaアプリケーションの構築とは少し異なります。「ネイティブ・イメージ」では、「クローズド・ワールド」仮定と呼ばれるものが使用されます。

「Closed World」という仮定は、実行時にコールできるアプリケーション内のすべてのバイトコードが、構築時に認識される(つまり、native-imageツールがスタンドアロン実行可能ファイルを構築している場合)必要があることを意味します。

続行する前に、GraalVM Native Imageで作成されたアプリケーションの構築/実行モデルについて検討してみてください。

  1. Javaソース・コードのJavaバイト・コード・クラスへのコンパイル
  2. native-imageツールを使用して、ネイティブ実行可能ファイルにこれらのJavaバイト・コード・クラスを作成します。
  3. ネイティブ実行可能ファイルを実行

しかし、手順2で実際に何が起こりますか。

最初に、native-imageツールは分析を実行して、アプリケーション内のどのクラスが到達可能かを確認します。これについては、まもなく詳細に見ていきます。

次に、初期化しても安全であることがわかっているクラス(セーフ・クラスの自動初期化)が初期化されます。初期化されたクラスのクラス・データはイメージ・ヒープにロードされ、その後、スタンドアロンの実行可能ファイルに保存されます(テキスト・セクション内)。これは、このようなファスト・スタート・アプリケーションに合せて作成できるGraalVM native-imageツールの機能の1つです。

注::これはオブジェクトの初期化とは異なります。オブジェクトの初期化は、ネイティブ実行可能ファイルの実行時に行われます。

私たちは到達可能性の話題に戻ると言った。前述のように、分析では、スタンドアロンの実行可能ファイルに含める必要があるクラス、メソッドおよびフィールドが決定されます。分析は静的であり、コードは実行されません。解析では、ダイナミック クラスのロードとリフレクションの使用(を参照)のケースを判断できますが、選択できない場合があります。

Javaの動的機能を処理するには、どのクラスが反射を使用するか、どのクラスがダイナミカル・ロードされているかについて分析が知っている必要があります。

例を見てみましょう。

ステップ3:反射を使用した例

次のクラスReflectionExample.javaがあるとします(このコピーはディレクトリdemo/ReflectionExample.javaにあります)。

import java.lang.reflect.Method;

class StringReverser {
    static String reverse(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}

class StringCapitalizer {
    static String capitalize(String input) {
        return input.toUpperCase();
    }
}

public class ReflectionExample {
    public static void main(String[] args) throws ReflectiveOperationException {
        String className = args[0];
        String methodName = args[1];
        String input = args[2];

        Class<?> clazz = Class.forName(className);
        Method method = clazz.getDeclaredMethod(methodName, String.class);
        Object result = method.invoke(null, input);
        System.out.println(result);
    }
}

最初に、VSコード内に端末を作成します。これは、「ターミナル」→「新規ターミナル」から実行します

次に、コードを作成します。シェルで、VSコード内から次のコマンドを実行します。

javac ReflectionExample.java

クラスReflectionExampleのmainメソッドは、引数として名前が渡されたクラス(非常に動的なユースケース)をロードします。クラスの2番目の引数は、動的にロードされるクラスで起動されるメソッド名です。

それを実行して、何が実行されているかを確認します。

java ReflectionExample StringReverser reverse "hello"

想定どおり、クラスStringReverserのメソッドreverseがリフレクションを介して見つかりました。メソッドが呼び出され、入力文字列"hello"が元に戻されました。今のところは順調だよ。

よろしいですか。ただし、ネイティブ・イメージをプログラムから構築しようとするとどうなりますか。やってみよう。シェルで次のコマンドを実行します。

native-image --no-fallback ReflectionExample

注: native-image--no-fallbackオプションを指定すると、スタンドアロンのネイティブexecutabaleを構築できない場合、ビルドは失敗します。

生成されたネイティブ実行可能ファイルを実行し、その動作を確認します。

./reflectionexample StringReverser reverse "hello"

Exception in thread "main" java.lang.ClassNotFoundException: StringReverser
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:60)
	at java.lang.Class.forName(DynamicHub.java:1214)
	at ReflectionExample.main(ReflectionExample.java:21)

ここで何が起きたのですか。ネイティブ実行可能ファイルはクラスStringReverserを見つけることができなかったようです。どのようになったか?今までには、その理由がおそらくわかると思います。クローズされたワールド仮定。

native-imageツールが実行した分析中に、クラスStringReverserが使用されたことを判別できませんでした。そのため、生成されたネイティブ実行可能ファイルからクラスを削除しました。ノート:スタンドアロンの実行可能ファイルから不要なクラスを削除することで、使用されることがわかっているクラスのみを含めて構築されるコードを縮小するツール。ここまで見たように、これは反射の問題を招くかもしれませんが、幸運にもこれに対処する方法があります。

ステップ4:ネイティブ・イメージ反射構成の導入

native-imageビルド・ツールに、特別な構成ファイルを使用したリフレクションのインスタンスについて通知できます。これらのファイルはJSONで記述され、フラグを使用してnative-imageツールに渡すことができます。

そのため、native-imageビルド・ツールには、その他のタイプの構成情報を渡すことができますか。ツールは現在、詳細を含むファイルの読み取りをサポートしています。

この実習での意見に対処する方法を検討しているだけなので、そのことに焦点を当てます。

次に、これらのファイルの例を示します(ここから説明します)。

[
  {
    "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" }
    ]
  }
]

ここから、Reflection APIを介してアクセスされるクラスおよびメソッドを構成する必要があることがわかります。この操作は手動で実行できますが、これらの構成ファイルを生成する最も便利な方法は、補助された構成javaagentを使用することです。

ステップ5:ネイティブ・イメージ、補助構成: Javaエージェントの入力

完全なリフレクション構成ファイルを最初から作成することは確実可能ですが、GraalVM Javaランタイムはjavaトレース・エージェント(javaagent)を提供しています。このエージェントは、アプリケーションの実行時に自動的に生成されます。

これをやってみよう。

トレース・エージェントを有効にしてアプリケーションを実行します。このシェルで次を実行します。

# Note: the tracing agent parameter must come before the classpath and jar params on the command ine
java -agentlib:native-image-agent=config-output-dir=META-INF/native-image ReflectionExample StringReverser reverse "hello"

エージェント構成のトレース

作成した構成を確認します。

cat META-INF/native-image/reflect-config.json
[
    {
    "name":"StringReverser",
    "methods":[{"name":"reverse","parameterTypes":["java.lang.String"] }]
    }
]

次の例に示すように、native-image-agent=config-merge-dirを指定すると、このプロセスを複数回実行でき、実行がマージされます。

java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image ReflectionExample StringCapitalizer capitalize "hello"

スタンドアロン実行可能ファイルを構築すると、提供された構成が使用されます。次のように構築します。

native-image --no-fallback ReflectionExample

それよりもうまく機能するかどうかを見ていきましょう。

./reflectionexample StringReverser reverse "hello"

はい。

結論

GraalVM Native Imageのスタンドアロン・実行可能ファイルを構築することは、クローズド・ワールド仮定に頼っています。これは、スタンドアロン実行可能ファイルを構築する際に、コード内で発生する可能性のある反射のケースについて事前に知っておく必要があることです。

GraalVMプラットフォームは、リフレクションが使用される場合に、native-imageビルド・ツールに指定する方法を提供します。ノート:簡単な場合、native-imageツールでは、これら自体を検出できます。

GraalVMプラットフォームは、Java Traceエージェントを通じて反射やその他の動的動作の使用状況を検出する方法も提供しており、native-imageツールに必要な構成ファイルを自動的に生成できます。

トレース・エージェントを使用する際には、次のことを考慮する必要があります。

このチュートリアルを楽しんでいただければ、ネイティブ・イメージの使用時にリフレクションを処理する方法について学びます。

さらに学ぶ

その他の学習リソース

docs.oracle.com/learnの他のラボを調べるか、Oracle Learning YouTubeチャネルでさらに無料の学習コンテンツにアクセスします。さらに、education.oracle.com/learning-explorerにアクセスしてOracle Learning Explorerにします。

製品ドキュメントは、Oracleヘルプ・センターを参照してください。