返回

使用 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 需要 Java 17 到 21 之间的版本(参见 Gradle 兼容性矩阵)。但是,如果您想在 Java 23(或更高版本)上运行您的应用程序,有一个变通方法:将 JAVA_HOME 设置为 Java 17 到 21 之间的版本,并将 GRAALVM_HOME 设置为适用于 JDK 23 的 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 {
             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);
    
             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<>();
             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. 打开 Gradle 配置文件 build.gradle,并将其内容替换为以下内容
     plugins {
         id 'application'
         // 1. Native Image Gradle plugin
         id 'org.graalvm.buildtools.native' version '0.10.3'
     }
    
     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 {
         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() 用于指定生成二进制文件的名称。从插件文档中了解其他配置选项。

  4. 该插件尚未在 Gradle 插件门户上提供,因此请声明一个额外的插件仓库。打开 settings.gradle 文件,将其默认内容替换为以下内容
     pluginManagement {
         repositories {
             mavenCentral()
             gradlePluginPortal()
         }
     }
    
     rootProject.name = 'H2Example'
     include('H2Example')
    

    请注意,pluginManagement {} 块必须出现在文件中任何其他语句之前。

  5. (可选)构建并运行应用程序
    gradle run
    

    这会生成一个可运行的 JAR 文件,该文件返回存储在 H2 数据库中的客户列表。

使用 GraalVM 可达性元数据仓库构建原生可执行文件

GraalVM 可达性元数据仓库 为默认不支持 GraalVM Native Image 的库提供了 GraalVM 配置。本应用程序所依赖的 H2 数据库 就是其中之一。

Native Image Gradle 插件在构建时自动从仓库下载元数据

使用 Gradle,您可以一步构建并运行原生可执行文件

gradle nativeRun

名为 h2example 的原生可执行文件在 build/native/nativeCompile 目录中创建。该命令还会从该原生可执行文件运行应用程序。

使用追踪代理构建原生可执行文件

native-image 提供元数据配置的第二种方法是在编译时注入 追踪代理(后文简称 代理)。

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

  • 标准模式:无条件收集元数据。如果您正在构建原生可执行文件,建议使用此模式。
  • 条件模式:有条件地收集元数据。如果您正在为未来使用的原生共享库创建条件元数据,建议使用此模式。
  • 直接模式:仅适用于高级用户。此模式允许直接控制传递给代理的命令行。

您可以通过在命令行上传递选项或在 build.gradle 文件中配置代理。请参阅下文,了解如何使用代理收集元数据并构建原生可执行文件。

  1. 打开 build.gradle 文件,并在 graalvmNative 块中添加代理配置
     agent {
         defaultMode = "standard"
     }
    

    它定义了代理应运行的模式。如果您更喜欢命令行选项,它是 -Pagent=standard

  2. 现在,在 JVM 上使用代理运行您的应用程序。要使用 Native Image Gradle 插件启用代理,请将 -Pagent 选项传递给任何扩展 JavaForkOptions 的 Gradle 任务(例如,testrun
    gradle -Pagent run
    

    代理捕获并记录了在测试运行期间遇到的对 H2 数据库的所有调用以及所有动态特性,并将其写入 /build/native/agent-output/run 目录中的 JSON 文件。

  3. 收集元数据后,使用 metadataCopy 任务将其复制到项目的 /META-INF/native-image/ 目录中
     gradle metadataCopy --task run --dir src/main/resources/META-INF/native-image
    

    输出目录不强制要求但建议为 /resources/META-INF/native-image/native-image 工具会自动从该位置获取元数据。有关如何自动收集应用程序元数据的更多信息,请参阅 自动收集元数据

  4. 使用代理收集的配置构建原生可执行文件
     gradle nativeRun
    

    该命令还会运行应用程序。

  5. (可选)要清理项目,请运行 gradle clean,并删除 META-INF 目录及其内容。

总结

本指南演示了如何使用 GraalVM 可达性元数据仓库 和追踪代理构建原生可执行文件。目标是展示它们之间的区别,并证明使用可达性元数据如何简化工作。使用 GraalVM 可达性元数据仓库增强了 Native Image 对依赖第三方库的 Java 应用程序的可用性。

联系我们