ネイティブ・イメージGradleプラグインを使用した到達可能性メタデータを含める

Gradleを使用して、Javaアプリケーションからネイティブ実行可能ファイルをビルドできます。そのためには、ネイティブ・ビルド・ツール・プロジェクトの一部として提供されているGraalVMネイティブ・イメージGradleプラグインを使用します。

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

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

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

このガイドでは、GraalVM到達可能性メタデータ・リポジトリおよびトレース・エージェントを使用してネイティブ実行可能ファイルをビルドする方法を説明します。このガイドの目的は、2つのアプローチの違いを説明し、到達可能性メタデータの使用により、開発タスクを簡略化できる仕組みを示すことです。

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

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

ノート: Gradleの実行には、バージョンが17から20の間のJavaが必要です(Gradleの互換性マトリックスを参照してください)。ただし、Java 21以上でアプリケーションを実行する場合は、次のような回避策があります: JAVA_HOMEをJavaバージョン17から20に設定し、GRAALVM_HOMEをJDK 21のGraalVMに設定します。詳細は、ネイティブ・イメージGradleプラグインのドキュメントを参照してください。

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

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

  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を開き、次の内容を次のように置き換えます:

     plugins {
         id 'application'
         // 1. Native Image Gradle plugin
         id 'org.graalvm.buildtools.native' version '0.9.28'
     }
    
     repositories {
         mavenCentral()
     }
        
     // 2. Application main class
     application {
         mainClass.set('org.graalvm.example.H2Example')
     }
    
     dependencies {
         // 3. H2 Database dependency
         implementation("com.h2database:h2:2.2.220")
     }
    
     // 4. Native Image build configuration
     graalvmNative {
         agent {
             defaultMode = "standard"
         }
         binaries {
             main {
                 imageName.set('h2example')
                 buildArgs.add("-Ob")
             }
         }
     }
    

    1 ネイティブ・イメージGradleプラグインを有効にします。プラグインにより、どのJARファイルをnative-imageに渡す必要があり、実行可能ファイルのメイン・クラスはどのような内容であることが必要かが検出されます。

    2 アプリケーションのメイン・クラスを明示的に指定します。

    3 Java用のオープン・ソースSQLデータベースであるH2データベースへの依存性を追加します。アプリケーションは、JDBCドライバを介してこのデータベースと対話します。

    4 graalvmNativeプラグイン構成のnative-imageツールにパラメータを渡すことができます。個々のbuildArgsでは、コマンド・ラインの場合とまったく同じ方法でパラメータを渡すことが可能です。クイック・ビルド・モードを有効にする-Obオプション(開発時にのみ推奨)が例として使用されています。imageName.set()は、結果のバイナリの名前を指定するために使用します。その他の構成オプションの詳細は、プラグインのドキュメントを参照してください。

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

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

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

  7. (オプション)アプリケーションをビルドします。リポジトリのルート・ディレクトリから、次のコマンドを実行します:

    ./gradlew run
    

    これにより、実行可能JARファイルが生成されますが、このファイルには、アプリケーションのすべての依存性と正しく構成されたMANIFESTファイルも含まれています。

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

ネイティブ・イメージGradleプラグインでは、GraalVM到達可能性メタデータ・リポジトリがサポートされています。このリポジトリは、デフォルトでGraalVMネイティブ・イメージをサポートしていないライブラリに対してGraalVM構成を提供します。そのうちの1つが、このアプリケーションが依存するH2データベースです。サポートは明示的に有効にする必要があります。

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

     metadataRepository {
         enabled = true
     }
    

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

     graalvmNative {
         agent {
             defaultMode = "standard"
         }
         binaries {
             main {
                 imageName.set('h2example')
                 buildArgs.add("-Ob")
             }
         }
         metadataRepository {
             enabled = true
         }
     }
    

    プラグインにより、メタデータがリポジトリから自動的にダウンロードされす。

  2. そのメタデータを使用して、ネイティブ実行可能ファイルがビルドされます:

     ./gradlew nativeRun
    

    これにより、プラットフォームのネイティブ実行可能ファイル(h2example)が、build/native/nativeCompile/ディレクトリに生成されます。このコマンドでは、そのネイティブ実行可能ファイルからアプリケーションも実行されます。

GraalVM到達可能性メタデータ・リポジトリを使用すると、サード・パーティ・ライブラリに応じて、Javaアプリケーションのネイティブ・イメージの操作性が向上します。

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

native-imageのメタデータ構成を提供する2つ目の方法は、コンパイル時にトレース・エージェント(以降はエージェント)を注入するものです。

エージェントは、次の3つのモードで実行できます:

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

  1. build.gradleファイルを開き、graalvmNativeプラグイン構成に指定されているエージェント・モードを確認します。
     graalvmNative {
         agent {
             defaultMode = "standard"
         }
         ...
     }    
    

    コマンドライン・オプションを使用する場合は、-Pagent=standardです。

  2. 次に、JVMでエージェントを使用してアプリケーションを実行します。ネイティブ・イメージGradleプラグインでエージェントを有効にするには、JavaForkOptionsを拡張するGradleタスク(testrunなど)に-Pagentオプションを渡します:
    ./gradlew -Pagent run
    

    エージェントにより、H2データベースへのコールや、テスト実行中に検出されたすべての動的機能が、複数の*-config.jsonファイルに取得されて記録されます。

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

    必須ではありませんが、出力ディレクトリは/resources/META-INF/native-image/にすることをお薦めします。native-imageツールは、この場所からメタデータを自動的に取得します。アプリケーションのメタデータを自動的に収集する方法の詳細は、メタデータの自動収集に関する項を参照してください。

  4. エージェントによって収集された構成を使用して、ネイティブ実行可能ファイルをビルドします:
     ./gradlew nativeCompile
    

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

  5. ネイティブ実行可能ファイルからアプリケーションを実行します:
     ./build/native/nativeCompile/h2example
    
  6. (オプション)プロジェクトをクリーン・アップするには、./gradlew cleanを実行し、ディレクトリMETA-INFとそのコンテンツを削除します。

サマリー

このガイドでは、GraalVM到達可能性メタデータ・リポジトリおよびトレース・エージェントを使用して、ネイティブ実行可能ファイルをビルドする方法を説明しました。目的としたのは、その違いを示し、到達可能性メタデータを使用すると作業をどのように簡素化できるかを証明することです。

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

./gradlew nativeRun