共有到達可能性メタデータを使用したネイティブ・イメージの構成

GraalVMネイティブ・イメージのGradleプラグインを使用すると、Javaアプリケーションからネイティブ実行可能ファイルを簡単にビルドできます。このプラグインは、ネイティブ・ビルド・ツール・プロジェクトの一部として提供され、Gradleビルド・ツールを使用します。アプリケーションが実行時にクラスを動的にロードしない場合、ワークフローは./gradlew nativeRunという1つのコマンドにすぎません。

実際には、ほとんどの場合、アプリケーションは、Javaリフレクションや動的プロキシ・オブジェクトを呼び出すか、なんらかのネイティブ・コードを呼び出すか、クラス・パス上のリソース(native-imageツールがビルド時に認識する必要がある動的機能で、メタデータの形式で提供される)にアクセスします。ネイティブ・イメージは、実行時にではなく、ビルド時にクラスを動的にロードします。

アプリケーションの依存性に応じて、ネイティブ・イメージGradleプラグインにメタデータを提供する方法が3つあります:

  1. トレース・エージェントの使用
  2. 共有GraalVM到達可能性メタデータ・リポジトリの使用
  3. 自動検出(src/main/resourcesディレクトリのクラスパスで必要なリソースが直接使用可能な場合)

このガイドで使用されているJavaアプリケーションでは、最初の2つのアプローチを適用できます。このガイドでは、トレース・エージェントを使用してGraalVM到達可能性メタデータ・リポジトリを使用してネイティブ実行可能ファイルをビルドする方法を示します。目的は、ユーザーに違いを示し、共有メタデータを使用して作業をどのように簡素化できるかを証明することです。

手順に従ってステップごとにアプリケーションを作成することをお薦めします。または、完成例に進むこともできます。

