bootJar 混入 spring-boot-testcontainers:统一开发与运行配置的版本
应用打包成功却在启动时缺少 Startable,可能是 testAndDevelopmentOnly 与 runtimeClasspath 对同一模块解析出不同版本,打包差集未能排除开发依赖。
bootJar 能完成,启动可执行 JAR 却报:
NoClassDefFoundError: org/testcontainers/lifecycle/Startable
若 BOOT-INF/lib 里有 spring-boot-testcontainers,却没有它需要的 Testcontainers 核心库,应检查开发专用依赖是否漏进了生产包。这里不是把缺少的测试库继续加入生产就算修好。
一种明确条件是:通过 implementation(platform(...)) 导入 Boot BOM,但 testAndDevelopmentOnly 没得到相同约束。同一模块在 runtimeClasspath 中升级了,在开发配置中仍是另一版本,打包时按文件做差集就可能无法把升级后的 JAR 排掉。
对比两个配置的实际版本
检查 runtimeClasspath 与 testAndDevelopmentOnly 中 org.springframework.boot:spring-boot-testcontainers 的解析结果和引入路径。具体场景中,两边分别为 4.1.0 和 4.0.6;相同现象也能在 4.0.7 与 4.0.6 之间触发,因此不能把它限定为 Boot 4.1.0 独有回归。
Gradle 的 platform 只约束声明它的配置及继承该配置的其他配置。主源码的依赖版本看起来正确,不代表 Boot 新建的开发专用配置也得到相同 BOM。
如果失败是“无法选择变体”,而不是同模块版本不同,应另查配置属性。这两类问题都可能涉及开发依赖,但不能用同一段配置代替诊断。
让开发配置按运行配置一致解析
在已应用 Java 与 Boot 插件的 Kotlin build.gradle.kts 中,可为现有配置增加:
configurations {
named("testAndDevelopmentOnly") {
shouldResolveConsistentlyWith(named("runtimeClasspath").get())
}
}
这表达的是两个配置应使用一致的解析版本。若工程有意让它们使用不同版本,应先重新评估打包策略;一致解析会改变这种安排,不能无条件推广到所有配置。
也可把相同 BOM 明确应用到需要它的开发配置,或使用项目既有的统一依赖管理方式。关键是核对实际约束覆盖范围,而不是堆叠多种插件后假定版本自然一致。
不要先对整个运行配置排除所有 Testcontainers 模块:这可能掩盖真正需要某些模块的功能,也无法解释为什么只漏进其中一个 JAR。先让版本和配置职责一致,再核对产物。
重新生成生产 JAR,检查 BOOT-INF/lib 不含本应排除的开发集成,并按真实启动方式验证应用。随后运行开发和测试入口,确认它们仍能启动容器。保存原依赖与构建配置,回退时一起恢复;仅让生产启动不再缺类,不能证明打包边界正确。


