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

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

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

アプリケーションの依存性に応じて、メタデータを提供する方法が3つあります:

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

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

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

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

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

  2. お気に入りのIDEでMavenを使用して、またはコマンド・ラインからMavenを使用して、H2Exampleという新しいJavaプロジェクトをorg.graalvm.exampleパッケージに作成します。

  3. メイン・クラス・ファイルsrc/main/java/org/graalvm/example/H2Example.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. プロジェクト構成ファイルpom.xmlを開き、その内容を次のように置き換えます:
     <?xml version="1.0" encoding="UTF-8"?>
     <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
             http://maven.apache.org/xsd/maven-4.0.0.xsd">
         <modelVersion>4.0.0</modelVersion>
    
         <groupId>org.graalvm.buildtools.examples</groupId>
         <artifactId>maven</artifactId>
         <version>1.0.0-SNAPSHOT</version>
    
         <properties>
             <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
             <h2.version>2.2.220</h2.version>
             <!-- Replace with your Java version -->
             <java.version>21</java.version>
             <imageName>h2example</imageName>
             <mainClass>org.graalvm.example.H2Example</mainClass>
         </properties>
    
         <dependencies>
             <!-- 1. H2 Database dependency -->
             <dependency>
                 <groupId>com.h2database</groupId>
                 <artifactId>h2</artifactId>
                 <version>${h2.version}</version>
             </dependency>
         </dependencies>
         <!-- 2. Native Image Maven plugin within a Maven profile -->
         <profiles>
             <profile>
                 <id>native</id>
                 <build>
                     <plugins>
                         <plugin>
                             <groupId>org.graalvm.buildtools</groupId>
                             <artifactId>native-maven-plugin</artifactId>
                             <version>0.9.28</version>
                             <extensions>true</extensions>
                             <executions>
                                 <execution>
                                     <id>build-native</id>
                                     <goals>
                                         <goal>compile-no-fork</goal>
                                     </goals>
                                     <phase>package</phase>
                                 </execution>
                             </executions>
                             <configuration>
                                 <buildArgs>
                                     <!-- 3. Quick build mode -->
                                     <buildArg>-Ob</buildArg>
                                 </buildArgs>
                             </configuration>
                         </plugin>
                     </plugins>
                 </build>
             </profile>
         </profiles>
         <build>
             <finalName>${project.artifactId}</finalName>
             <plugins>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-surefire-plugin</artifactId>
                     <version>3.0.0-M5</version>
                 </plugin>
    
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-compiler-plugin</artifactId>
                     <version>3.11.0</version>
                     <configuration>
                         <source>${java.version}</source>
                         <target>21</target>
                     </configuration>
                 </plugin>
    
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-jar-plugin</artifactId>
                     <version>3.3.0</version>
                     <configuration>
                         <archive>
                             <manifest>
                                 <addClasspath>true</addClasspath>
                                 <mainClass>${mainClass}</mainClass>
                             </manifest>
                         </archive>
                     </configuration>
                 </plugin>
    
                 <plugin>
                     <groupId>org.codehaus.mojo</groupId>
                     <artifactId>exec-maven-plugin</artifactId>
                     <version>3.1.1</version>
                     <executions>
                         <execution>
                             <id>java</id>
                             <goals>
                                 <goal>java</goal>
                             </goals>
                             <configuration>
                                 <mainClass>${mainClass}</mainClass>
                             </configuration>
                         </execution>
                         <execution>
                             <id>native</id>
                             <goals>
                                 <goal>exec</goal>
                             </goals>
                             <configuration>
                                 <executable>${project.build.directory}/${imageName}</executable>
                                 <workingDirectory>${project.build.directory}</workingDirectory>
                             </configuration>
                         </execution>
                     </executions>
                 </plugin>
             </plugins>
         </build>
    
     </project>
    

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

    2 packageフェーズに添付されたMavenプロファイル内で、ネイティブ・イメージMavenプラグインを有効にします。(Mavenプロファイルを使用して、ネイティブ実行可能ファイルをビルドします。)Mavenプロファイルを使用すると、JARファイルをビルドするか、ネイティブ実行可能ファイルをビルドするかを判断できます。プラグインにより、どのJARファイルをnative-imageに渡す必要があり、実行可能ファイルのメイン・クラスはどのような内容であることが必要かが検出されます。

    3 <buildArgs>セクションを使用して、基礎となるnative-imageビルド・ツールにパラメータを渡すことができます。個々の<buildArg>タグでは、コマンド・ラインの場合とまったく同じ方法でパラメータを渡すことが可能です。クイック・ビルド・モードを有効にする-Obオプション(開発時にのみ推奨)が例として使用されています。その他の構成オプションの詳細は、プラグインのドキュメントを参照してください。

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

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

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

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

  1. pom.xmlを開き、nativeプロファイルの<configuration>要素に次の内容を入力し、GraalVM到達可能性メタデータ・リポジトリを有効にします:
     <metadataRepository>
         <enabled>true</enabled>
     </metadataRepository>
    

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

     <configuration>
         <buildArgs>
             <buildArg>-Ob</buildArg>
         </buildArgs>
         <metadataRepository>
             <enabled>true</enabled>
         </metadataRepository>
     </configuration>
    

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

  2. 次に、プロファイルを使用してネイティブ実行可能ファイルをビルドします(プロファイル名は-Pフラグで指定されています):
     mvn package -Pnative
    

    これにより、プラットフォームのネイティブ実行可能ファイル(h2example)が、targetディレクトリに生成されます。

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

     ./target/h2example 
    

    アプリケーションから、H2データベースに格納されている顧客のリストが返されます。

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

