⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 jianshu.com/p/f78a016b30fe 「馒头虫」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

前言

在热心群友的提示下,入了 GraalVM 的坑。本来以为花个三两天应该就能搞定,没想到这一搞就是一个周,终于取得了阶段性的胜利。

编注:此文为运行包精简主题的一篇,其他精简方法请参见文末。

背景如下:

> 最近由于某些原因,需要做一个自带运行环境的程序。由于各种原因,选定了 Java 和 Python 作为备选语言。但是 Java 由于 JRE 的臃肿(100M+)以及 Spring Boot 的日渐臃肿(helloworld 15M),需要在这两方面进行 size 的缩减。

环境

  • 系统:Ubuntu 16.04
  • GraalVM:GraalVM Community 21.3.0 (Based on OpenJDK 11.0.13)

https://www.graalvm.org/downloads/

环境准备

参考: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 方法 支持预配置代码已提交到:

https://gitee.com/noear/solon

文中提到的 solondemo 项目,已经提交到:

https://gitee.com/mantouchong/solondemo

其他探索

  • java运行包精简探索(一):https://www.jianshu.com/p/f1da836fb4d6

  • java运行包精简探索(二):https://www.jianshu.com/p/5e3b1913cf3b

文章目录
  1. 1. 前言
  2. 2. 环境
  3. 3. 环境准备
  4. 4. 打包
  5. 5. 总结