| 项目 | 版本/信息 |
|---|---|
| GraalVM | 21.0.12+7-LTS (Oracle GraalVM) |
| Spring Boot | 3.4.5 |
| native-maven-plugin | 0.10.6 |
| 操作系统 | Windows 25H2 |
| C 编译器 | cl.exe (Microsoft x64 19.51.36256) |
| [环境信息] |
项目背景
Spring Boot 3.x 智能客服项目,集成了 Nacos(配置中心/服务注册)、Milvus(向量数据库)、Logback(日志)、MyBatis-Plus、Elasticsearch、Redis 等组件。配置了 Maven 双 Profile(JVM / Native)支持 GraalVM AOT 编译。
编译命令是:
bash
mvn clean -Pnative native:compile -X
第一阶段:grpc-netty-shaded 类初始化冲突(不可调和)
错误现象
Error: Classes that should be initialized at run time got initialized during image building:
io.grpc.netty.shaded.io.netty.util.AbstractReferenceCounted was unintentionally initialized at build time.
...(27+ 个 netty 类)
根因分析
Nacos 2.x 通过 nacos-client 传递依赖 grpc-netty-shaded,该 jar 内嵌 META-INF/native-image/native-image.properties 声明了所有 netty 类为 --initialize-at-run-time。但 native-image builder 的 points-to 分析阶段会通过 ensureClassInitialized 强制在构建时初始化这些类,两者不可调和。
GraalVM 21 硬性阻止从 RUN_TIME 改为 BUILD_TIME(ClassInitializationConfiguration.insertRec() 方法),报 "Incompatible change of initialization policy"。
排查过程
最初以为是 Nacos 独有来源,排除 nacos-client 后发现 grpc-netty-shaded 仍在 classpath 上:
bash
mvn dependency:tree -Pnative -Dincludes=io.grpc:grpc-netty-shaded
发现第二条依赖链:langchain4j-milvus → milvus-sdk-java → grpc-netty-shaded:1.59.1
最终方案
将两个冲突源的依赖 scope 设为 provided(编译可用,native-image 运行时不在 classpath):
XML
<profile>
<id>native</id>
<dependencies>
<!-- Nacos: grpc-netty-shaded via nacos-client -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<scope>provided</scope>
</dependency>
<!-- Milvus: grpc-netty-shaded via milvus-sdk-java -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-milvus</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
注意 :不能简单用 <exclusions> 排除 nacos-client,因为 spring-cloud-starter-alibaba-nacos-logging 子模块会引用 nacos-client 的 NacosLogging 类,导致 NoClassDefFoundError。provided scope 是更干净的方案。
第二阶段:Logback/SLF4J 类初始化冲突(通配符陷阱)
错误现象
排除 grpc-netty-shaded 后,构建继续报错:
Error: Classes that should be initialized at run time got initialized during image building:
ch.qos.logback.core.status.StatusBase was unintentionally initialized at build time.
ch.qos.logback.classic.Logger was unintentionally initialized at build time.
org.slf4j.LoggerFactory was unintentionally initialized at build time.
org.apache.commons.logging.LogFactory was unintentionally initialized at build time.
...(共 11 个类)
踩坑 1:** 通配符不生效
首先在 pom.xml 的 buildArgs 中添加:
XML
<buildArg>--initialize-at-build-time=ch.qos.logback.**,org.slf4j.**,org.apache.commons.logging.**</buildArg>
结果:完全无效,同样的错误。
踩坑 2:* 通配符也不生效
改为单星号:
XML
<buildArg>--initialize-at-build-time=ch.qos.logback.*,org.slf4j.*,org.apache.commons.logging.*</buildArg>
结果:同样无效。
根因:GraalVM 21 的通配符语义
GraalVM 21 的 ClassInitializationConfiguration 使用 startsWith + 边界检查的匹配逻辑。** 和 * 在包名模式中的行为与直觉不符 ------它们并不像 Ant 风格 glob 那样工作。经过实测验证,GraalVM 21 的通配符对这些类不生效,必须使用显式类名。
最终方案:显式列出所有 11 个类
XML
<buildArg>--initialize-at-build-time=ch.qos.logback.core.status.StatusBase,ch.qos.logback.core.status.InfoStatus,ch.qos.logback.core.util.StatusPrinter,ch.qos.logback.core.util.StatusPrinter2,ch.qos.logback.core.util.Loader,ch.qos.logback.classic.Logger,ch.qos.logback.classic.Level,org.slf4j.LoggerFactory,org.slf4j.helpers.Reporter,org.apache.commons.logging.LogFactory,org.apache.commons.logging.impl.Jdk14Logger</buildArg>
结果:类初始化错误全部消失!构建进入下一阶段。
第三阶段:JDK Console 相关类冲突(连环坑)
错误 1:ProxyingConsole 堆对象错误
Error: An object of type 'java.io.ProxyingConsole' was found in the image heap.
Trace: Object was reached by
trying to constant fold static field sun.security.util.Password$ConsoleHolder.c1
builder 分析阶段尝试常量折叠 Password$ConsoleHolder.c1 静态字段,该字段持有 ProxyingConsole 对象。但 ProxyingConsole 被标记为 RUN_TIME,不允许出现在 image heap 中。
尝试修复 :添加 --initialize-at-build-time=java.io.ProxyingConsole
错误 2:Console 继承链冲突
Error: Incompatible change of initialization policy for java.io.Console:
trying to change BUILD_TIME super type of java.io.ProxyingConsole to RUN_TIME
for JDK native code support via JNI
ProxyingConsole extends Console,而 GraalVM 21 的 JDK JNI 注册要求 java.io.Console 保持 RUN_TIME。强制 ProxyingConsole 为 BUILD_TIME 会级联要求父类 Console 也变为 BUILD_TIME,与 JDK 原生注册冲突。
结论 :不能将 ProxyingConsole 设为 BUILD_TIME,必须从源头阻止对象创建。
错误 3:级联初始化失败
改为 --initialize-at-run-time=sun.security.util.Password$ConsoleHolder 后:
Class initialization of jdk.internal.io.JdkConsoleImpl$1ConsoleHolder failed.
java.lang.IllegalStateException: Can't create a dedicated password console
since a real console already exists
Password$ConsoleHolder 的 <clinit> 触发了 JdkConsoleImpl$1ConsoleHolder 的初始化,后者在构建环境中失败(没有可用的专用密码控制台)。
最终方案:同时标记两个 JDK 内部类为 RUN_TIME
XML
<buildArg>--initialize-at-run-time=jdk.internal.io.JdkConsoleImpl$1ConsoleHolder,sun.security.util.Password$ConsoleHolder</buildArg>
Password$ConsoleHolder→ RUN_TIME:阻止构建时创建ProxyingConsole对象JdkConsoleImpl$1ConsoleHolder→ RUN_TIME:阻止级联初始化崩溃
结果:构建成功通过!
最终配置总览
pom.xml native profile 关键配置
XML
<profile>
<id>native</id>
<dependencies>
<!-- grpc-netty-shaded 冲突源:provided scope 排除 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-milvus</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.6</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals><goal>compile</goal></goals>
<phase>package</phase>
</execution>
<execution>
<id>default-cli</id>
<goals><goal>compile</goal></goals>
<phase>package</phase>
<configuration>
<buildArgs>
<buildArg>--initialize-at-build-time=ch.qos.logback.core.status.StatusBase,ch.qos.logback.core.status.InfoStatus,ch.qos.logback.core.util.StatusPrinter,ch.qos.logback.core.util.StatusPrinter2,ch.qos.logback.core.util.Loader,ch.qos.logback.classic.Logger,ch.qos.logback.classic.Level,org.slf4j.LoggerFactory,org.slf4j.helpers.Reporter,org.apache.commons.logging.LogFactory,org.apache.commons.logging.impl.Jdk14Logger</buildArg>
<buildArg>--initialize-at-run-time=jdk.internal.io.JdkConsoleImpl$1ConsoleHolder,sun.security.util.Password$ConsoleHolder</buildArg>
</buildArgs>
</configuration>
</execution>
</executions>
<configuration>
<buildArgs>
<buildArg>--initialize-at-build-time=ch.qos.logback.core.status.StatusBase,ch.qos.logback.core.status.InfoStatus,ch.qos.logback.core.util.StatusPrinter,ch.qos.logback.core.util.StatusPrinter2,ch.qos.logback.core.util.Loader,ch.qos.logback.classic.Logger,ch.qos.logback.classic.Level,org.slf4j.LoggerFactory,org.slf4j.helpers.Reporter,org.apache.commons.logging.LogFactory,org.apache.commons.logging.impl.Jdk14Logger</buildArg>
<buildArg>--initialize-at-run-time=jdk.internal.io.JdkConsoleImpl$1ConsoleHolder,sun.security.util.Password$ConsoleHolder</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
native-image.properties
java
Args = -H:IncludeResources=.*\\.xml$|.*\\.yml$|.*\\.properties$|.*\\.md$|.*\\.sql$ \
--no-fallback \
-H:+ReportExceptionStackTraces
踩坑经验总结
| # | 坑 | 教训 |
|---|---|---|
| 1 | grpc-netty-shaded 的 RUN_TIME 声明不可覆盖 |
GraalVM 21 硬性阻止 RUN_TIME → BUILD_TIME 的策略变更,唯一方案是排除依赖 |
| 2 | ** 通配符在 GraalVM 21 中不生效 |
ClassInitializationConfiguration 使用 startsWith 匹配,** 被当作字面字符 |
| 3 | * 通配符同样不生效 |
单星号也不匹配子包中的类,必须使用显式类名 |
| 4 | --initialize-at-build-time=ProxyingConsole 与 JDK JNI 冲突 |
ProxyingConsole extends Console,父类 Console 被 JDK 原生注册锁定为 RUN_TIME |
| 5 | 只标记 Password$ConsoleHolder 为 RUN_TIME 会级联崩溃 |
其 <clinit> 触发 JdkConsoleImpl$1ConsoleHolder 初始化,必须两者同时标记 |
| 6 | XML 注释中不能包含 -- |
pom.xml 的 XML 注释中写 --initialize-at-run-time 会导致解析错误 |
| 7 | provided scope 优于 <exclusions> |
排除传递依赖的子模块可能引用被排除 jar 的类,provided 更干净 |
Native 模式限制
| 功能 | JVM 模式 | Native 模式 |
|---|---|---|
| Nacos 配置中心/服务注册 | ✅ | ❌ (使用本地 application.yml) |
| Milvus 向量存储 | ✅ | ❌ (provided scope 排除) |
| 其他所有功能 | ✅ | ✅ |
希望这篇记录能帮助遇到类似问题的同学。GraalVM Native Image 的编译错误排查核心思路:仔细阅读错误
信息中的 trace 和 suggestion,它们通常已经给出了精确的修复方向