JVM 的未来——GraalVM 集成入门

用户头像
孤岛旭日
关注
发布于: 2020 年 06 月 15 日
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的魅力。



关注我的公众号:



发布于: 2020 年 06 月 15 日 阅读数: 83
用户头像

孤岛旭日

关注

努力成为一个好爸爸、好丈夫。 2018.10.30 加入

10年前,毕业时,芳华正茂,立志Coding完美世界,10年后,35,志向未成但理想依旧,勉励之、践行之。

评论

发布
暂无评论
JVM的未来——GraalVM集成入门