前言
在热心群友的提示下,入了 GraalVM 的坑。本来以为花个三两天应该就能搞定,没想到这一搞就是一个周,终于取得了阶段性的胜利。
编注:此文为运行包精简主题的一篇,其他精简方法请参见文末。
背景如下:
> 最近由于某些原因,需要做一个自带运行环境的程序。由于各种原因,选定了 Java 和 Python 作为备选语言。但是 Java 由于 JRE 的臃肿(100M+)以及 Spring Boot 的日渐臃肿(helloworld 15M),需要在这两方面进行 size 的缩减。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
环境
- 系统 :Ubuntu 16.04
- GraalVM :GraalVM Community 21.3.0 (Based on OpenJDK 11.0.13)
https://www.graalvm.org/downloads/
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
环境准备
参考:https://www.graalvm.org/docs/getting-started/
- 关键配置
- 环境变量设置
export JAVA_HOME=/path/to/<graalvm> export PATH=/path/to/<graalvm>/bin:$PATH
安装 native-image
gu install native-image
安装 native-image 需要的组件
apt-get install build-essential libz-dev zlib1g-dev
注意 :这里有个其他文章都没有提到的坑。ld 需要更新到 2.26+,不然在构建过程中会报告莫名其妙的异常(这里我耗了大半天)。
apt-get install binutils-2.26
另一个坑
最开始我是在一台 aliyun 的机器上实验的,没有注意到内存问题,在实验过程中遇到异常中断。排查 syslog 发现是 OOMKiller。排查发现我的可用内存只有 4G。
后改为在本机机器 VMware 里的 Ubuntu 操作,内存分配到 8G。经观察,native-image 打包过程中会用到 5.2G 左右的内存,所以这里要注意一下。
至此,环境准备完成。
打包
helloworld 的尝试就跳过了,网上一搜一堆。先了解一下打包命令。
https://www.graalvm.org/uploads/quick-references/native-image-quick-reference-v2_A4.pdf
项目依赖
主要用到了 solon、solon-api、h2、weed3、logback、slf4j、jlhttp 等包。
首先,通过 Maven 把我的项目 solondemo 打包为可以运行的 jar,确保通过。
java -jar solondemo.jar
可以正常运行并访问。然后把 solondemo.jar 上传到前面准备好的 GraalVM 环境。
首先,需要使用 GraalVM 提供的配置工具,对想要打包的程序的一些静态分析无法分析到的信息进行采集。
如果你确认项目没有使用任何反射、代理等特性,可以省略这一步。执行
java -agentlib:native-image-agent=config-output-dir=./config/ -jar solondemo.jar
执行后,最好能跑一跑 testcase,尽量保证代码覆盖率 100%,避免打包后遇到 classnotfound。
执行完成后,终止执行。
如果需要多次运行采集信息 可以使用如下命令再次执行,工具会自动合并采集结果而不是覆盖。
java -agentlib:native-image-agent=config-merge-dir=./config/ -jar solondemo.jar
执行结束,config 下生成如下 5 个文件:
jni-config.json predefined-classes-config.json proxy-config.json reflect-config.json resource-config.json serialization-config.json
坑又来了
根据网上的 说法,可以通过指定 -H:ConfigurationFileDirectories=./config 的方式来使用前面生成的配置文件。
但是,最后老是会把 -H:ConfigurationFileDirectories= 认为是指定的生成文件名,然而根据文档,-H:Name=xxx 才是指定输出文件名的参数。
注意 :这个问题是开始在 CentOS 上操作遇到的,最后我在 Ubuntu 上又尝试用这种方式指定配置文件的时候,它生效了,原因未知。
我采用了另外一种配置方式。把这些文件打包到 jar 包的 META-INF/native-image 目录下。
打包命令:
native-image -jar solondemo.jar --allow-incomplete-classpath -H:+ReportExceptionStackTraces --enable-http
这个命令也是经过反复多次尝试最终得出的可用命令,尝试的过程就略去3W字了。
注意 :如果你的应用需要对外提供 HTTP 服务,必须加上配置 --enable-http。如果提供 HTTPS 服务,则必须加 --enable-https。否则哪怕运行起来了访问也永远是 500。打包成功,目录下生成 solondem 文件。
执行运行:运行成功了,但是插件没加载。翻阅 solon 源码,发现插件加载的流程大致如下:
通过反复添加日志排查,发现:
在 graalvm native-image 下运行,这里扫描到 META-INF/solon 这个目录的 type,不是 file/jar,而是 resource。
所以到了这里自然就无法遍历目录下的文件了。于是我尝试让 resource 类型也走 file 的方式去扫描。
调研搜索后发现,GraalVM 内部资源管理自己实现了一套 FileSystem,URL 描述符定义为 resource,有一套自定义的 API(由于时间有限,暂未深入研究)。
对本来是目录类型的 resource 使用 File 方式去处理,得到的结果是 file not exists!但是对于确定的文件,是可以正常读取的。
于是我考虑预处理,在 GraalVM 外面就先把能扫描到的文件清单提取出来,通过配置的方式,插件扫描的时候直接返回预置的文件清单。
因为本地执行是可以正常扫描的,所以我在扫描结束的时候,增加一个输出:
然后在配置中添加:
scan 流程做如下修改
插件扫描成功并运行。
扫描注解也有同样的问题,排查过程与配置文件扫描类似,解决方案已与配置文件扫描的解决方案合并,略去 3万字。
至此,主框架已经可以 run 起来了,但是嵌入式数据库 h2 还在作妖。
调研发现 Github 已经有人提过这个问题:
https://github.com/h2database/h2database/issues/2207
但是我尝试按照他们说的 使用 1.4.199 版本,却仍然各种异常。没办法,下载 h2 源码 加 log 排查吧。
首先,这里报空指针,那么唯一的可能就是 defaultProvider 为空。分析 defaultProvider 初始化过程:
发现了 Class.forName,以及吃掉了异常:
e.printStackTrace();
打包,再来运行:
Caused by: java.lang.ClassNotFoundException ...... org.h2.store.fs.disk.FilePathDisk
好嘛。native-image 的 agent 居然没有把这个扫出来。手动把这些添加到 reflect-config.json 里面,再打包运行。又报了个别的 class not found。再添加,再打包。
[ ........, { "name": "org.h2.store.fs.FilePathDisk", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathMem", "methods":[{"name":"<init>","parameterTypes":[] }] } , { "name": "org.h2.store.fs.FilePathMemLZF", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathNioMem", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathNioMemLZF", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathNioMapped", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathAsync", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathZip", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathRetryOnInterrupt", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathNio", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.store.fs.FilePathSplit", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name": "org.h2.mvstore.db.MVTableEngine", "methods":[{"name":"<init>","parameterTypes":[] }], "allDeclaredFields":true } ]
成了!
访问接口、增删改查、静态页面 、日志都 OK 了。
幸福来得如此突然。
总结
最终可用的打包命令
native-image -jar solondemo.jar --allow-incomplete-classpath -H:+ReportExceptionStackTraces --enable-http
ld 需要升级到 2.26+
root@ubuntu:/home/hx/graalvm/demo3# ld -version GNU ld (GNU Binutils for Ubuntu) 2.26.1 Copyright (C) 2015 Free Software Foundation, Inc. This program is free software; you may redistribute it under the terms of the GNU General Public License version 3 or (at your option) a later version. This program has absolutely no warranty.
其他注意事项
- -agentlib:native-image-agent 不一定能检查出所有的反射;
- GraalVM 有自己的文件系统实现,暂未找到遍历目录的方法;
- 第三方包不能运行时,大概率是由于反射没有检查到导致的 class not found;
- 排查第三方包问题时,一定要注意被吃掉的 Exception;
- native-image 打包时需要 5G+ 内存。
补充
1. native-image后序列化失败问题(比如 JSON.toJSONString(JavaBean))
fastjson1.2.68 版本下在程序启动时增加如下代码:
ParserConfig.getGlobalInstance().setAsmEnable(false); SerializeConfig.getGlobalInstance().setAsmEnable(false);
2. 反射方法报错
需将反射类手动配置到 reflect-config.json 文件中,也可在编译打包成 jar 时添加配置
-agentlib:native-image-agent=config-output-dir=../META-INF/native-image
后打包,然后 java -jar 或 java -cp 运行起来后,执行对应测试用例后,会自动将反射类信息生成到。
reflect-config.json 文件中(但真的不一定)。配置文件样例:
[ { "name":"com.test.A", "allDeclaredFields":true, "allPublicFields":true, "queryAllPublicMethods":true, "methods":[ {"name":"getA","parameterTypes":[] }, {"name":"getD","parameterTypes":[] }, {"name":"getF","parameterTypes":[] }, {"name":"getI","parameterTypes":[] }, {"name":"getQ","parameterTypes":[] }, {"name":"getR","parameterTypes":[] }, {"name":"getT","parameterTypes":[] }, {"name":"getY","parameterTypes":[] }, {"name":"getU","parameterTypes":[] }, {"name":"getV","parameterTypes":[] } ] } ]
3. GraalVM 有自己的文件系统实现
暂未找到遍历目录的方法(即上文说说的).
如果你的程序中有涉及 ClassLoad.getResource("com.org") 这样的代码并打算对齐返回的结果以 File 或 jar 文件的方式扫描 com.org 下的所有类文件时会报错。
解决方式如上文所说,手动配置需要扫描的类文件,然后读取该配置(替代 getResource 方式)。
4. 控制 native 化后的二进制程序内存大小(配置参数不多说,一看就明白)
样例:
./solondemo -Xmx16m -Xms16m -XX:MaxDirectMemorySize=8m
上文提到的 solon scan 方法 支持预配置代码已提交到:
文中提到的 solondemo 项目,已经提交到:
https://gitee.com/mantouchong/solondemo
其他探索
- java运行包精简探索(一):https://www.jianshu.com/p/f1da836fb4d6
- java运行包精简探索(二):https://www.jianshu.com/p/5e3b1913cf3b