native-imageのメタデータ構成を提供する2つ目の方法は、コンパイル時にトレース・エージェント(以降はエージェント)を注入するものです。エージェントはデフォルトで無効になっていますが、pom.xmlファイルまたはコマンド・ラインで有効にできます。

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

トレース・エージェントでメタデータを収集し、提供された構成を適用するネイティブ実行可能ファイルをビルドする方法を次に示します。

  1. nativeプロファイルの<configuration>要素に次の内容を追加して、エージェントを有効にします:
     <agent>
         <enabled>true</enabled>
     </agent>
    

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

     <configuration>
         <agent>
             <enabled>true</enabled>
         </agent>
         <buildArgs>
             <buildArg>-Ob</buildArg>
         </buildArgs>
         <metadataRepository>
             <enabled>true</enabled>
         </metadataRepository>
     </configuration>
    
  2. エージェントを使用したアプリケーションの実行はより複雑になり、Javaプロセスのフォークを可能にする別のMOJO実行の構成が必要になります。native Mavenプロファイル・セクションで、exec-maven-pluginプラグインを追加します:
     <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>exec-maven-plugin</artifactId>
         <version>3.1.1</version>
         <executions>
             <execution>
                 <id>java-agent</id>
                 <goals>
                     <goal>exec</goal>
                 </goals>
                 <phase>test</phase>
                 <configuration>
                     <executable>java</executable>
                     <workingDirectory>${project.build.directory}</workingDirectory>
                     <arguments>
                         <argument>-classpath</argument>
                         <classpath/>
                         <argument>${mainClass}</argument>
                     </arguments>
                 </configuration>
             </execution>
         </executions>
     </plugin>
    
  3. JVMでエージェントを有効にしてアプリケーションを実行します:
     mvn -Pnative -Dagent=true -DskipTests -DskipNativeBuild=true package exec:exec@java-agent
    

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

  4. エージェントによって収集された構成を使用して、ネイティブ実行可能ファイルをビルドします:
     mvn -Pnative -Dagent=true -DskipTests package exec:exec@native
    

    これにより、プラットフォームのネイティブ実行可能ファイル(h2example)が、targetディレクトリに生成されます。

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

     ./target/h2example 
    
  6. (オプション)プロジェクトをクリーン・アップするには、mvn cleanを実行し、ディレクトリMETA-INFとそのコンテンツを削除します。

サマリー

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

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

mvn package -Pnative