返回

使用 Native Image Gradle 外掛程式包含可達性中繼資料

您可以使用 Gradle 從 Java 應用程式建置原生可執行檔。為此,請使用 Native Build Tools 專案中提供的 GraalVM Native Image Gradle 外掛程式。

「真實世界」的 Java 應用程式可能需要一些 Java 反射物件,或呼叫一些原生程式碼,或存取類別路徑上的資源 - native-image 工具必須在建置時知道的動態功能,並以中繼資料的形式提供。(Native Image 在建置時動態載入類別,而不是在執行時載入。)

根據您的應用程式相依性,有三種方法可以使用 Native Image Gradle 外掛程式提供中繼資料

  1. 使用 GraalVM 可達性中繼資料儲存庫
  2. 使用追蹤代理程式
  3. 自動偵測(如果所需資源直接位於類別路徑上,則在 src/main/resources/ 目錄中)

本指南示範如何使用 GraalVM 可達性中繼資料儲存庫,以及 追蹤代理程式,建置原生可執行檔。本指南的目標是說明這兩種方法之間的差異,並示範使用可達性中繼資料如何簡化您的開發工作。

我們建議您按照說明逐步建立應用程式。或者,您可以直接前往完成的範例

準備示範應用程式

注意:執行 Gradle 需要 17 到 20 之間的 Java 版本(請參閱 Gradle 相容性矩陣)。但是,如果您想使用 Java 21(或更高版本)執行您的應用程式,有一個解決方法:將 JAVA_HOME 設定為 17 到 20 之間的 Java 版本,並將 GRAALVM_HOME 設定為適用於 JDK 21 的 GraalVM。有關更多詳細資訊,請參閱 Native Image Gradle 外掛程式文件

先決條件

請確保您已安裝 GraalVM JDK。入門最簡單的方式是使用 SDKMAN!。如需其他安裝選項,請造訪下載區段

  1. 在您最喜歡的 IDE 中使用 Gradle 建立一個新的 Java 專案,稱為「H2Example」,在 org.graalvm.example 套件中。

  2. 將預設的 app/ 目錄重新命名為 H2Example/,然後將預設檔案名稱 App.java 重新命名為 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;
         }
     }
    
  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 啟用 Native Image Gradle 外掛程式。此外掛程式會探索哪些 JAR 檔案需要傳遞給 native-image,以及可執行檔的主要類別應該是什麼。

    2 明確指定應用程式的主要類別。

    3 加入 H2 資料庫 的相依性,這是 Java 的開放原始碼 SQL 資料庫。應用程式透過 JDBC 驅動程式與此資料庫互動。

    4 您可以在 graalvmNative 外掛程式組態中將參數傳遞給 native-image 工具。在個別的 buildArgs 中,您可以完全以從命令列執行的方式傳遞參數。啟用快速建置模式(僅建議在開發期間使用)的 -Ob 選項即為一個範例。imageName.set() 用於指定產生的二進位檔的名稱。從外掛程式的文件中瞭解其他組態選項。

  5. 此外掛程式尚未在 Gradle 外掛程式入口網站上提供,因此請宣告一個額外的外掛程式儲存庫。開啟 settings.gradle 檔案,並將預設內容取代為以下內容
     pluginManagement {
         repositories {
             mavenCentral()
             gradlePluginPortal()
         }
     }
    
     rootProject.name = 'H2Example'
     include('H2Example')
    

    請注意,pluginManagement {} 區塊必須出現在檔案中的任何其他陳述式之前。

  6. (選用)建置應用程式。從儲存庫的根目錄中,執行以下命令
    ./gradlew run
    

    這會產生一個「可執行」JAR 檔案,其中包含應用程式的所有相依性,以及一個正確設定的 MANIFEST 檔案。

使用 GraalVM 可達性中繼資料儲存庫建置原生可執行檔

Native Image Gradle 外掛程式提供對 GraalVM 可達性中繼資料儲存庫的支援。此儲存庫為預設情況下不支援 GraalVM Native Image 的程式庫提供 GraalVM 組態。其中之一是此應用程式所依賴的 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 的可用性。

使用追蹤代理程式建置原生可執行檔

native-image 提供中繼資料組態的第二種方法是在編譯時注入 追蹤代理程式(以下稱為代理程式)。

代理程式可以在三種模式下執行

  • 標準:無條件收集中繼資料。如果您要建置原生可執行檔,建議使用此模式。
  • 條件式:有條件地收集中繼資料。如果您要為用於進一步使用的原生共用程式庫建立條件式中繼資料,建議使用此模式。
  • 直接:僅限進階使用者使用。此模式允許直接控制傳遞給代理程式的命令列。

您可以透過在命令列上傳遞選項或在 build.gradle 檔案中組態代理程式。請參閱下文如何使用追蹤代理程式收集中繼資料,並建置套用所提供組態的原生可執行檔。

  1. 開啟 build.gradle 檔案,並查看 graalvmNative 外掛程式組態中指定的代理程式模式
     graalvmNative {
         agent {
             defaultMode = "standard"
         }
         ...
     }    
    

    如果您偏好使用命令列選項,則是 -Pagent=standard

  2. 現在在 JVM 上使用代理程式執行您的應用程式。若要使用 Native Image Gradle 外掛程式啟用代理程式,請將 -Pagent 選項傳遞給任何擴充 JavaForkOptions 的 Gradle 工作(例如,testrun
    ./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

與我們聯繫