デモ・アプリケーションの準備

  1. GraalVM JDKがインストール済であることを確認します。最も簡単に始めるには、SDKMAN!を使用します。その他のインストール・オプションについては、「ダウンロード」セクションを参照してください。

  2. org.graalvm.exampleパッケージで、お気に入りのIDEにGradleを使用して"H2Example"という新しいJavaプロジェクトを作成します。

  3. デフォルトのappディレクトリの名前をH2Exampleに変更し、デフォルトのファイル名App.javaH2Example.javaに変更して、その内容を次のように置き換えます:

     package org.graalvm.example;
    
     import java.sql.Connection;
     import java.sql.DriverManager;
     import java.sql.PreparedStatement;
     import java.sql.ResultSet;
     import java.sql.SQLException;
     import java.util.ArrayList;
     import java.util.Comparator;
     import java.util.HashSet;
     import java.util.List;
     import java.util.Set;
    
     public class H2Example {
    
         public static final String JDBC_CONNECTION_URL = "jdbc:h2:./data/test";
    
         public static void main(String[] args) throws Exception {
             // Cleanup
             withConnection(JDBC_CONNECTION_URL, connection -> {
                 connection.prepareStatement("DROP TABLE IF EXISTS customers").execute();
                 connection.commit();
             });
    
             Set<String> customers = Set.of("Lord Archimonde", "Arthur", "Gilbert", "Grug");
    
             System.out.println("=== Inserting the following customers in the database: ");
             printCustomers(customers);
    
             // Insert data
             withConnection(JDBC_CONNECTION_URL, connection -> {
                 connection.prepareStatement("CREATE TABLE customers(id INTEGER AUTO_INCREMENT, name VARCHAR)").execute();
                 PreparedStatement statement = connection.prepareStatement("INSERT INTO customers(name) VALUES (?)");
                 for (String customer : customers) {
                     statement.setString(1, customer);
                     statement.executeUpdate();
                 }
                 connection.commit();
             });
    
             System.out.println("");
             System.out.println("=== Reading customers from the database.");
             System.out.println("");
    
             Set<String> savedCustomers = new HashSet<>();
             // Read data
             withConnection(JDBC_CONNECTION_URL, connection -> {
                 try (ResultSet resultSet = connection.prepareStatement("SELECT * FROM customers").executeQuery()) {
                     while (resultSet.next()) {
                         savedCustomers.add(resultSet.getObject(2, String.class));
                     }
                 }
             });
    
             System.out.println("=== Customers in the database: ");
             printCustomers(savedCustomers);
         }
    
         private static void printCustomers(Set<String> customers) {
             List<String> customerList = new ArrayList<>(customers);
             customerList.sort(Comparator.naturalOrder());
             int i = 0;
             for (String customer : customerList) {
                 System.out.println((i + 1) + ". " + customer);
                 i++;
             }
         }
    
         private static void withConnection(String url, ConnectionCallback callback) throws SQLException {
             try (Connection connection = DriverManager.getConnection(url)) {
                 connection.setAutoCommit(false);
                 callback.run(connection);
             }
         }
    
         private interface ConnectionCallback {
             void run(Connection connection) throws SQLException;
         }
     }
    
  4. H2Example/src/test/javaディレクトリを削除します。

  5. Gradle構成ファイルbuild.gradleを開き、applicationセクションのメイン・クラスを更新します:
     application {
         mainClass.set('org.graalvm.example.H2Example')
     }
    
  6. Java用のオープン・ソースSQLデータベースであるH2データベースへの明示的な依存性を追加します。アプリケーションは、JDBCドライバを介してこのデータベースと対話します。build.gradledependenciesセクションに次の行を挿入します:
     dependencies {
         implementation("com.h2database:h2:2.1.210")
     }
    

    また、dependenciesセクションで、使用されないguavaに対する依存性を削除します。

    次のステップでは、ネイティブ・イメージGradleプラグインを有効にするために実行する必要がある操作に焦点を当てます。

  7. ネイティブ・イメージGradleプラグインを登録します。プロジェクトのbuild.gradleファイルのpluginsセクションに次を追加します:
     plugins {
     // ...
     id 'org.graalvm.buildtools.native' version '0.9.13'
     }
    

    プラグインにより、どのJARファイルをnative-imageビルダーに渡す必要があり、実行可能ファイルのメイン・クラスがどのようなものである必要があるかが検出されます。

  8. プラグインはまだGradleプラグイン・ポータルで使用できないため、追加のプラグイン・リポジトリを宣言します。settings.gradleファイルを開き、デフォルトの内容を次のように置き換えます:

     pluginManagement {
         repositories {
             mavenCentral()
             gradlePluginPortal()
         }
     }
    
     rootProject.name = 'H2Example'
     include('H2Example')
    

    pluginManagement {}ブロックは、ファイル内の他の文の前に指定する必要があります。

エージェントを使用したネイティブ実行可能ファイルのビルド

ネイティブ・イメージGradleプラグインは、コンパイル時にトレース・エージェント(後のエージェント)を自動的に注入することで、必要なメタデータの生成を簡略化します。エージェントを有効にするには、JavaForkOptionsを拡張するGradleタスク(testrunなど)に-Pagentオプションを渡すだけです。

エージェントは複数のモードで実行できます:

