GraalVM - 可达元数据(反射/动态资源获取)

反射

JVM 的动态语言功能(包括反射和资源处理)在运行时计算动态访问的程序元素,例如调用的方法或资源 URL。native-image工具在构建本机二进制文件时执行静态分析以确定这些动态功能,但它无法始终详尽地预测所有用途。为了确保将这些元素包含在本机二进制文件中,您应该向构建器提供可访问性元数据(在下文中称为元数据native-image。向构建器提供可访问性元数据还可确保在运行时与第三方库无缝兼容。

package org.example;

import java.io.File;
import java.io.IOException;

public class Main {
    static class Father {
        public static void greet() {
            System.out.println("Hello, Father?");
        }
    }

    static class Son {
        public static void greet() {
            System.out.println("Hello, Son?");
        }
    }

    public static void main(String[] args) {
        Father.greet();

        try {
            var fatherClass = fetchFather();
            System.out.println(fatherClass);
            var method = fatherClass.getMethod("greet");
            method.invoke(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Class<Father> fetchFather() throws ClassNotFoundException {
        return (Class<Father>) Class.forName("org.example.Main$Father");
    }
}

例如上方代码,我们通过反射获取了org.example.Main$Father ,这段代码无需手动配置反射元数据,因为反射获取的是常量,在构建本机二进制文件并将其存储在其初始堆中时,将Class.forName("Father")计算为常量。如果该类不存在,则调用将转换为。Foothrow ClassNotFoundException("Father") ,说人话就是,他会在编译期就帮你反射出来,无需手动反射,这大大提高了反射效率,不会出现反射查表,JVM为什么没有推广这个操作呢?

常量反射分析作用域

以下的反射操作都会被GraalVM进行常量反射分析,避免运行时反射(或许他就不能运行时反射):

  • java.lang.Class: getField, getMethod, getConstructor, getDeclaredField, getDeclaredMethod, getDeclaredConstructor, forName, getClassLoader

  • java.lang.invoke.MethodHandles: publicLookup, privateLookupIn, arrayConstructor, arrayLength, arrayElementGetter, arrayElementSetter, arrayElementVarHandle, byteArrayViewVarHandle, byteBufferViewVarHandle, lookup

  • java.lang.invoke.MethodHandles.Lookup: in , findStatic , findVirtual , findConstructor , findClass , accessClass , findSpecial , findGetter , findSetter , findVarHandle , findStaticGetter , findStaticSetter , findStaticVarHandle , unreflect , unreflectSpecial , unreflectConstructor , unreflectGetter , unreflectSetter , unreflectVarHandle

  • java.lang.invoke.MethodType: methodType, genericMethodType, changeParameterType, insertParameterTypes, appendParameterTypes, replaceParameterTypes, dropParameterTypes, changeReturnType, erase, generic, wrap, unwrap, parameterType, parameterCount, returnType, lastParameterType

Class<?>[] params0 = new Class<?>[]{String.class, int.class};
Integer.class.getMethod("parseInt", params0);
Class<?>[] params1 = new Class<?>[2];
params1[0] = Class.forName("java.lang.String");
params1[1] = int.class;
Integer.class.getMethod("parseInt", params1);
Class<?>[] params2 = {String.class, int.class};
Integer.class.getMethod("parseInt", params2);

以上三种操作,传递常量数组时,从构建器的角度来看,声明和填充数组的方法是等效的。

动态反射

接下来我们将代码修改为

package org.example;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
    static class Father {
        public static void greet() {
            System.out.println("Hello, Father?");
        }
    }

    static class Son {
        public static void greet() {
            System.out.println("Hello, Son?");
        }
    }

    public static void main(String[] args) {
        Father.greet();

        try {
            var fatherClass = fetchFather();
            System.out.println(fatherClass);
            var method = fatherClass.getMethod("greet");
            method.invoke(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String readFile(String path) {
        try {
            return new String(Files.readAllBytes(Paths.get(path)));
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    private static Class<Father> fetchFather() throws ClassNotFoundException {
        return (Class<Father>) Class.forName(readFile("/home/fuqiuluo/IdeaProjects/GraalVM/ref"));
    }
}

这段代码,ref文件中的内容是org.example.Main$Son 这在JVM是正常运行的。在GraalVM运行则会喜提报错

我们需要创建一个名为reflect-config.json的文件,其内容大概如下:

[
  {
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true,
    "allPublicConstructors": true,
    "allPublicMethods": true,
    "fields": [],
    "methods": [],
    "name": "boolean",
    "unsafeAllocated": false
  },
  {
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true,
    "allPublicConstructors": true,
    "allPublicMethods": true,
    "fields": [],
    "methods": [
      {
        "name": "greet",
        "parameterTypes": []
      }
    ],
    "name": "org.example.Main$Son",
    "unsafeAllocated": false
  },
  {
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true,
    "allPublicConstructors": true,
    "allPublicMethods": true,
    "fields": [],
    "methods": [
      {
        "name": "greet",
        "parameterTypes": []
      }
    ],
    "name": "org.example.Main$Father",
    "unsafeAllocated": false
  }
]

https://github.com/oracle/graalvm-reachability-metadata/tree/master,这里有几乎所有你需要使用的元数据配置大全!

存放路径如上图,META-INF/native-image/<groupId>/<artifactId> 路径下即可,其中native-image.properties里面写入以下内容

Args=-H:ReflectionConfigurationResources=${.}/reflect-config.json \
-H:JNIConfigurationResources=${.}/jni-config.json \
-H:ResourceConfigurationResources=${.}/resource-config.json

有时候会莫名其妙的爆点WARNING出来,添加<arg>-H:+UnlockExperimentalVMOptions</arg> 构建参数即可解决。完成这个配置之后的运行,就不会提示错误了。

Java的Object和其它基础类型的例如Int,Long什么的都是默认加入到反射元数据里面了的(GraalVM 21开始),如果不放心还是可以手写进去...

其它元数据

https://www.graalvm.org/latest/reference-manual/native-image/metadata/

可以去官网看看,这个东西老是改来改去...不知道什么时候稳定...