Java运行包精简探索

电子说

1.3w人已加入

描述

背景如下:

> 最近由于某些原因,需要做一个自带运行环境的程序。由于各种原因,选定了 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)

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

环境准备

关键配置

环境变量设置

 

export JAVA_HOME=/path/to/
export PATH=/path/to//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 的尝试就跳过了,网上一搜一堆。先了解一下打包命令。

openjdk

项目依赖

openjdk

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

openjdk

打包命令:

 

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

 

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

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

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

openjdk

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

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

openjdk

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

openjdk

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

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

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

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

openjdk

然后在配置中添加:

openjdk

scan 流程做如下修改

openjdk

插件扫描成功并运行。

openjdk

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

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

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

openjdk

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

openjdk

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

 

成了!

openjdk

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

openjdk

幸福来得如此突然。

总结

最终可用的打包命令

 

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

 






审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分