JVM 的“救世主”来了?---GraalVM 集成入门

要说 JVM 的未来那有很多的可能,但在云原生如日中天、Serverless 日渐成熟、新语言百花齐放的当下,跨语言、Native 支持、高性能低资源占用的技术必定是其璀璨的明珠,而 GraalVM 正是这样一个承载了 JVM 未来,将 Java 带入下一波技术浪潮的弄潮儿,本文我们就来实践下 GraalVM 集成支持。

Java 的问题

在讲 GraalVM 前我们先回看下 Java 当前遇到的问题,概括而言如下:

  1. 云时代的掉队者,由于 Java 启动的高延时、对资源的高占用、导致在 Serverless 及 FaaS 架构下力不从心,在越来越流行的边缘计算、IoT 方向上也是难觅踪影

  2. 系统级应用开发的旁观者,Java 语言在业务服务开发中孤独求败,但在系统级应用领域几乎是 C、C++、搅局者 Go、黑天鹅 Rust 的天下

  3. 移动应用、敏捷应用的追随者,移动应用中 Android 逐步去 Java,前端又是 JS 的世界,敏捷开发方面前有 Ruby、Python 后有 NodeJS

有人说 Java 吃老本,不思进取,也对也不对吧,毕竟 Java 作为企业级软件开发最主流的语言,兼容稳定远胜于创新求变,所以 Java 很苦恼,即便是看似激进的 JDK 版本策略也敌不过臃肿守旧的印象。

怎么办?要兼容稳定,那么别打语法、API、字节码创新的心思,Java 本身就那样了,但它背后的 JVM 却有更多的选择。Java 的问题可以让 JVM 来补救,说资源占用高那先来个 JPMS 模块化(但目前看貌似并不成功),说启动延迟大那咱支持 AoT 搞 Native 吧,说对系统级应用、移动应用、敏捷应用支持不好那你行你上,我把你们都包进来纳入到我 JVM 大生态中,这就是 GraalVM 正在做的。

GraalVM 简述

GraalVM 是一个新的 JVM,原本用于替换 HotSpot 的 C2 编译器,后来独立成 JVM 的一个产品,它很新但架不住对 Native Image、多语言集成、高性能特性的诱惑,就连“后知后觉”的 Spring 也着手相关的支持工作,而新新的框架诸如 quarkus、micronaut 都已提供了比较好的支持。

但是问题来了,你说得这么好,为什么不见人用,国内外找了一圈都是些介绍性的文章?原因嘛,因为 GraalVM 要解决的问题有很多,现有的应用、框架都需要一定的改造。前面扯了这么多,接下来才是本文的重点:以实例切入带各位体验下 GraalVM 的集成改造。

实例:Dew-Common GraalVM 集成

