点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:jianshu.com/p/f78a016b30fe

f809321c4ea0f743d0846f4069fbc863.jpeg


前言

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

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

背景如下:

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

环境

  • 系统 :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://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

环境准备

参考: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 的尝试就跳过了,网上一搜一堆。先了解一下打包命令。

214b1ba77f524d5aa6f838c5df02ca6f.png

https://www.graalvm.org/uploads/quick-references/native-image-quick-reference-v2_A4.pdf

项目依赖

7ab57f69e4d289f91e986ad324c38583.png

主要用到了 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 目录下。

2fb536bb0a1fcdaf99eaf10180ced6ce.png

打包命令:

native-image  -jar solondemo.jar --allow-incomplete-classpath -H:+ReportExceptionStackTraces --enable-http

这个命令也是经过反复多次尝试最终得出的可用命令,尝试的过程就略去3W字了。

注意 :如果你的应用需要对外提供 HTTP 服务,必须加上配置 --enable-http。如果提供 HTTPS 服务,则必须加 --enable-https。否则哪怕运行起来了访问也永远是 500。打包成功,目录下生成 solondem 文件。

执行运行:运行成功了,但是插件没加载。翻阅 solon 源码,发现插件加载的流程大致如下:

3548832042948293cc7da2a87519d619.png

通过反复添加日志排查,发现:

在 graalvm native-image 下运行,这里扫描到 META-INF/solon 这个目录的 type,不是 file/jar,而是 resource。

e56eadfe09f0646b94f6fd90cc472ddf.jpeg

所以到了这里自然就无法遍历目录下的文件了。于是我尝试让 resource 类型也走 file 的方式去扫描。

6892f2763013a28425904d8337b1b01e.png

调研搜索后发现,GraalVM 内部资源管理自己实现了一套 FileSystem,URL 描述符定义为 resource,有一套自定义的 API(由于时间有限,暂未深入研究)。

对本来是目录类型的 resource 使用 File 方式去处理,得到的结果是 file not exists!但是对于确定的文件,是可以正常读取的。

于是我考虑预处理,在 GraalVM 外面就先把能扫描到的文件清单提取出来,通过配置的方式,插件扫描的时候直接返回预置的文件清单。

因为本地执行是可以正常扫描的,所以我在扫描结束的时候,增加一个输出:

ece4e6806aa83ffc98f0c3e6be80879b.png

然后在配置中添加:

2abe62c7e65d9541c2ebe17abe62ecda.jpeg

scan 流程做如下修改

d0b13a63ce9f88a08f1148fd5c83cb3a.png

插件扫描成功并运行。

44678d9a0784271ea4c3ca99f41049f8.png

扫描注解也有同样的问题,排查过程与配置文件扫描类似,解决方案已与配置文件扫描的解决方案合并,略去 3万字。

至此,主框架已经可以 run 起来了,但是嵌入式数据库 h2 还在作妖。

调研发现 Github 已经有人提过这个问题:

https://github.com/h2database/h2database/issues/2207

但是我尝试按照他们说的 使用 1.4.199 版本,却仍然各种异常。没办法,下载 h2 源码 加 log 排查吧。

39d4ff506e578ad4185fceb230493a01.png

首先,这里报空指针,那么唯一的可能就是 defaultProvider 为空。分析 defaultProvider 初始化过程:

a1670c7f513d8a9462ca5991062c0cdc.png

发现了 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
  }
]

成了!

05e02da3ed1f3ad55f1ce988c8df0b84.png

访问接口、增删改查、静态页面 、日志都 OK 了。

dd335b6c6e7b21f05fbee2fd2a07c023.png 8402e49b4d3f78b2da3eeb9c85a2e9ef.png 978b401518cb8fadb85634892d1afb11.png

幸福来得如此突然。

总结

最终可用的打包命令

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



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

d059a6a1d6a0b459c0e3514a0f7f8aab.png

已在知识星球更新源码解析如下:

94a642cb0443faacdbf8fa1d67ab2f29.jpeg

da424b86276b20bb454c39a573ecb777.jpeg

cbb13f5a408bb799b8496f5fb33d8643.jpeg

8c1536969c6d2a4d59afe584e015ebab.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