Gradleを使用したJavaアプリケーションからのネイティブ実行可能ファイルのビルド

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

GraalVMネイティブ・イメージのGradleプラグインは、applicationプラグインと連携して、多数のタスクおよび拡張機能を登録します。詳細は、プラグインのドキュメントを参照してください。

このガイドでは、ネイティブ・イメージGradleプラグインを使用して、Javaアプリケーションからネイティブ実行可能ファイルをビルドし、動的機能のサポートを追加して、JUnitテストを実行する方法を示します。

従来のfortune UnixプログラムをシミュレートするFortuneデモ・アプリケーションを使用します。fortuneフレーズのデータは、YourFortuneによって提供されます。

Javaアプリケーションからネイティブ実行可能ファイルをビルドするには、次の2つの方法があります:

手順に従ってステップごとにアプリケーションを作成することをお薦めします。または、既存のプロジェクトを使用することもできます。GraalVMデモ・リポジトリをクローニングし、fortune-demo/gradle/fortuneディレクトリに移動します:

git clone https://github.com/graalvm/graalvm-demos && cd graalvm-demos/fortune-demo/fortune-gradle

ノート: ネイティブ・ビルド・ツールを使用するには、最初にGraalVMをインストールします。GraalVMをインストールする最も簡単な方法は、GraalVM JDKダウンローダ: bash <(curl -sL https://get.graalvm.org/jdk)を使用することです。

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

  1. 次のコマンドを使用して、Gradleで新しいJavaプロジェクトを作成します(または、IDEを使用してプロジェクトを生成することもできます):

     gradle init --project-name fortune --type java-application --package demo --test-framework junit-jupiter --dsl groovy
    
  2. デフォルトのappディレクトリの名前をfortuneに変更し、デフォルトのファイル名App.javaの名前をFortune.javaに変更して、その内容を次のように置き換えます:

     package demo;
    
     import com.fasterxml.jackson.core.JsonProcessingException;
     import com.fasterxml.jackson.databind.JsonNode;
     import com.fasterxml.jackson.databind.ObjectMapper;
    
     import java.io.BufferedReader;
     import java.io.IOException;
     import java.io.InputStream;
     import java.io.InputStreamReader;
     import java.nio.charset.StandardCharsets;
     import java.util.ArrayList;
     import java.util.Iterator;
     import java.util.Random;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    
     public class Fortune {
    
         private static final Random RANDOM = new Random();
         private final ArrayList<String> fortunes = new ArrayList<>();
    
         public Fortune() throws JsonProcessingException {
             // Scan the file into the array of fortunes
             String json = readInputStream(ClassLoader.getSystemResourceAsStream("fortunes.json"));
             ObjectMapper omap = new ObjectMapper();
             JsonNode root = omap.readTree(json);
             JsonNode data = root.get("data");
             Iterator<JsonNode> elements = data.elements();
             while (elements.hasNext()) {
                 JsonNode quote = elements.next().get("quote");
                 fortunes.add(quote.asText());
             }
         }
    
         private String readInputStream(InputStream is) {
             StringBuilder out = new StringBuilder();
             try (InputStreamReader streamReader = new InputStreamReader(is, StandardCharsets.UTF_8);
                  BufferedReader reader = new BufferedReader(streamReader)) {
                 String line;
                 while ((line = reader.readLine()) != null) {
                     out.append(line);
                 }
    
             } catch (IOException e) {
                 Logger.getLogger(Fortune.class.getName()).log(Level.SEVERE, null, e);
             }
             return out.toString();
         }
    
         public String randomFortune() {
             //Pick a random number
             int r = RANDOM.nextInt(fortunes.size());
             //Use the random number to pick a random fortune
             return fortunes.get(r);
         }
    
         private void printRandomFortune() throws InterruptedException {
             String f = randomFortune();
             // Print out the fortune s.l.o.w.l.y
             for (char c : f.toCharArray()) {
                 System.out.print(c);
                 Thread.sleep(100);
             }
             System.out.println();
         }
    
         /**
          * @param args the command line arguments
          */
         public static void main(String[] args) throws InterruptedException, JsonProcessingException {
             Fortune fortune = new Fortune();
             fortune.printRandomFortune();
         }
     }
    
  3. fortune/src/test/javaディレクトリを削除します。後の段階でテストを追加します。

  4. 次のファイルfortunes.jsonをコピーして、fortune/src/main/resources/の下に貼り付けます。プロジェクト・ツリーは次のようになります:

     .
     ├── fortune
     │   ├── build.gradle
     │   └── src
     │       ├── main
     │       │   ├── java
     │       │   │   └── demo
     │       │   │       └── Fortune.java
     │       │   └── resources
     │       │       └── fortunes.json
     │       └── test
     │           └── resources
     ├── gradle
     │   └── wrapper
     │       ├── gradle-wrapper.jar
     │       └── gradle-wrapper.properties
     ├── gradlew
     ├── gradlew.bat
     └── settings.gradle
    
  5. Gradle構成ファイルbuild.gradleを開き、applicationセクションのメイン・クラスを更新します:

     application {
         mainClass = 'demo.Fortune'
     }
    
  6. JSON、データ・バインディング(デモ・アプリケーションで使用)の読取りおよび書込み機能を提供する明示的なFasterXML Jackson依存性を追加します。build.gradledependenciesセクションに次の3行を挿入します:

     implementation 'com.fasterxml.jackson.core:jackson-core:2.13.2'
     implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2'
     implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.2'
    

    また、使用されないguavaに対する依存性も削除します。

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

  7. ネイティブ・イメージGradleプラグインを登録します。プロジェクトのbuild.gradleファイルのpluginsセクションに次を追加します:

     plugins {
     // ...
    
     id 'org.graalvm.buildtools.native' version '0.9.18'
     }
    

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

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

     pluginManagement {
         repositories {
             mavenCentral()
             gradlePluginPortal()
         }
     }
    
     rootProject.name = 'fortune-parent'
     include('fortune')
    

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

リソース自動検出を使用したネイティブ実行可能ファイルのビルド

./gradlew nativeCompileを実行してネイティブ実行可能ファイルをビルドすることも、./gradlew nativeRunを起動して直接実行することもできます。ただし、この段階でネイティブ実行可能ファイルの実行は失敗します。このアプリケーションでは追加のメタデータが必要であるためです。ロードするリソースのリストを指定する必要があります。

  1. ネイティブ実行可能ファイルに含めるリソースを自動的に検出するようにプラグインに指示します。これをbuild.gradleファイルに追加します:

     graalvmNative {
         binaries.all {
             resources.autodetect()
         }
         toolchainDetection = false
     }
    

    ここで注意すべきもう1つの点として、Gradleの制限のため、プラグインがGraalVMインストールを正しく検出できない場合があります。デフォルトでは、プラグインはJava 11 GraalVM Community Editionを選択します。GraalVM Enterprise、または特定のバージョンのGraalVMおよびJavaを使用する場合は、プラグインの構成を明示的に指定する必要があります。たとえば:

     graalvmNative {
         binaries {
             main {
                 javaLauncher = javaToolchains.launcherFor {
                     languageVersion = JavaLanguageVersion.of(8)
                     vendor = JvmVendorSpec.matching("GraalVM Community")
                 }
             }
         }
     }
    

    この回避策は、toolchainDetection = falseコマンドを使用してツールチェーンの検出を無効にすることです。

  2. プロジェクトをコンパイルし、1ステップでネイティブ実行可能ファイルをビルドします:

     ./gradlew nativeRun
    

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

  3. ネイティブ実行可能ファイルを実行します:

    ./fortune/build/native/nativeCompile/fortune
    

    アプリケーションが起動し、ランダムな引用文が出力されます。

バイナリに含めるリソース(resources.autodetect())を自動的に検出するようにgraalvmNativeプラグインを構成することは、この例を動作させる1つの方法です。resources.autodetect()ソリューションを使用すると、アプリケーションはsrc/main/resourcesの場所で直接使用可能なリソース(fortunes.json)を使用するため、機能します。

しかし、このガイドでは、デモのために、エージェントを使用して同じことを実行できるということを説明します。

エージェントを使用したリソースの検出によるネイティブ実行可能ファイルのビルド

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

追加した構成ブロックはリソースの検出を処理しますが、必要以上のものが追加される可能性があり、動的プロキシなどのより高度なユースケースを処理できない場合があります。このアプローチを実証するには、resources.autodetect()構成ブロックを削除します。

次のステップでは、エージェントを使用してメタデータを収集し、そのメタデータを使用してネイティブ実行可能ファイルをビルドする方法を示します。

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

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

     ./gradlew metadataCopy --task run --dir src/main/resources/META-INF/native-image
    
  3. Gradleでエージェントによって取得されたメタデータを使用してネイティブ実行可能ファイルをビルドします。

     ./gradlew nativeCompile
    

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

  4. ネイティブ実行可能ファイルを実行します:

     ./fortune/build/native/nativeCompile/fortune
    

    アプリケーションが起動し、ランダムな引用文が出力されます。

アプリケーションをネイティブ実行可能ファイルとして実行する利点を確認するには、所要時間timeを確認し、Javaアプリケーションとして実行した場合と結果を比較します。

プラグインをカスタマイズできます。たとえば、ネイティブ実行可能ファイルの名前を変更し、次のようにbuild.gradleファイル内のプラグインに追加のパラメータを渡します:

graalvmNative {
    binaries {
        main {
            imageName.set('fortuneteller') 
            buildArgs.add('--verbose') 
        }
    }
}

このとき、ネイティブ実行可能ファイルはfortunetellerと呼ばれます。buildArgs.add構文を使用して、native-imageツールに追加の引数を渡す方法に注意してください。

JUnitテストの追加

GraalVMネイティブ・イメージのGradleプラグインは、ネイティブ実行可能ファイルに対してJUnitプラットフォーム・テストを実行できます。これは、テストがコンパイルされ、ネイティブ・コードとして実行されることを意味します。

  1. fortunate/src/test/java/demo/FortuneTest.javaファイルに次のテストを作成します:

     package demo;
    
     import com.fasterxml.jackson.core.JsonProcessingException;
     import org.junit.jupiter.api.DisplayName;
     import org.junit.jupiter.api.Test;
    
     import static org.junit.jupiter.api.Assertions.assertTrue;
    
     class FortuneTest {
         @Test
         @DisplayName("Returns a fortune")
         void testItWorks() throws JsonProcessingException {
             Fortune fortune = new Fortune();
             assertTrue(fortune.randomFortune().length()>0);
         }
     }
    
  2. JUnitテストを実行します:

     ./gradlew nativeTest
    

    このプラグインは、ネイティブ実行可能ファイルからテストを実行する前に、JVMでテストを実行します。テスト・サポート(デフォルト)を無効にするには、次の構成をbuild.gradleファイルに追加します:

     graalvmNative {
         testSupport = false
     }
    

エージェントを使用したテストの実行

エージェントによるメタデータの収集をテストする必要がある場合は、testおよびnativeTestタスク呼出しに-Pagentオプションを追加します:

  1. エージェントを使用してJVMでテストを実行します:

     ./gradlew -Pagent test
    

    エージェントを使用してJVM上でアプリケーションを実行し、メタデータを収集してnative-imageでのテストに使用します。生成された構成ファイル(メタデータを含む)は、${buildDir}/native/agent-output/${taskName}ディレクトリにあります。この場合、たとえばbuild/native/agent- output/testになります。また、ネイティブ・イメージGradleプラグインは、エージェント・オプションの{output_dir}を置き換えて、このディレクトリを指すようにします。

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

     ./gradlew -Pagent nativeTest
    

サマリー

ネイティブ・イメージGradleプラグインには、さらに多くの構成オプションがあります。詳細は、プラグインのドキュメントを参照してください。

アプリケーションが実行時に動的にクラスを呼び出さない場合、エージェントによる実行は不要です。その場合、ワークフローは次のようになります:

./gradlew nativeRun

最後に、JAVA_HOME環境としてGraalVM Enterpriseを使用する場合、プラグインはエンタープライズ機能を有効にしてネイティブ実行可能ファイルをビルドします。