返回

使用 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 可达性元数据存储库跟踪代理构建本机可执行文件。本指南的目标是说明两种方法之间的区别,并演示如何使用可达性元数据可以简化您的开发任务。

我们建议您按照说明一步一步地创建应用程序。或者,您可以直接转到 完成的示例

准备演示应用程序

注意:需要 Java 17 到 20 之间的版本才能执行 Gradle(请参见 Gradle 兼容性矩阵)。但是,如果您想使用 Java 21(或更高版本)运行应用程序,有一个变通方法:将 JAVA_HOME 设置为 Java 17 到 20 之间的版本,并将 GRAALVM_HOME 设置为 JDK 21 的 GraalVM。有关更多详细信息,请参见 Native Image Gradle 插件文档

先决条件

确保您已安装 GraalVM JDK。最简单的入门方法是使用 SDKMAN!。有关其他安装选项,请访问 下载部分

  1. 在您喜欢的 IDE 中使用**Gradle**创建一个名为“H2Example”的新 Java 项目,位于 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 插件。该插件会发现它需要传递给 native-image 的哪些 JAR 文件以及可执行文件的主类应该是哪个。

    2 显式指定应用程序主类。

    3 添加对 H2 数据库(一个用于 Java 的开源 SQL 数据库)的依赖关系。应用程序通过 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 可达性元数据存储库构建本机可执行文件

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 可达性元数据存储库增强了 Native Image 对依赖于第三方库的 Java 应用程序的可用性。

使用跟踪代理构建本机可执行文件

native-image 提供 medatata 配置的第二种方法是在编译时注入 跟踪代理(后面简称代理)。

该代理可以在三种模式下运行

  • 标准:无条件收集元数据。如果您正在构建本机可执行文件,建议使用此模式。
  • 条件:有条件地收集元数据。如果您要为打算进一步使用的本机共享库创建条件元数据,建议使用此模式。
  • 直接:仅供高级用户使用。此模式允许直接控制传递给代理的命令行。

您可以通过传递命令行上的选项或在 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

与我们联系