如何使用graalvm为带有反射功能的java代码生成native image

译自Configure Native Image with the Tracing Agent graal官方文档 , 以下所有命令需要在linux环境下操作,graalvm也支持windows。

要为使用 Java 反射、动态代理对象、JNI 或类路径资源的 Java 应用程序构建本机可执行文件,应为 native-image 工具提供 JSON 格式的配置文件或在代码中预先计算元数据。

您可以手动创建配置文件,但更方便的方法是使用跟踪代理(即Tracing agent,下面用代理一词代称)生成配置。本指南演示如何使用代理进行配置 native-image 。当您在 JVM 上运行应用程序时,代理会自动为您生成配置。

若要了解如何使用代码中预先计算的元数据生成本机可执行文件,请参阅文档 - Reachability Metadata 可访问性元数据

本指南中的示例应用程序使用 Java 反射。该 native-image 工具仅部分检测使用 Java 反射 API 访问的应用程序元素。因此,您需要向它提供有关反射访问的类、方法和字段的详细信息。

不配置Json格式的反射信息示例

  1. 确保您已安装 GraalVM JDK,下载地址如下:https://www.oracle.com/java/technologies/downloads/ 选择graalvm的版本即可

  2. 将以下源代码保存在名为 ReflectionExample.java 的文件中:

    java 复制代码
     import java.lang.reflect.Method;
        
     class StringReverser {
         static String reverse(String input) {
             return new StringBuilder(input).reverse().toString();
         }
     }
        
     class StringCapitalizer {
         static String capitalize(String input) {
             return input.toUpperCase();
         }
     }
        
     public class ReflectionExample {
         public static void main(String[] args) throws ReflectiveOperationException {
             if (args.length == 0) {
                 System.err.println("You must provide the name of a class, the name of its method and input for the method");
                 return;
             }
             String className = args[0];
             String methodName = args[1];
             String input = args[2];
        
             Class<?> clazz = Class.forName(className);
             Method method = clazz.getDeclaredMethod(methodName, String.class);
             Object result = method.invoke(null, input);
             System.out.println(result);
         }
     }

    该Java 应用程序使用命令行参数来确定要执行的反射操作。

  3. 运行如下命令:

    shell 复制代码
    $JAVA_HOME/bin/javac ReflectionExample.java   # 编译
    $JAVA_HOME/bin/java ReflectionExample StringReverser reverse "hello"   # 输出 olleh
    $JAVA_HOME/bin/java ReflectionExample StringCapitalizer capitalize "hello"   # 输出 HELLO
  4. 使用 native-image 命令创建本机可执行文件,如下所示:

    shell 复制代码
     $JAVA_HOME/bin/native-image --no-fallback ReflectionExample
  5. 使用以下命令运行生成的本机可执行文件:

    shell 复制代码
    ./reflectionexample StringReverser reverse "hello"

    您将看到一个异常,类似于:

    log 复制代码
     Exception in thread "main" java.lang.ClassNotFoundException: StringReverser
     at java.lang.Class.forName(DynamicHub.java:1338)
     at java.lang.Class.forName(DynamicHub.java:1313)
     at ReflectionExample.main(ReflectionExample.java:25)

    这表明,根据静态分析,native-image 工具无法确定 StringReverser 类是否被使用,所以未将其包含在本机可执行文件中。

配置Json格式的反射信息示例

以下步骤演示如何使用代理及其输出来创建依赖于反射且需要配置的本机可执行文件。

  1. 在工作目录中创建名为 META-INF/native-image 的目录:

    shell 复制代码
    mkdir -p META-INF/native-image
  2. 在启用代理的情况下运行应用程序生成json配置,如下所示:

    shell 复制代码
    $JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image ReflectionExample StringReverser reverse "hello"

    此命令创建一个名为 reflect-config.json 的文件,其中包含类 StringReverser 的名称及其 reverse() 方法。

    json 复制代码
     [
     {
     "name":"StringReverser",
     "methods":[{"name":"reverse","parameterTypes":["java.lang.String"] }]
     }
    ]
  3. 构建本机可执行文件:

    java 复制代码
    $JAVA_HOME/bin/native-image ReflectionExample

    native-image 工具会自动使用 META-INF/native-image 目录中的配置文件。但是,建议将 META-INF/native-image 目录放到类路径上,可以通过 JAR 文件或使用标志 -cp 。(这样可以避免 IDE 用户在目录结构由 IDE 本身定义时出现混淆。)

  4. 测试可执行文件

    shell 复制代码
    ./reflectionexample StringReverser reverse "hello" # 输出 olleh
    shell 复制代码
    ./reflectionexample StringCapitalizer capitalize "hello"

    执行后会看到一个异常,类似于:

    log 复制代码
     Exception in thread "main" java.lang.ClassNotFoundException: StringCapitalizer
     at java.lang.Class.forName(DynamicHub.java:1338)
     at java.lang.Class.forName(DynamicHub.java:1313)
     at ReflectionExample.main(ReflectionExample.java:25)

    跟踪代理和 native-image 工具都无法确保配置文件完整。当您运行程序时,代理会观察并记录使用反射访问了哪些程序元素。在这种情况下,该 native-image 工具尚未配置为包含对类 StringCapitalizer 的引用。

  5. 更新配置以包含类 StringCapitalizer 。您可以使用以下 config-merge-dir 选项手动编辑 reflect-config.json 文件或重新运行跟踪代理以更新现有配置文件,如下所示:

    shell 复制代码
     $JAVA_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image ReflectionExample StringCapitalizer capitalize "hello"

    此命令更新 reflect-config.json 文件,以包含类 StringCapitalizer 的名称及其 capitalize() 方法。

    json 复制代码
     [
         {
         "name":"StringCapitalizer",
         "methods":[{"name":"capitalize","parameterTypes":["java.lang.String"] }]
         },
         {
         "name":"StringReverser",
         "methods":[{"name":"reverse","parameterTypes":["java.lang.String"] }]
         }
     ]
  6. 重新生成本机可执行文件并运行,不会有报错了。

    shell 复制代码
     $JAVA_HOME/bin/native-image ReflectionExample
    shell 复制代码
     ./reflectionexample StringCapitalizer capitalize "hello"

总结

graalvm让native镜像支持反射的关键是利用json提前告诉它哪些类的哪些方法会被反射调用,然后它就能力在运行时支持反射了。