Dew-Common( https://github.com/gudaoxuri/dew-common )是笔者开源的一个 Java 基础工具包,包含了 Json、Bean(反射)、Package Scan、JS 交互、Shell 调用等常用操作的支持,拿这个工具包做 GraalVM 的集成可以比较全面的检验集成的效果。

前置准备

安装 GraalVM 及相关的依赖,GraalVM 支持 Linux、Windows 及 MacOS,但一般推荐在 Linux 下操作。笔者使用的是 Windows,Windows 10 2004 版本的 WSL2 提供了完整的 Linux 内核,非常适合开发调试(Windows 是最好的 Linux 发行版本😂)。

# https://github.com/graalvm/graalvm-ce-builds/releases 下载需要的版本
# 解压,设置PATH/JAVA_HOME
# 添加多语言环境(可选,gu是GraalVM Updater的命令)
# 没有JS?因为JS内置了,GraalVM带了Node环境,支持各类常用的NPM包!
gu install ruby
gu install r
gu install python
gu install wasm
# 添加Native Image(可选,一般都会安装,这是GraalVM的一大亮点)
gu install native-image
gu install llvm-toolchain

Note:详见 https://www.graalvm.org/getting-started/#install-graalvm

POM 改造

<dependencies>
    <dependency> (1)
        <groupId>org.graalvm.sdk</groupId>
        <artifactId>graal-sdk</artifactId>
        <version>${graalvm.version}</version>
        <scope>provided</scope>
    </dependency>
    <!-- HotSpot 兼容处理 --> (2)
    <dependency>
        <groupId>org.graalvm.truffle</groupId>
        <artifactId>truffle-api</artifactId>
        <version>${graalvm.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.graalvm.js</groupId>
        <artifactId>js</artifactId>
        <version>${graalvm.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.graalvm.js</groupId>
        <artifactId>js-scriptengine</artifactId>
        <version>${graalvm.version}</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>
...
<profiles>
    <profile>
        <id>native</id> (3)
        <dependencies>
            <!-- 去除 HotSpot 兼容处理 --> (4)
            <dependency>
                <groupId>org.graalvm.truffle</groupId>
                <artifactId>truffle-api</artifactId>
                <version>${graalvm.version}</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.graalvm.js</groupId>
                <artifactId>js</artifactId>
                <version>${graalvm.version}</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.graalvm.js</groupId>
                <artifactId>js-scriptengine</artifactId>
                <version>${graalvm.version}</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId> (5)
                    <configuration>
                        <argLine>
                            -agentlib:native-image-agent=access-filter-file=${project.basedir}/src/main/resources/META-INF/native-image/com.ecfront.dew/common/agent-access-filter.json,config-output-dir=${project.basedir}/src/main/resources/META-INF/native-image/com.ecfront.dew/common/
                        </argLine>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.graalvm.nativeimage</groupId>
                    <artifactId>native-image-maven-plugin</artifactId> (6)
                    <version>${graalvm.version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>native-image</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                    <configuration>
                        <skip>false</skip>
                        <mainClass>${mainClass}</mainClass>
                        <imageName>${imageName}</imageName>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>
  1. 一般情况下我们不需要引入额外的依赖,但如果需要执行跨语言操作就必须引入 graal-sdk 依赖,该依赖提供了 GraalVM 特有的语法 API,注意 scope 为 provided,即它只作用于编译、测试阶段,运行时不需要

  2. 下面的几个包是用于跨语言操作的兼容处理,在 GraalVM 环境不需要,但在 HotSpot 必须引入

  3. 使用特定的 profile 执行 Native Image 打包操作

  4. Native Image 由 GraalVM 的 SubstrateVM(定制轻量 VM)运行,不需要第 2 步引入的兼容依赖,所以这里做了排除

  5. 调用 mvn test 附加执行参数,用于 Native Image 动态调用的代码收集,后文会细讲

  6. Native Image 打包的核心插件,这里需要指定 main 方法,可指定镜像的名称

Tip:GraalVM 没有集成 javax 包,所以如果需要诸如 validation 注解则需要手工引入 jakarta.validation-api 依赖

小结如下:

  1. 只是将程序运行在 GraalVM 下,那么只要把 GraalVM 缺失的依赖(如上面说的 javax 包)引入即可

  2. 要做跨语言操作,那么完成第 1、2 步骤即可

  3. 要支持 Native Image 则必须完成后续的步骤

跨语言调用

JSR 223 规范下脚本调用

private static final ScriptEngineManager SCRIPT_ENGINE_MANAGER = new ScriptEngineManager(); (1)
private Invocable invocable;
private ScriptHelper(Invocable invocable) {
    this.invocable = invocable;
}
/**
 * Build script helper.
 *
 * @param jsFunsCode    the js funs code
 * @param addCommonCode the add common code
 * @return the script helper
 * @throws RTScriptException the rt script exception
 */
public static ScriptHelper build(String jsFunsCode, boolean addCommonCode) throwsRTScriptException {
    Compilable jsEngine = (Compilable) SCRIPT_ENGINE_MANAGER.getEngineByName("nashorn");
    if (addCommonCode) {
        jsFunsCode = "var $ = Java.type('com.ecfront.dew.common.$');\r\n" + jsFunsCode; (2)
    }
    try {
        CompiledScript script = jsEngine.compile(jsFunsCode);
        script.eval();
        return new ScriptHelper((Invocable) script.getEngine());
    } catch (ScriptException e) {
        throw new RTScriptException(e);
    }
}
/**
 * Execute.
 *
 * @param <T>       the type parameter
 * @param jsFunName the js fun name
 * @param args      the args
 * @return the t
 * @throws RTScriptException              the rt script exception
 * @throws RTReflectiveOperationException the rt reflective operation exception
 */
public <T> T execute(String jsFunName, Object... args) throws RTScriptException, RTReflectiveOperationException {
    try {
        return (T) invocable.invokeFunction(jsFunName, args);
    } catch (ScriptException e) {
        throw new RTScriptException(e);
    } catch (NoSuchMethodException e) {
        throw new RTReflectiveOperationException(e);
    }
}
  1. 用 ScriptEngineManager 定义脚本引擎管理器

  2. 添加对 Java 方法的调用支持

上面是 JSR 223 规范下的使用方式,使用了 nashorn 引擎,但在 JDK11 下已经标记过时,后期会移除,为什么移除?自然是因为了有 GraalVM,Java 官方也推荐使用 GraalVM 运行脚本。

GraalVM 下的脚本调用

    private final Context context;
    private final ScriptKind scriptKind;

    /**
     * Build script helper.
     *
     * @param scriptKind     the script kind
     * @param scriptFunsCode the script funs code
     * @param addCommonCode  the add common code
     * @return the script helper
     * @throws RTScriptException the rt script exception
     */
    public static ScriptHelper build(ScriptKind scriptKind, String scriptFunsCode, boolean addCommonCode) throws RTScriptException {
        try {
            Context context = Context.newBuilder().allowAllAccess(true).build();
            if (addCommonCode) {
                switch (scriptKind) {
                    case JS:
                        scriptFunsCode = "const $ = Java.type('com.ecfront.dew.common.$')\r\n" + scriptFunsCode;
                        break;
                    case PYTHON:
                        // ...
                    default:
                        throw new RTScriptException("Script kind {" + scriptKind.toString() + "} NOT exist.");
                }
            }
            context.eval(Source.newBuilder(scriptKind.toString(), scriptFunsCode, "src.js").build());
            return new ScriptHelper(context, scriptKind);
        } catch (IOException e) {
            throw new RTScriptException(e);
        }
    }

    /**
     * Execute.
     *
     * @param <T>         the type parameter
     * @param funName     the fun name
     * @param returnClazz the return clazz
     * @param args        the args
     * @return the t
     */
    public <T> T execute(String funName, Class<T> returnClazz, Object... args) {
        return context.getBindings(scriptKind.toString()).getMember(funName).execute(args).as(returnClazz);
    }

在语法层面变动比较大,但套路类似。

Note:Dew-Common 相关代码详见 https://github.com/gudaoxuri/dew-common/blob/master/src/main/java/com/ecfront/dew/common/ScriptHelper.java

Note:GraalVM Polyglot 详见 https://www.graalvm.org/docs/reference-manual/polyglot/

Classpath 相关

当我们打包成 Native Image 时 GraalVM 内置的 SubstrateVM 对 Classpath 相关的操作需要注意,这里举几个例子:

# 正常应该是当前的classpath,但输出为null
ClassLoader.getSystemResource("") > null
# 对于Jar包外打印,正常应该为当前的classpath,Native Image与Jar内打印一样,输出为空
new File("").getPath() >
# XX为某些Class
# 对于Jar包外打印,正常应该为当前的classpath
# 对于jar包内打印,正常应该是当前的Jar路径,但输出的是Native Image文件路径
XX.class.getProtectionDomain().getCodeSource().getLocation().getPath() > /mnt/c/Users/i/OneDrive/workspaces/1.personal/dew/dew-common/it/target/NativeImageTest
# 正常应该是当前的classpath,但输出为null
Thread.currentThread().getContextClassLoader().getResource("") > null
我们还需要注意在 Native Image 中好像没有 package 的概念( https://github.com/oracle/graal/issues/1108 ),导致我们无法对“jar 包”做遍历,如 https://github.com/gudaoxuri/dew-common/blob/master/src/main/java/com/ecfront/dew/common/ClassScanHelper.java 下的 scan 就无法实现。

反射处理

看过 GraalVM 介绍的话大家都应该知道 Native Image 是基于静态代码可达分析,而对于反射方法的操作是无法自动发现的。这个影响很大,比如我们常用的 BeanCopy、Json 与 Java 对象的互转、动态代理等会有不同程度的限制。

这些动态调用需要我们来告诉 GraalVM,GraalVM 为我们提供了一个 agent 用于运行期自动收集相关的数据,收集时要确保所有动态调用都被执行到。

下面以 Dew-Common 为例子说明下如何操作:

  1. 所有相关的代码都写成单元测试

  2. 配置 Native Image 到/src/main/resources/META-INF/native-image/com.ecfront.dew/common/native-image.properties(InfoQ 显示有问题,详见:https://www.idealworld.group/2020/06/12/getting-started-with-graalvm/

  3. 配置 Agent 的过滤器到/src/main/resources/META-INF/native-image/com.ecfront.dew/common/agent-access-filter.json(InfoQ 显示有问题,详见:https://www.idealworld.group/2020/06/12/getting-started-with-graalvm/

  4. 为单元测试添加参数,更完整的见 POM改造 章节

  5. 运行单元测试

上面的操作会在调用 mvn test -P native 后会把单元测试收集的包含反射、代理等动态操作写入 config-output-dir 指定的目录下。

这样我们可以配置 native-image-maven-plugin (见 POM 改造章节) , 该插件默认会去 META-INF/native-image/<groupId>/<artifactId>/ 找对应的 Native Image 配置及 Agent 收集信息,调用该插件 mvn package -P native 完成 Native Image 打包。

测试

经过上述操作,只要单元测试覆盖全面那么 Native Image 应该就可以正常工作了,但作为类库,我们还需要有集成测试以确保符合我们的预期。相关的操作可参见 Dew-Common it 目录下的测试工程。

总结

本文简单地介绍了 GraalVM 的使用,但 GraalVM 的 Native Image 目前并不完善,比如对 Spring 的支持还很有限,Spring 有对应的 spring-graalvm-native ( https://github.com/spring-projects-experimental/spring-graalvm-native )工程,该工程还没有 Release,问题很多。不过在今年晚些时候应该可以 Ready,届时我们再一起体现下 Spring Native 的魅力。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

力哥讲技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值