逃离Maven:再记Solon与Gradle的牵线搭桥之旅
flyway 天生与 native-image 不对付,在 solon 下也是如此。gradle 的大手近些年越伸越远,但在国内,还是 maven 的天下。本文主要是上一篇《轻装上阵-暂别JVM:小记Solon和它的朋友们在native-image下的爱恨情仇》的续集,继续解决 flyway 如何在 naitve-image 环境下与 solon 打配合。以及尝试让 gradle 的大手再往 solon 伸一些。
序
上一篇博客我们留下了两个烂摊子,分别是 flyway 的 sql 文件索引问题和原生编译工具链问题。正如我在上篇博客所说,我真的是个 java 新手。尤其是在 solon 方面😠当然,这些问题都是在 native-image 下的,所以本篇文章我们真的要和 jvm 世界说拜拜了。
编译,但是略施小计
这次我们走点阳关道。
gradle 插件
上次我们提到,只使用 gradle 编译会导致二进制文件启动后直接退出,表现得像是没有找到任何 Bean。我们简单思索片刻不难注意到,类似于 SpringBoot 和 Solon 这种主要以依赖注入的方式实现的框架,在启动时必然会有一个扫描 Bean 的作用。
扫描!
GraalVM Native Image 基于 封闭世界 假设。其要求所有需要访问的代码和资源都必须在编译时可知。任何动态和不可知的需求在运行时都是不允许的,比如反射、classpath扫描。
会不会是 solon-aot 会在编译前执行一些类似于我们在上篇文章提到的 flyway-resources.idx 文件,在运行时通过对索引文件的扫描来达到类似于 Bean 扫描的结果?
重新观察 solon-aot 源码,我们不难发现其入口类为 SolonAotProcessor。其中自然包含对 Bean 和 资源文件 的扫描、建索引。为了达到在 native-image 运行时扫描 Bean 的操作,我们不能在 native-image 环境下也使用适用于 jvm 的扫描方式,需要”重新实现”一套基于文件索引的扫描。就像我们在上篇文章中实现的 IndexedResourceProvider 一样。
继续阅读源码可知,solon 自带了这些实现,其在检测到当前为 native-image 环境时,会读取 reflect-config.json 作为补充项用于扫描,这些元数据由 SolonAotProcesser 在编译期得到,仅使用 agent 不会创建这些用户扫描的元数据。我们需要在编译期实现完成对 SolonAotProceser 的调用。为此我们必须要实现一套适用于 gradle 的 solon-aot 插件。但是从 opensolon 的仓库我们不难发现,这里是 maven 的天下……😭看来只能自己实现了
buildSrc
上个月在 solon 群里闲聊的时候,突然提到了这个。群主说有人做了 gradle 插件,但是 gradle 插件发布太啰嗦,一直没发到中央仓库。仓库地址:Gitee/Github。这俩甚至都只有一个 star(都是我🤣)……
gradle 的插件发布确实麻烦,除了我代劳发布插件外,我们还可以在项目工程根目录下创建一个 buildSrc 文件夹。gradle 允许将其作为本项目的依赖。即便是 gradle 插件也可以正常引入运行。接下来,我们的工作变得简单起来:
- 创建一个 buildSrc 目录
- 直接拷贝 solon-gradle-plugin 仓库的代码到 buildSrc 目录
solon-gradle-plugin 仓库是两个项目,但是buildSrc不让这么写。所幸这两个项目也比较简单,我们直接先复制文件较多的那个项目,再把文件较少的项目的 java 文件也复制到新文件夹中即可。最终的路径应该类似于:
1 | |
- 在我们的项目工程中像正常引入依赖一样,直接写
id("org.noear.solon.native")引入插件
应该还有一些别的改动,但都比较细碎,如果你看到这篇文章的时候,solon-gradle-plugin 还没有发布到中央仓库,可以参考 本项目-XiaomiAlbumSyncer。一切准备就绪,运行 ./gradlew clean nativeCompile 静候佳音。
再寻一条 flyway
上篇文章我们自己在 gradle 中实现了
- sql 的索引文件生成
- 基于文件索引的 resourceProvider
既然我们已经可以在 gradle 下完成 solon 的原生编译,那么我们完全可以使用 solon 提供的适用于 native-image 的资源扫描工具:ResourceUtil.scanResources(String resExpr)。其实现思路与我们的 IndexedResourceProvider 几乎相同。
- 在编译期间收集所有资源文件,将其写入到文件
solon-resource.json中。 - 在运行时检测当前是否为 native 环境,如果是,则解析匹配索引文件中的项目,以此完成扫描的操作。
我们要做的工作也非常简单:
- 确保我们的 sql 文件被 solon 的原生编译工具扫描并索引
- 重写 IndexedResourceProvider 使其直接使用
ResourceUtil.scanResources完成扫描
向 solon 注册资源文件
solon-aot 提供了一个接口,我们可以实现它来完成扫描、索引、嵌入这些操作。
1 | |
其中 MIGRATION_SQL_PATTERN_IN_NATIVE 是一个常量,值为 db/.*。表示资源路径下的 db 文件下的所有文件。我们的迁移文件就在其中。
1 | |
注册之前(选读)
你可能会问,为什么要注册 db 下的所有文件?不能只注册 db/migration 下的所有文件吗?
哈哈哈🤣我也是这么想,但是我发现除非明确写出单个文件的完整路径(eg. db/migration/V0.4.1__timeline_map.sql),否则不管我怎么注册,怎么换表达式的写法,都注册不上。
执行这里的注册代码是在编译期运行的,准确来说是在编译的前置步骤,对其调试并不是一个容易的事情。但好在其入口函数 SolonAotProcessor 比较简单,我们可以从编译工具(gradle 或者 maven)的日志中找到传给入口函数的参数。然后直接用 IDEA 打开这个 solon-aot 插件,启动这个 processor。
简单 debug 后可以发现在 SolonAotProcessor第225行处,其使用第一个路径作为父路径,再用剩余的内容作为匹配表达式。坦率地说,我看不懂这段代码,也不明白为什么要这么写。😭最终的结果就是我们只能在第一层文件夹匹配,不能做多层文件夹的匹配…
也可能是我 java 写太少了,毕竟我还是个 java 新手😋
原生扫描
使用 solon 的 ResourceUtil.scanResources 扫描非常简单,我们不需要各种乱七八糟的读取,这个接口返回的就是一个 List。
1 | |
写在最后
至此,我们已经走通了所有通往 native-image 的路径。原生编译带来的最大好处是极致的启动速度和略优的内存占用。就此新项目而言,内存占用从 jvm 的``100M-200M下降到20M - 120M`。应用层的内存占用获取还有较大的优化空间。但更多的这只是一次对新兴技术的尝试和探索。
好吧,其实原生编译也不是什么新鲜事
最后的最后也是介绍一下本项目 XiaomiAlbumSyncer,这是一个用于 全量/增量/定时 下载小米云服务中的相册到本地 的应用程序,最近的版本新增了 时间线对比 功能,将同一相册的增量同步时间优化到极低的水平。还支持 exif 填充,用于在外部应用中正确显示图片的真实保存时间。
或许下一篇文章就是 XiaomiAlbumSyncer 和 immich 的协同部署分享😋