solon 是一款完全对标 SpringBoot 的国产 web 开发框架。native-image 技术允许你将 Java 服务编译到特定平台的二进制代码,从而在无 JRE 环境的情况下启动,并带来远超 jvm 的预热速度。本文主要基于 XiaomiAlbumSync 项目,简述在二进制编译下使用Solon遇到的一系列问题和对应的解决方案。技术栈包含:Solon, Kotlin, Jimmer, Flyway, SaToken, Gradle, Maven.
序 好久没写博客了。这段时间 LLM 这么火,总觉得应该写点LLM的,但是实在是没啥好玩的,社区乱糟糟,未来雾茫茫。保持关注吧还是。最近在做 XiaomiAlbumSyncer 项目的重构,之前写的 Python 代码实在是不忍直视,痛下杀手从头再来。和 Uniboard 一样,还是依托于 java 生态,使用 Kotlin 作为编码语言,在一些技术栈上,选择了比较熟悉的 Jimmer(ORM), SaToken(鉴权), Flyway(数据库变更)。唯有提供 web 服务的基础框架,选中了 SpringBoot 的直接竞品:Solon。由此引发了一系列问题~包括概要中提到的两个构建工具的混用问题,我们下文详细说。
JVM 世界 开发是在 jvm 下运行的,我也不知道这是好事还是坏事……有太多问题直到编译时才暴露,麻烦很大。但是 native-image 也没法用于开发时测试。一根筋俩头堵了啊。
这一小节主要描述在 jvm 中遇到的一些小问题,主要都是 Solon 的引入导致,缺失了许多的 starter (Solon 中叫做 plugin)。体验了之后才知道,为什么要把 java 程序员称呼为 SpringBoot 程序员。
SaToken 最简单的一集,SaToken 为 Solon提供了第一方支持的 plugin,只需要引入即可
1 implementation("cn.dev33:sa-token-solon-plugin:1.44.0" )
一切都是开箱即用,和在 SpringBoot 下没有任何区别。
Flyway 作为一个数据库迁移工具,其核心逻辑不依赖于任何 web 框架。SpringBoot 的 flyway starter 严格遵循”约定大于配置”,将大量的配置都以默认值的形式封装在 starter 中,只需要引入依赖,然后一行代码都不用写。所幸 flyway 的核心逻辑也不复杂,在 Solon 中使用它没什么难度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Configuration class DatabaseMigration { private val log = LoggerFactory.getLogger(DatabaseMigration::class .java) @Managed fun migrate (dataSource: DataSource ) : Flyway { val flyway: Flyway = Flyway.configure() .dataSource(dataSource) .locations("classpath:db/migration" ) .baselineOnMigrate(true ) .validateOnMigrate(true ) .load() try { val infoService: MigrationInfoService = flyway.info() infoService.all().forEach { info -> log.info("发现迁移: 版本: ${info.version} , 描述: ${info.description} , 状态: ${info.state} " ) } log.info("开始数据库迁移..." ) val migrationsExecuted: Int = flyway.migrate().migrationsExecuted log.info("成功执行了 $migrationsExecuted 个迁移" ) return flyway } catch (e: Exception) { log.error("数据库迁移失败: " + e.message) throw RuntimeException("数据库迁移失败" , e) } } }
简单总结一下:
创建一个 Flyway 实例
传入对应的配置(Flyway 会扫描对应目录的所有 sql 文件)
触发一次迁移
就是这么简单。
并非这么简单,待会儿到 native-image 就有得折腾了😋
Jimmer 我确实不喜欢 xml 这种文件格式。这个项目又有大量的连表操作。这次可以深入体验一下这个”JVM下最先进的ORM”。
核心功能 Jimmer 核心功能没有与任何 web 框架绑定,我们只需要实现一个返回 KSqlClinet 对象的 Bean 就可以像 SpringBoot 中使用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration class JimmerStarter { private val log = LoggerFactory.getLogger(JimmerStarter::class .java) @Managed fun sql (dataSource: DataSource , flyway: Flyway ) : KSqlClient { try { val kSqlClient = newKSqlClient { log.info("初始化 kSqlClient 并校验表结构" ) setDialect(SQLiteDialect()) setConnectionManager(ConnectionManager.simpleConnectionManager(dataSource)) setDatabaseNamingStrategy(DefaultDatabaseNamingStrategy.LOWER_CASE) setExecutor(Executor.log()) setSqlFormatter(SqlFormatter.PRETTY) setDatabaseValidationMode( DatabaseValidationMode.ERROR ) } return kSqlClient } catch (e: DatabaseValidationException) { log.info("数据库校验失败: " + e.message) log.info( "1. 如果您正处于开发环境,请确保已执行最新的迁移脚本,并根据报错信息手动调整数据库或Flyway迁移文件。\n" + "2. 如果您正处于生产环境,请前往Github仓库提交issue寻求帮助。此报错不应该出现在生产环境。" ) throw e } } }
客户端代码 Jimmer 还提供了非常简单好用的 TypeScript 代码生成,这样前端工程就可以直接调用ts方法,对于大量的DO、VO、FO,真正实现了:一次定义,随处可用。同时得益于 Jimmer 对 null 和 undefined 的分别判断,我们可以确保空安全 从浏览器 到数据库 的全流程一致。让我们努力一下,让 Jimmer 的这个功能在 Solon 下也可用。
Jimmer 的客户端代码生成有 swagger 和 TypeScript 两种表现形式,这是同一份元数据的不同表现形式。
元数据包括但不限于
接口路径
路径参数
param参数
body参数
请求方法
显然地,这些内容与Web框架强绑定,比如用于声明路径参数的注解,在 SpingBoot 中为 @PathVariable,在Solon中为 @Path。Jimmer 将这部分逻辑实现在了 starter 中,我们只需要参考其实现,为 Solon 也实现一份即可。具体的代码有点多,而且基本都是 copy 过来的,我们简要提几句。
我们需要实现两个核心类:
org.babyfish.jimmer.client.runtime.Metadata.OperationParser : 接口的基本信息,包括: 路径、请求方法、是否为流
org.babyfish.jimmer.client.runtime.Metadata.ParameterParser : 接口的参数信息,包括: 请求头、param参数、路径参数、请求体、默认值、是否可空以及其它配置
在实现中,我们需要判断传入的参数上的注解,判断是否为当前接口需要的参数,并选择性返回。
Jimmer 在启动后会扫描所有被注册的 api,然后取到这些方法的入参,交由我们的实现处理,最终汇聚转换完成 swagger 文件和 typescript 文件的输出和生成。
具体的代码可以参考 Github 仓库源码页面 。其中还有三个 Controller,分别对应了 Swagger UI、openapi yml、TypeScript 代码。
序列化与反序列化 Jimmer 使用且仅支持使用 Jackson 作为序列化和反序列化框架,但是 Solon 自带了一个叫做 snack3 的 Json 序列化和反序列化框架。我们需要引入 Jackson,然后对 Solon-web 排除 snack3 ,确保在 api 交互过程中只有 Jackson 参与。
1 2 3 4 5 6 7 8 9 10 dependencies { implementation("org.noear:solon-web" ) { exclude(group = "org.noear" , module = "solon-serialization-snack3" ) } implementation("org.noear:solon-serialization-jackson:3.5.1" ) implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2" ) }
然后还需要向 Jackson 注册 Jimmer 的序列化和反序列化 module。
不保证这段代码符合最佳实践,我只知道它能按预期运行……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration class Jackson { private val log = LoggerFactory.getLogger(Jackson::class .java) @Bean fun registerJimmerJacksonModule (@Inject factory: JacksonRenderFactory , @Inject executor: JacksonActionExecutor ) { log.info("注册适用于 Jimmer 实体的 Jackson module..." ) factory.config().registerModule(ImmutableModule()) factory.config().registerModule(KotlinModule.Builder().build()) executor.config().registerModule(ImmutableModule()) executor.config().registerModule(KotlinModule.Builder().build()) factory.addConvertor(Instant::class .java, { it.toString() }) } }
暂别 JVM
GraalVM Native Image 基于 封闭世界 假设。其要求所有需要访问的代码和资源都必须在编译时可知。任何动态和不可知的需求在运行时都是不允许的,比如反射、classpath扫描。
编译,不择手段地编译 相较于编译到 Jar,native-image 要求在编译时收集并明确所有”动态”代码和资源,主要是反射和资源。这些信息可以用两种方法获取到
依赖包自带,一般在 META-INF/native-image 下,由依赖自行维护。这是最优选择
使用 Graal Agent 收集,在 jvm 下模拟生产环境运行应用程序,由 agent 自动收集所有动态信息
放不下的 Gradle 由于 Jimmer 使用了代码生成,而且本应用程序使用 Kotlin 作为编写语言,KSP 在 maven 下的兼容性存在问题,gradle 是唯一的选择。
在 plugins 中引入
1 2 3 4 5 6 7 8 plugins { java application kotlin("jvm" ) version "2.2.10" id("com.google.devtools.ksp" ) version "2.2.10-2.0.2" id("org.graalvm.buildtools.native" ) version "0.11.0" id("com.github.johnrengelman.shadow" ) version "8.1.1" }
然后即可执行./gradlew nativeRun运行或执行./gradle nativeCompile编译
除非您的项目本身不使用反射等动态特性,且所有依赖都自带了完整且准确的 META-INF/native-image 信息,否则,请继续以下步骤使用agent收集信息。
运行./gradle shadowJar编译一个 Jar 出来
运行java -agentlib:native-image-agent=config-output-dir=./src/main/resources/META-INF/native-image -jar path/to/xiaomi-album-syncer.jar
模拟线上需求,尽可能调用所有接口,确保调用的接口覆盖所有动态性代码
终止上面这个 java 进程
接下来我们会在制定的输出路径看到 agent 收集到的各类信息
1 2 3 4 5 6 7 8 9 10 11 12 yang@mbp server % tree ./src/main/resources/META-INF/native-image ./src/main/resources/META-INF/native-image ├── agent-extracted-predefined-classes ├── jni-config.json ├── predefined-classes-config.json ├── proxy-config.json ├── reflect-config.json ├── resource-config.json └── serialization-config.json 2 directories, 6 files yang@mbp server %
在此路径下的信息会在二进制编译时自动应用。所以接下来我们只需要./gradlew nativeCompile即可完成二进制编译并顺利运行
吗?
如果你也在用 Solon 并尝试以此步骤完成二进制编译,接下来当你运行二进制文件大概率会遇到
1 2 3 4 5 6 7 8 9 10 11 12 yang@mbp server % ./build/native/nativeCompile/XiaomiAlbumSyncer INFO 2025-09-25 17:24:19.003 #47910 [-main][*][o.noear.solon.Solon]: App: Start loading INFO 2025-09-25 17:24:19.005 #47910 [-main][*][o.noear.solon.Solon]: App: Plugin starting INFO 2025-09-25 17:24:19.005 #47910 [-main][*][o.noear.solon.Solon]: App: Bean scanning INFO 2025-09-25 17:24:19.005 #47910 [-main][*][o.noear.solon.Solon]: App: End loading elapsed=21ms pid=47910 v=3.5.1 INFO 2025-09-25 17:24:19.005 #47910 [-Thread-0][*][o.noear.solon.Solon]: App: Stopped yang@mbp server %
项目启动直接就退出了!
为什么? 我也不知道😭,观察编译日志,agent 收集到的信息是起作用了的(methods registered for reflection 有显著增加),只能猜测是 agent 收集得不全。
接下来我们有两条路
研究到底是哪些信息没收集全,尝试补全它
找找别的奇技淫巧
我花了大约2天的时间尝试路线1,毕竟有条件没有理由不选名门正派。遗憾的是,进度和成果为0。其中不乏和 gpt-5、gemini-2.5-pro 深入交流,均一无所获。
Maven 在寻觅 Solon 官网文档的时候,发现 Solon 提供专用于二进制编译的依赖库: solon-aot。在对一个全新的 Solon 项目尝试后,发现在引入了这个依赖并使用该依赖提供的配置文件编译,即可完美编译出一个二进制文件!它能够正常启动,加载所有 Bean 并完成依赖注入,提供 web 服务。一些都是那么理所当然。
那么接下来我们只需要在 build.gradle.kts 中引入这个依赖,用它的打包命令替代 ./gradlew nativeCompile 就可以了
吗?
solon-aot 仅提供了对 Maven 的支持,其原理是在 maven 编译的生命周期中,启动我们的 solon 实例,并在此时收集所有 native-image 所需要的各类信息。其与 native-image agent 的区别在于 solon-aot 能以应用程序本身的视角去收集所有信息,而 agent 只能通过观察的方式收集。
好吧,我也不知道为啥 solon-aot 能收集到 agent 收集不到的信息
希望有朝一日我成为 java 高手后,可以为 solon-aot 完成对 gradle 的适配。
还记得我们选择 gradle 的原因是 maven 在 kotlin 下对 ksp 对支持有限吗?幸运的是,ksp 是编译前的一个完全独立的步骤,Jimmer 的 ksp 生成后的文件并不复杂,我们完全可以先用 gradle 生成这些文件,然后在 maven 中配置,将其添加为额外的源码目录 。
我知道这真的很邪门,但是我们现在的目标是先成功编译一个能跑的😭
这个操作很简单,只需要在 maven 的 plugins 标签里添加一个 plugin 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <plugin > <groupId > org.codehaus.mojo</groupId > <artifactId > build-helper-maven-plugin</artifactId > <version > 3.6.1</version > <executions > <execution > <id > add-source</id > <phase > generate-sources</phase > <goals > <goal > add-source</goal > </goals > <configuration > <sources > <source > build/generated/ksp/main/kotlin</source > </sources > </configuration > </execution > </executions > </plugin >
OK! 如果一切顺利,那么我们只需要
确保 build.gradle.kts 和 pom.xml 中的版本号与依赖一致
执行 ./gradlew clean kspKotlin 执行 KSP 生成
执行 ./mvnw clean install -DskipTests 使用 maven 打包为 Jar
执行 ./mvnw clean native:compile -P native -DskipTests 使用 solon-aot 插件完成 native-image 的编译
文件 ./target/xiaomi-album-syncer 即为最终产物
就可以完美运行了!
集大成者 哈哈 骗你的,实测发现 solon-aot 也收集不到所有 native-image 需要的信息,在实例化任意 Jimmer 实体时就会报错。其实到这里已经精疲力尽😣无比绝望了😭。但是抱着死马当活马医的心态,我们能不能试试让 solon-aot 同时引入 agent 收集的信息?两种信息收集方式总能覆盖需要的数据了吧?
agent 收集的命令和上面一样,solon-aot 在编译二进制文件的时候也会自动应用此路径下的信息。最后,我们的编译顺序应该是:
确保 build.gradle.kts 和 pom.xml 中的版本号与依赖一致
执行 ./gradlew clean preCompile 执行 KSP 生成与 Flyway 迁移文件索引文件生成
执行 ./mvnw clean install -DskipTests 使用 maven 打包为 Jar
执行 java -agentlib:native-image-agent=config-output-dir=./src/main/resources/META-INF/native-image -jar target/xiaomi-album-syncer.jar 使用 GraalVM 运行 Jar 包,生成 native-image 所需的配置文件(主要是反射配置)
执行 ./mvnw clean native:compile -P native -DskipTests 使用 solon-aot 插件完成 native-image 的编译
文件 ./target/xiaomi-album-syncer 即为最终产物
大功告成!!!
这绝对不是最佳实践,理想情况应当使用 solon-aot 收集所有元数据,并在单一构建工具中完成。与此相关的工作正在进行中。(我是java新手,进度有点慢。2025-09-28 留)
where is flyway? 还记得我们之前提到的 Flyway 的伏笔吗?让我们同时回忆一下 Flyway 的版本迁移逻辑和 native-image 限制。
Flyway 会扫描目录,发现其中的所有 sql 文件
GraalVM Native Image 基于 封闭世界 假设,任何动态和不可知的需求在运行时都是不允许的,比如反射、classpath 扫描
好巧不巧,Flyway 发现 sql 文件的逻辑恰好命中了 native-image 的限制!从 Flyway 的功能需求去考虑,这一限制(或者说冲突)是不可避免的。Github Issues中有两个3509 、2927 对此的讨论,这条路并不好走。
如果有朋友在 SpringBoot 下用过 Flyway 并编译到二进制文件会发现,Flyway 是能够完成发现和迁移步骤的。这难道有什么魔法?或者是 SpringBoot 又又对 Flyway 开了小灶?
准确的说是 Flyway 和 SpringBoot 相互开了小灶。spring-aot 在编译时会把所有静态文件做一份索引,这样在运行时就可以去索引里做“列出”的操作。同时 Flyway 允许传入一个 ResourceProvider 对象,将”发现sql”这个功能交由用户自行实现的方法执行。
Flyway 的 SpringBoot starter 自带了一个名为 NativeImageResourceProviderCustomizer 的实现,并智能地在 native-image 运行时应用这个对象到 Flyway 实例。最终实现了 Flyway 在 native-image 下的适配。
solon 连 Flayway 的 plugin 都没,当然没有这些乱七八糟的实现了。让我们自己动手,丰衣足食💪
编译时:静态文件索引 我真的不喜欢 xml 文件格式,而且我们本来就有 gradle 的流程,所以让我们在 gradle 中注册一个任务来实现索引构建的流程吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 val generateFlywayIndex = tasks.register("generateFlywayIndex" ) { val resourcesDir = layout.projectDirectory.dir("src/main/resources" ) val migrationsDir = resourcesDir.dir("db/migration" ) val indexFile = resourcesDir.file("META-INF/flyway-resources.idx" ) inputs.dir(migrationsDir) outputs.file(indexFile) doLast { val base = migrationsDir.asFile.toPath() val indexPath = indexFile.asFile.toPath() indexPath.parent?.let { Files.createDirectories(it) } val lines = Files.walk(base) .filter { Files.isRegularFile(it) } .map { base.relativize(it).toString().replace('\\' , '/' ) } .map { "db/migration/$it " } .sorted() .toList() Files.write(indexPath, lines) println("Generated flyway index with ${lines.size} entries at $indexPath " ) } }
内容很简单,就是把 db.migration 中的所有文件的文件名写一份到 META-INF/flyway-resources.idx 中。这里有些路径和下文的运行时会存在重复定义的情况,瑕不掩瑜。
我们还可以创建一个聚合任务,这样 ksp 生成和这个索引生成 只需要运行一次就行了。
1 2 3 tasks.register("preCompile" ) { dependsOn("kspKotlin" , generateFlywayIndex) }
solon-aot 自带了这些实现。实际上我们无需自行完成这些工作。与此相关的工作正在进行中。(我是java新手,进度有点慢。2025-09-28 留)
运行时:从索引中发现 sql 文件 实现 ResourceProvider 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 class IndexedResourceProvider ( private val classLoader: ClassLoader = Thread.currentThread().contextClassLoader, private val encoding: Charset = StandardCharsets.UTF_8, private val indexPath: String = "META-INF/flyway-resources.idx" , private val failIfIndexMissing: Boolean = true ) : ResourceProvider { @Volatile private var cachedIndex: List<String>? = null override fun getResource (name: String ) : LoadableResource? { val url = classLoader.getResource(name) ?: return null return ClassPathResource(null , name, classLoader, encoding) } override fun getResources (prefix: String , suffixes: Array <out String >) : MutableCollection<LoadableResource> { val index = loadIndexOrEmpty() if (index.isEmpty()) { return mutableListOf() } val matched = index.asSequence() .filter { path -> startsWithAndEndsWith(path, prefix, suffixes) } .mapNotNull { path -> val url = classLoader.getResource(path) ?: return @mapNotNull null ClassPathResource(null , path, classLoader, encoding) as LoadableResource } .toList() return matched.toMutableList() } private fun startsWithAndEndsWith (filename: String , prefix: String , suffixes: Array <out String >) : Boolean { if (!filename.substringAfterLast('/' ).startsWith(prefix)) { return false } for (suf in suffixes) { if (filename.endsWith(suf)) { return true } } return false } private fun loadIndexOrEmpty () : List<String> { val cached = cachedIndex if (cached != null ) { return cached } synchronized(this ) { val again = cachedIndex if (again != null ) return again val stream = classLoader.getResourceAsStream(indexPath) if (stream == null ) { if (failIfIndexMissing) { throw IllegalStateException( "Resource index not found on classpath: $indexPath . " + "Please generate it at build time and include it in the image." ) } cachedIndex = emptyList() return emptyList() } stream.use { ins -> BufferedReader(InputStreamReader(ins, encoding)).use { reader -> val lines = reader.lineSequence() .map { it.trim() } .filter { it.isNotEmpty() && !it.startsWith("#" ) } .toList() cachedIndex = lines return lines } } } } }
记得对 Flyway 实例传入这个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 val flyway: Flyway = Flyway.configure() .dataSource(dataSource) .locations("classpath:db/migration" ) .baselineOnMigrate(true ) .validateOnMigrate(true ) .resourceProvider( IndexedResourceProvider( classLoader = Thread.currentThread().contextClassLoader, encoding = Charsets.UTF_8, indexPath = "META-INF/flyway-resources.idx" , failIfIndexMissing = true ) ) .load()
okkk! 万事俱备,只欠编译。最后顺便欣赏一下 log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 ~/Documents/code/xiaomialbumsyncer/server refactor* 1m 42s ❯ ./target/xiaomi-album-syncer INFO 2025-09-26 15:59:15.360 #57880 [-main][*][o.noear.solon.Solon]: App: Start loading INFO 2025-09-26 15:59:15.362 #57880 [-main][*][o.noear.solon.Solon]: App: Plugin starting INFO 2025-09-26 15:59:15.366 #57880 [-main][*][o.noear.solon.Solon]: Render mapping: @json=StringSerializerRender#jackson-json INFO 2025-09-26 15:59:15.366 #57880 [-main][*][o.noear.solon.Solon]: Render mapping: @type_json=StringSerializerRender#jackson-json INFO 2025-09-26 15:59:15.366 #57880 [-main][*][o.noear.solon.Solon]: Session: Local session state plugin is loaded INFO 2025-09-26 15:59:15.367 #57880 [-main][*][o.noear.solon.Solon]: App: Bean scanning INFO 2025-09-26 15:59:15.368 #57880 [-main][*][c.c.x.config.Jackson]: 注册适用于 Jimmer 实体的 Jackson module... INFO 2025-09-26 15:59:15.369 #57880 [-main][*][c.z.h.HikariDataSource]: SQLitePool - Starting... INFO 2025-09-26 15:59:15.514 #57880 [-main][*][c.z.h.p.HikariPool]: SQLitePool - Added connection org.sqlite.jdbc4.JDBC4Connection@669714a0 INFO 2025-09-26 15:59:15.514 #57880 [-main][*][c.z.h.HikariDataSource]: SQLitePool - Start completed. WARN 2025-09-26 15:59:15.522 #57880 [-main][*][o.f.c.i.s.c.ClassPathScanner]: Unable to scan location: /db/migration (unsupported protocol: resource) INFO 2025-09-26 15:59:15.523 #57880 [-main][*][o.f.c.FlywayExecutor]: Database: *********************************************************** INFO 2025-09-26 15:59:15.524 #57880 [-main][*][c.c.x.c.f.DatabaseMigration]: 发现迁移: 版本: 0.0.1, 描述: init, 状态: SUCCESS INFO 2025-09-26 15:59:15.524 #57880 [-main][*][c.c.x.c.f.DatabaseMigration]: 开始数据库迁移... WARN 2025-09-26 15:59:15.525 #57880 [-main][*][o.f.c.i.s.c.ClassPathScanner]: Unable to scan location: /db/migration (unsupported protocol: resource) INFO 2025-09-26 15:59:15.525 #57880 [-main][*][o.f.c.i.c.DbValidate]: Successfully validated 1 migration (execution time 00:00.000s) INFO 2025-09-26 15:59:15.525 #57880 [-main][*][o.f.c.i.c.DbMigrate]: Current version of schema "main": 0.0.1 INFO 2025-09-26 15:59:15.525 #57880 [-main][*][o.f.c.i.c.DbMigrate]: Schema "main" is up to date. No migration necessary. INFO 2025-09-26 15:59:15.525 #57880 [-main][*][c.c.x.c.f.DatabaseMigration]: 成功执行了 0 个迁移 INFO 2025-09-26 15:59:15.526 #57880 [-main][*][c.c.x.c.j.JimmerStarter]: 初始化 kSqlClient 并校验表结构 INFO 2025-09-26 15:59:15.538 #57880 [-main][*][o.b.j.s.r.ExecutorForLog]: Execute SQL===> Purpose: QUERY SQL: select tb_1_.id, tb_1_.name, tb_1_.description, tb_1_.enabled, tb_1_.config from crontab tb_1_ JDBC response status: success Time cost: 1ms <===Execute SQL INFO 2025-09-26 15:59:15.538 #57880 [-main][*][c.c.x.c.TaskScheduler]: 载入定时任务完成,共注册 0 个任务 ____ ____ ___ ____ _ _ ____ _ _ [__ |__| __ | | | |_/ |___ |\ | ___] | | | |__| | \_ |___ | \| https://sa-token.cc (v1.44.0) INFO 2025-09-26 15:59:15.540 #57880 [-main][*][o.noear.solon.Solon]: solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080} INFO 2025-09-26 15:59:15.540 #57880 [-main][*][o.noear.solon.Solon]: Server:main: smarthttp: Started (smart http 2.5/3.5.1) @1ms INFO 2025-09-26 15:59:15.540 #57880 [-main][*][o.n.s.s.s.JobManager]: JobManager started, job.size=0 INFO 2025-09-26 15:59:15.540 #57880 [-main][*][o.noear.solon.Solon]: App: End loading elapsed=198ms pid=57880 v=3.5.1
写在最后 从 solon 官网的文档看,solon-aot 也提供了类似于 spring-aot 构建静态文件索引的能力。但是我一直测试不成功。对于 agent 和 solon-aot 各自收集 native-image 信息不全的问题也是一桩悬案。如果您对本文相关内容有任何疑问、见解或者建议,欢迎在 Github XiaomiAblumSyncer Issues 讨论。