戻る

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

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

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

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

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

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

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

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

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

前提条件

GraalVM JDKがインストールされていることを確認してください。開始する最も簡単な方法はSDKMAN!です。その他のインストールオプションについては、ダウンロードセクションを参照してください。

  1. お気に入りのIDEで、org.graalvm.exampleパッケージに「H2Example」という名前の新しいJavaプロジェクトをGradleで作成します。

  2. デフォルトの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;
         }
     }
    
  3. H2Example/src/test/java/ディレクトリ(存在する場合)を削除します。

  4. Gradle構成ファイルbuild.gradleを開き、その内容を以下に置き換えます。
     plugins {
         id 'application'
         // 1. Native Image Gradle plugin
         id 'org.graalvm.buildtools.native' version '0.10.1'
     }
    
     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プラグインを有効にします。プラグインは、native-imageに渡す必要があるJARファイルと、実行可能メインクラスを検出します。

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

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

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

  5. プラグインはまだGradle Plugin Portalでは利用できないため、追加のプラグインリポジトリを宣言します。settings.gradleファイルを開き、デフォルトのコンテンツを以下に置き換えます。
     pluginManagement {
         repositories {
             mavenCentral()
             gradlePluginPortal()
         }
     }
    
     rootProject.name = 'H2Example'
     include('H2Example')
    

    pluginManagement {}ブロックは、ファイル内の他のステートメントの前に表示する必要があります。

  6. (オプション)アプリケーションをビルドします。リポジトリのルートディレクトリから、次のコマンドを実行します。
    ./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
    

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

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

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

native-imageのメタデータ構成を提供する2番目の方法は、コンパイル時にトレーシングエージェント(以降、エージェント)を挿入することです。

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

  • 標準:条件なしでメタデータを収集します。ネイティブ実行ファイルをビルドする場合は、これが推奨されます。
  • 条件付き:条件付きでメタデータを収集します。さらに使用するネイティブ共有ライブラリの条件付きメタデータを作成する場合は、これが推奨されます。
  • 直接:上級ユーザーのみ。このモードでは、エージェントに渡されるコマンドラインを直接制御できます。

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

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

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

  2. 次に、JVMでエージェントを使用してアプリケーションを実行します。ネイティブイメージGradleプラグインでエージェントを有効にするには、JavaForkOptionsを拡張するGradleタスク(たとえば、testまたはrun)に-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

お問い合わせ