エージェントは、コマンドラインまたはbuild.gradleファイルでオプションを渡すように構成できます。ネイティブ・イメージGradleプラグインを構成し、トレース・エージェントでメタデータを収集し、提供された構成を適用するネイティブ実行可能ファイルをビルドする方法を次に示します。

  1. (オプション)標準モードで実行するようにエージェントに指示します。build.gradleファイルの下部に次の構成ブロックを挿入します:

     graalvmNative {
         agent {
             defaultMode = "standard"
         }
         binaries {
             main {
                 imageName.set('h2demo') 
             }
         }
         toolchainDetection = false
     }
    

    コマンドライン・オプションを使用する場合は、-Pagent=standardになります。構成の2番目の部分は、最終的なネイティブ実行可能ファイルのカスタム名を指定する方法を示しています。

    ここで注意すべきもう1つの点として、Gradleの制限のため、プラグインがGraalVMインストールを正しく検出できない場合があります。回避策は、toolchainDetection = falseコマンドを使用してツールチェーンの検出を無効にすることです。GraalVMツールチェーンの選択の詳細は、こちらを参照してください。

  2. 次に、JVMでエージェントを有効にしてアプリケーションを実行します:

     ./gradlew -Pagent run
    

    エージェントは、H2データベースへの呼出しおよびテスト実行中に検出されたすべての動的機能を複数の*-config.jsonファイルに取得して書き込みます。

  3. メタデータが収集されたら、metadataCopyタスクを使用してプロジェクトの/META-INF/native-imageディレクトリにコピーします:

     ./gradlew metadataCopy --task run --dir src/main/resources/META-INF/native-image
    

    JSONファイルは、META-INF/native-image/<group.id>/<artifact.id>プロジェクト・ディレクトリに格納されます。必須ではありませんが、出力ディレクトリは/resources/META-INF/native-image/にすることをお薦めします。native-imageツールは、その場所からメタデータを自動的に取得します。アプリケーションのメタデータを自動的に収集する方法の詳細は、メタデータの自動収集に関する項を参照してください。このステップの後の予想されるファイル・ツリーは次のとおりです:

    エージェントによって生成される構成ファイル

  4. エージェントによって取得されたメタデータを使用してネイティブ実行可能ファイルをビルドします:

     ./gradlew nativeCompile
    

    h2demoという名前のネイティブ実行可能ファイルがbuild/native/nativeCompileディレクトリに作成されます。

  5. ネイティブ実行可能ファイルからアプリケーションを実行します:

     ./H2Example/build/native/nativeCompile/h2demo
    

ネイティブ・イメージGradleプラグインとのエージェントの使用の詳細は、こちらを参照してください。

重要: 次の項に進むには、プロジェクト./gradlew cleanをクリーンアップします。META-INFとその内容を必ず削除してください。

GraalVM到達可能性メタデータ・リポジトリを使用したネイティブ実行可能ファイルのビルド

リリース0.9.11以降、ネイティブ・イメージGradleプラグインは、GraalVM到達可能性メタデータ・リポジトリの試験段階のサポートを追加します。このリポジトリは、デフォルトでGraalVMネイティブ・イメージをサポートしていないライブラリに対してGraalVM構成を提供します。サポートは明示的に有効にする必要があります。

  1. build.gradleファイルを開き、graalvmNativeプラグイン構成でGraalVM到達可能性メタデータ・リポジトリを有効にします:

     metadataRepository {
         enabled = true
     }
    

    構成ブロック全体は次のようになります:

     graalvmNative {
         agent {
             defaultMode = "standard"
         }
         binaries {
             main {
                 imageName.set('h2demo') 
             }
         }
         metadataRepository {
             enabled = true
         }
         toolchainDetection = false
     }
    

    プラグインはメタデータをリポジトリから自動的にダウンロードします。

  2. 次に、共有リポジトリからネイティブ実行可能ファイルの再利用メタデータをビルドします:
     ./gradlew nativeRun
    
  3. ネイティブ実行可能ファイルからアプリケーションを実行します:

     ./H2Example/build/native/nativeCompile/h2demo
    

より少ないステップで、同じ結果に到達しています。共有GraalVM到達可能性メタデータ・リポジトリを使用すると、サード・パーティ・ライブラリに応じて、Javaアプリケーションのネイティブ・イメージの操作性が向上します。

サマリー

GraalVM到達可能性メタデータ・リポジトリを使用すると、ネイティブ・イメージ・ユーザーはJavaエコシステム内のライブラリおよびフレームワークのメタデータを共有および再利用できるため、サード・パーティの依存関係を維持する負担が共有されます。

アプリケーションが実行時に動的機能を呼び出さない場合、エージェントの実行またはGraalVM到達可能性メタデータ・リポジトリの有効化は不要です。その場合、ワークフローは次のようになります:

./gradlew nativeRun