背景如下:
> 最近由于某些原因,需要做一個自帶運行環(huán)境的程序。由于各種原因,選定了 Java 和 Python 作為備選語言。但是 Java 由于 JRE 的臃腫(100M+)以及 Spring Boot 的日漸臃腫(helloworld 15M),需要在這兩方面進行 size 的縮減。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
環(huán)境
系統(tǒng) :Ubuntu 16.04
GraalVM :GraalVM Community 21.3.0 (Based on OpenJDK 11.0.13)
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
環(huán)境準備
關(guān)鍵配置
環(huán)境變量設置
exportJAVA_HOME=/path/to/exportPATH=/path/to/ /bin:$PATH
安裝 native-image
guinstallnative-image
安裝 native-image 需要的組件
apt-getinstallbuild-essentiallibz-devzlib1g-dev
注意 :這里有個其他文章都沒有提到的坑。ld 需要更新到 2.26+,不然在構(gòu)建過程中會報告莫名其妙的異常(這里我耗了大半天)。
apt-getinstallbinutils-2.26
另一個坑
最開始我是在一臺 aliyun 的機器上實驗的,沒有注意到內(nèi)存問題,在實驗過程中遇到異常中斷。排查 syslog 發(fā)現(xiàn)是 OOMKiller。排查發(fā)現(xiàn)我的可用內(nèi)存只有 4G。
后改為在本機機器 VMware 里的 Ubuntu 操作,內(nèi)存分配到 8G。經(jīng)觀察,native-image 打包過程中會用到 5.2G 左右的內(nèi)存,所以這里要注意一下。
至此,環(huán)境準備完成。
打包
helloworld 的嘗試就跳過了,網(wǎng)上一搜一堆。先了解一下打包命令。
項目依賴
主要用到了 solon、solon-api、h2、weed3、logback、slf4j、jlhttp 等包。
首先,通過 Maven 把我的項目 solondemo 打包為可以運行的 jar,確保通過。
java-jarsolondemo.jar
可以正常運行并訪問。然后把 solondemo.jar 上傳到前面準備好的 GraalVM 環(huán)境。
首先,需要使用 GraalVM 提供的配置工具,對想要打包的程序的一些靜態(tài)分析無法分析到的信息進行采集。
如果你確認項目沒有使用任何反射、代理等特性,可以省略這一步。執(zhí)行
java-agentlib:native-image-agent=config-output-dir=./config/-jarsolondemo.jar
執(zhí)行后,最好能跑一跑 testcase,盡量保證代碼覆蓋率 100%,避免打包后遇到 classnotfound。
執(zhí)行完成后,終止執(zhí)行。
如果需要多次運行采集信息 可以使用如下命令再次執(zhí)行,工具會自動合并采集結(jié)果而不是覆蓋。
java-agentlib:native-image-agent=config-merge-dir=./config/-jarsolondemo.jar
執(zhí)行結(jié)束,config 下生成如下 5 個文件:
jni-config.json predefined-classes-config.json proxy-config.json reflect-config.json resource-config.json serialization-config.json
坑又來了
根據(jù)網(wǎng)上的 說法,可以通過指定 -H:ConfigurationFileDirectories=./config 的方式來使用前面生成的配置文件。
但是,最后老是會把 -H:ConfigurationFileDirectories= 認為是指定的生成文件名,然而根據(jù)文檔,-H:Name=xxx 才是指定輸出文件名的參數(shù)。
注意 :這個問題是開始在 CentOS 上操作遇到的,最后我在 Ubuntu 上又嘗試用這種方式指定配置文件的時候,它生效了,原因未知。
我采用了另外一種配置方式。把這些文件打包到 jar 包的 META-INF/native-image 目錄下。
打包命令:
native-image-jarsolondemo.jar--allow-incomplete-classpath-H:+ReportExceptionStackTraces--enable-http
這個命令也是經(jīng)過反復多次嘗試最終得出的可用命令,嘗試的過程就略去3W字了。
注意 :如果你的應用需要對外提供 HTTP 服務,必須加上配置 --enable-http。如果提供 HTTPS 服務,則必須加 --enable-https。否則哪怕運行起來了訪問也永遠是 500。打包成功,目錄下生成 solondem 文件。
執(zhí)行運行:運行成功了,但是插件沒加載。翻閱 solon 源碼,發(fā)現(xiàn)插件加載的流程大致如下:
通過反復添加日志排查,發(fā)現(xiàn):
在 graalvm native-image 下運行,這里掃描到 META-INF/solon 這個目錄的 type,不是 file/jar,而是 resource。
所以到了這里自然就無法遍歷目錄下的文件了。于是我嘗試讓 resource 類型也走 file 的方式去掃描。
調(diào)研搜索后發(fā)現(xiàn),GraalVM 內(nèi)部資源管理自己實現(xiàn)了一套 FileSystem,URL 描述符定義為 resource,有一套自定義的 API(由于時間有限,暫未深入研究)。
對本來是目錄類型的 resource 使用 File 方式去處理,得到的結(jié)果是 file not exists!但是對于確定的文件,是可以正常讀取的。
于是我考慮預處理,在 GraalVM 外面就先把能掃描到的文件清單提取出來,通過配置的方式,插件掃描的時候直接返回預置的文件清單。
因為本地執(zhí)行是可以正常掃描的,所以我在掃描結(jié)束的時候,增加一個輸出:
然后在配置中添加:
scan 流程做如下修改
插件掃描成功并運行。
掃描注解也有同樣的問題,排查過程與配置文件掃描類似,解決方案已與配置文件掃描的解決方案合并,略去 3萬字。
至此,主框架已經(jīng)可以 run 起來了,但是嵌入式數(shù)據(jù)庫 h2 還在作妖。
但是我嘗試按照他們說的 使用 1.4.199 版本,卻仍然各種異常。沒辦法,下載 h2 源碼 加 log 排查吧。
首先,這里報空指針,那么唯一的可能就是 defaultProvider 為空。分析 defaultProvider 初始化過程:
發(fā)現(xiàn)了 Class.forName,以及吃掉了異常:
e.printStackTrace();
打包,再來運行:
Causedby: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":"","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathMem", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathMemLZF", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathNioMem", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathNioMemLZF", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathNioMapped", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathAsync", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathZip", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathRetryOnInterrupt", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathNio", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.store.fs.FilePathSplit", "methods":[{"name":" ","parameterTypes":[]}] }, { "name":"org.h2.mvstore.db.MVTableEngine", "methods":[{"name":" ","parameterTypes":[]}], "allDeclaredFields":true } ]
成了!
訪問接口、增刪改查、靜態(tài)頁面 、日志都 OK 了。
幸福來得如此突然。
總結(jié)
最終可用的打包命令
native-image-jarsolondemo.jar--allow-incomplete-classpath-H:+ReportExceptionStackTraces--enable-http
ld 需要升級到 2.26+
root@ubuntu:/home/hx/graalvm/demo3#ld-version GNUld(GNUBinutilsforUbuntu)2.26.1 Copyright(C)2015FreeSoftwareFoundation,Inc. Thisprogramisfreesoftware;youmayredistributeitunderthetermsof theGNUGeneralPublicLicenseversion3or(atyouroption)alaterversion. Thisprogramhasabsolutelynowarranty.
其他注意事項
-agentlib:native-image-agent 不一定能檢查出所有的反射;
GraalVM 有自己的文件系統(tǒng)實現(xiàn),暫未找到遍歷目錄的方法;
第三方包不能運行時,大概率是由于反射沒有檢查到導致的 class not found;
排查第三方包問題時,一定要注意被吃掉的 Exception;
native-image 打包時需要 5G+ 內(nèi)存。
補充
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 運行起來后,執(zhí)行對應測試用例后,會自動將反射類信息生成到。
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 有自己的文件系統(tǒng)實現(xiàn)
暫未找到遍歷目錄的方法(即上文說說的).
如果你的程序中有涉及 ClassLoad.getResource("com.org") 這樣的代碼并打算對齊返回的結(jié)果以 File 或 jar 文件的方式掃描 com.org 下的所有類文件時會報錯。
解決方式如上文所說,手動配置需要掃描的類文件,然后讀取該配置(替代 getResource 方式)。
4. 控制 native 化后的二進制程序內(nèi)存大?。ㄅ渲脜?shù)不多說,一看就明白)
樣例:
./solondemo-Xmx16m-Xms16m-XX:MaxDirectMemorySize=8m
審核編輯:劉清
-
URL
+關(guān)注
關(guān)注
0文章
139瀏覽量
15369 -
JAVA語言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20112 -
HTTP協(xié)議
+關(guān)注
關(guān)注
0文章
66瀏覽量
9744 -
Ubuntu系統(tǒng)
+關(guān)注
關(guān)注
0文章
91瀏覽量
3991 -
openjdk
+關(guān)注
關(guān)注
0文章
8瀏覽量
2321
原文標題:Java 運行包精簡探索(GraalVM)
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論