轻装上阵-暂别JVM:小记Solon和它的朋友们在native-image下的爱恨情仇

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 {
// 创建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 {
// flyway 的注入仅用于确保在初始化 KSqlClient 之前执行 Flyway 迁移
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())
// 注册 Instant 序列化
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收集信息。

  1. 运行./gradle shadowJar编译一个 Jar 出来
  2. 运行java -agentlib:native-image-agent=config-output-dir=./src/main/resources/META-INF/native-image -jar path/to/xiaomi-album-syncer.jar
  3. 模拟线上需求,尽可能调用所有接口,确保调用的接口覆盖所有动态性代码
  4. 终止上面这个 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 收集得不全。

接下来我们有两条路

  1. 研究到底是哪些信息没收集全,尝试补全它
  2. 找找别的奇技淫巧

我花了大约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
<!-- Build Helper Maven Plugin - 添加额外的源码目录 -->
<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! 如果一切顺利,那么我们只需要

  1. 确保 build.gradle.kts 和 pom.xml 中的版本号与依赖一致
  2. 执行 ./gradlew clean kspKotlin 执行 KSP 生成
  3. 执行 ./mvnw clean install -DskipTests 使用 maven 打包为 Jar
  4. 执行 ./mvnw clean native:compile -P native -DskipTests 使用 solon-aot 插件完成 native-image 的编译
  5. 文件 ./target/xiaomi-album-syncer 即为最终产物

就可以完美运行了!

集大成者

哈哈 骗你的,实测发现 solon-aot 也收集不到所有 native-image 需要的信息,在实例化任意 Jimmer 实体时就会报错。其实到这里已经精疲力尽😣无比绝望了😭。但是抱着死马当活马医的心态,我们能不能试试让 solon-aot 同时引入 agent 收集的信息?两种信息收集方式总能覆盖需要的数据了吧?

agent 收集的命令和上面一样,solon-aot 在编译二进制文件的时候也会自动应用此路径下的信息。最后,我们的编译顺序应该是:

  1. 确保 build.gradle.kts 和 pom.xml 中的版本号与依赖一致
  2. 执行 ./gradlew clean preCompile 执行 KSP 生成与 Flyway 迁移文件索引文件生成
  3. 执行 ./mvnw clean install -DskipTests 使用 maven 打包为 Jar
  4. 执行 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 所需的配置文件(主要是反射配置)
  5. 执行 ./mvnw clean native:compile -P native -DskipTests 使用 solon-aot 插件完成 native-image 的编译
  6. 文件 ./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中有两个35092927对此的讨论,这条路并不好走。

如果有朋友在 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
/**
* 一个简化的、native-image 友好的 ResourceProvider 实现。
*
* 工作方式:
* - 通过在构建期生成的资源清单(index 文件)列出所有可用迁移脚本;
* - 运行期根据 prefix/suffix 进行匹配并返回可读的 LoadableResource。
*/
class IndexedResourceProvider(
private val classLoader: ClassLoader = Thread.currentThread().contextClassLoader,
private val encoding: Charset = StandardCharsets.UTF_8,
/**
* 资源索引文件路径(必须在 classpath 上),每行一个资源路径,如:
* db/migration/V1__init.sql
* db/migration/V2__add_table.sql
*/
private val indexPath: String = "META-INF/flyway-resources.idx",
/**
* 是否在找不到索引文件时抛出异常。
* 若为 false,找不到索引时仅返回空结果。
*/
private val failIfIndexMissing: Boolean = true
) : ResourceProvider {

// 缓存索引内容
@Volatile
private var cachedIndex: List<String>? = null

override fun getResource(name: String): LoadableResource? {
// 直接按绝对路径加载(如 db/migration/V1__init.sql)
val url = classLoader.getResource(name) ?: return null
// 这里使用 Flyway internal 的 ClassPathResource 简化实现
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 ->
// 再次确认资源存在(native 下只要已被打包,一般能找到)
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
.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 讨论。


轻装上阵-暂别JVM:小记Solon和它的朋友们在native-image下的爱恨情仇
https://coooolfan.com/2025/09/26/solon-and-native-image/
作者
Coolfan
发布于
2025年9月26日
许可协议