资讯详情

资讯详情

Spring Boot 3 + JavaFX 打包报错:No auto configuration classes found 排查与解决

1. 报错现场一条堆栈背后藏的是“自动配置饿死”问题1.1 我是在什么场景下遇到这个报错的先说背景。我这边维护着一个 JavaFX 桌面客户端项目界面层用 JavaFX 17后端框架从 Spring Boot 2.3 一路升上来过年前顺手把 Spring Boot 升到了 3.1.2。当时用 IDEA 直接跑main()方法一切正常但用mvn package打出可分发的安装包后再启动就弹出了这行报错java.lang.IllegalStateException: No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.项目结构本身不算复杂一个Launcher类作为 JavaFX 的入口一个AppConfig类标注SpringBootApplication通过SpringApplicationBuilder在 JavaFX 的init()阶段启动 Spring 容器。之前用 Spring Boot 2.5 的时候从没出过这个错升级后只要是走自定义打包流程就必现。这个报错的字面意思是Spring Boot 在启动阶段尝试加载自动配置类结果在META-INF/spring.factories里一个候选类都没找到。注意最后一句话——If you are using a custom packaging——这是 Spring Boot 在暗示问题大概率出在打包环节而不是你的代码逻辑。1.2 完整报错信息与发生阶段我把完整堆栈贴在下面方便大家对照java.lang.IllegalStateException: No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct. at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getCandidateConfigurations(AutoConfigurationImportSelector.java:120) at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.selectImports(AutoConfigurationImportSelector.java:97) at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.selectImports(AutoConfigurationImportSelector.java:87) at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:571) at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:549) at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:247) at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:573) at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:549) at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:247) at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:207) ...从堆栈能看出来错误发生在ConfigurationClassParser处理EnableAutoConfiguration导入的候选类时属于 Spring 容器刷新阶段的早期流程。换句话说Spring 刚准备初始化容器就发现自己手里没有任何自动配置类可加载直接放弃治疗。我当时的第一个直觉是是不是 IDEA 里创建 Spring Boot 项目时选的版本太高内部依赖跟 JavaFX 的某些模块冲突了后来验证完发现版本高只是表象真正的问题在自动配置类的注册方式变了而我的打包流程没有跟上。1.3 报错里为什么会点名“custom packaging”这就要说到 Spring Boot 自动配置的加载机制了。AutoConfigurationImportSelector在拿到当前 ClassLoader 之后会从两个位置读取自动配置类清单META-INF/spring.factories文件中的EnableAutoConfiguration键META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件如果两个位置都没有任何类它就会抛出刚才那个异常。此时框架心里很清楚常规执行java -jar的时候spring-boot-autoconfigure包里的配置一定存在既然找不到多半是你自己搞了某种“自定义打包”方式把资源配置弄丢了。JavaFX 项目正是“自定义打包”的重灾区。习惯上我们会用javafx-maven-plugin或jpackage做原生镜像而不是直接用 Spring Boot 官方插件打可执行 fat jar。这类工具对 classpath 和资源文件的处理逻辑各不相同一旦某个META-INF资源没被拷贝进去就会精准踩中这个雷。2. 先搞懂“自动配置类”是怎么被找到的2.1 spring.factories 时代的加载流程在 Spring Boot 2.6 及之前自动配置类的入口非常单一META-INF/spring.factories。这个文件以 properties 格式保存内容类似下面这样org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.starter.MyAutoConfiguration,\ com.example.starter.SecondAutoConfigurationSpringFactoriesLoader会扫描 classpath 下所有 jar 包里的META-INF/spring.factories挑出 key 为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值作为候选自动配置类列表交给AutoConfigurationImportSelector做条件过滤。为什么 Spring Boot 很强大核心之一就在这一步框架本身只提供一批“候选”真正加载哪几个取决于ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty等条件注解。你引入某个依赖对应自动配置类就被激活没引入它安静地待在候选清单里也不会有副作用。2.2 2.7 版本开始为什么要换成 imports 文件spring.factories是一个被很多组件共用的“万能袋”凡是需要 SPI 机制的地方都用它。比如org.springframework.context.ApplicationContextInitializer、org.springframework.boot.env.EnvironmentPostProcessor甚至连 Spring 生态外部的一些框架也可能往这个文件里塞内容。随着自动配置类的数量增长一个中型 Starter 里几十上百个配置类很正常这个文件的维护成本和冲突概率都在上升。Spring Boot 2.7 引入了专属于自动配置的独立文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。格式比 properties 简单得多每行一个类名com.example.starter.MyAutoConfiguration com.example.starter.SecondAutoConfiguration com.example.starter.ThirdAutoConfiguration注意目录层级在META-INF下面多了一层spring文件路径是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。这样做的好处是自动配置类清单和通用配置彻底分开既清晰又不容易被别的 SPI 机制误读。2.3 不同主版本下的加载行为对比很多人在 IDEA 创建 Spring Boot 项目的时候根本不关心版本差异反正选最新的就完事了。但自动配置类的读取逻辑在不同版本下是有本质区别的我用一张表对比给你看Spring Boot 版本读取spring.factories读取AutoConfiguration.imports缺失时的报错路径2.6.x 及以下支持唯一来源不支持META-INF/spring.factories2.7.x支持兼容有告警支持优先使用可能是spring.factories或 imports3.0.x 及以上不支持唯一来源META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports也就是说如果你在 Spring Boot 3.x 里启动看到的报错文本里META-INF/spring.factories可能已经变成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。但报错逻辑是一样的候选自动配置类集合为空。很多第三方 Starter 升级慢到 Spring Boot 3 时代还只提供spring.factories这是另一个常见的踩坑点。不过本文主角是 JavaFX 项目自身的打包问题第三方 Starter 的兼容性问题这里就不展开了。2.4 这个文件在打包时为什么容易“消失”自动配置类被加载的前提是spring-boot-autoconfigure这个 jar 包完整地存在于运行环境里。这个 jar 包里不仅包含了成百上千个自动配置类的字节码也包含了两个很重要的元数据文件META-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports当你做常规 Spring Boot 项目时spring-boot-maven-plugin的repackage目标会把所有依赖 jar 合并成一个大 fat jar保留原有的目录结构。JavaFX 项目不一样你大概率会改用javafx-maven-plugin或者jpackage来打包这个过程可能只复制了 class 文件却没有把META-INF/spring目录完整地装进产物。还有一种情况是项目里用了maven-assembly-plugin做自定义组装它的transformer配置如果漏掉了ServicesResourceTransformer就会导致多个 jar 里的同名 SPI 文件互相覆盖最后产出一个内容不完整的spring.factories。而AutoConfiguration.imports这种新路径老版本的工具甚至是第一次见不写专门配置根本不会拷贝。3. 四类修复方案按触发原因分类处理3.1 补全 spring-boot-autoconfigure 依赖先别急着改代码第一步要确认你的运行环境里到底有没有spring-boot-autoconfigure。这个模块是框架的核心之一正常情况下只要你引入了spring-boot-starter-xxx系列依赖它就会被自动带进来。但 JavaFX 项目里有个很典型的误操作为了减少依赖体积手动用exclusions排除了某些传递依赖或者干脆只引入了spring-boot没有引入任何spring-boot-starter。如果你走的是这种极端精简路线spring-boot-autoconfigure很可能压根不在 classpath 里自动配置类自然一个都找不到。在 Maven 项目里执行一下这条命令能一目了然地看到依赖情况mvn dependency:tree -Dincludesorg.springframework.boot:spring-boot-autoconfigure正常输出应该类似[INFO] - org.springframework.boot:spring-boot-autoconfigure:jar:3.1.2:compile如果你看到omitted for conflict或者完全没有这个节点那问题基本定位到了。补依赖的方式很简单dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-autoconfigure/artifactId /dependency前提是你们项目用spring-boot-dependencies做了统一的版本管理不然记得补version标签。3.2 修正 Maven/Gradle 打包对 META-INF 的处理假设依赖完整无缺那问题基本出在“依赖明明在但打出来的产物里没有对应文件”这种情况。先看一个非常容易出错的maven-assembly-plugin配置。很多人写自定义 assembly 描述符时只关注把什么目录打进包忽略了 jar 合并时的资源冲突处理plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId configuration descriptors descriptorsrc/assembly/bin.xml/descriptor /descriptors /configuration /plugin如果bin.xml里用unpacktrue/unpack把依赖解压后再重打包就需要额外配置一个资源转换器把多个 jar 里的META-INF/spring.factories和META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容进行合并containerDescriptorHandlers containerDescriptorHandler handlerNamemetaInf-services/handlerName /containerDescriptorHandler /containerDescriptorHandlers但说实话JavaFX 项目我更推荐走正规军路线——用javafx-maven-plugin配合spring-boot-maven-plugin把 Spring Boot 可执行 jar 作为中间产物再用jpackage二次封装。这样既能保留 Spring Boot 的启动逻辑又不用担心资源文件丢失。下面是我后来在项目里用的两段插件配置验证过可以正确产出可运行包plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId executions execution goals goalrepackage/goal /goals /execution /executions /pluginplugin groupIdorg.openjfx/groupId artifactIdjavafx-maven-plugin/artifactId version0.0.8/version configuration mainClasscom.example.Launcher/mainClass /configuration /plugin3.3 JavaFX 模块化工程的 module-info 处理如果你的 JavaFX 项目引入了module-info.java还需要额外小心模块系统的边界问题。JavaFX 从 Java 9 开始就要求显式声明模块依赖而 Spring Boot 3.x 本身对模块化的支持一直是“可以用但不是特别丝滑”的状态。一个常见问题是模块化打包使用了jlink但jlink只会链接它认为“被使用”的模块。假如你的module-info.java没有正确声明依赖spring-boot-autoconfigure内部的包可能整个被裁剪掉到时候连AutoConfiguration.imports文件都不会被输出。这类问题的排查思路是添加 Java 启动参数来观察模块解析情况java --show-module-resolution -jar your-app.jar如果发现spring.boot.autoconfigure没有被加载就去检查module-info.java里的requires声明把 Spring Boot 相关的模块依赖补上。不过我这里还是要说一句桌面项目如果非必要不建议上模块化。JavaFX Spring Boot 本身跨了两个体系再加模块化等于把三个复杂度叠加在一起寻找问题的难度会翻倍。3.4 换一种 Spring Boot 启动方式还有一种不太容易想到的情况你确实在 JavaFX 应用里调用了SpringApplication.run()但调用的位置不对导致 Spring 容器根本没有机会完成自动配置加载。举个例子很多新手喜欢在main()方法里既启动 Spring Boot又调用 JavaFX 的launch()public static void main(String[] args) { SpringApplication.run(Application.class, args); Application.launch(JavaFxApplication.class, args); }这段代码的隐患是SpringApplication.run()如果因为某种原因没有进入正常的容器刷新流程后续的 JavaFX 启动就会在残缺的环境里运行。更推荐的做法是把 Spring Boot 的启动挪到 JavaFX 的init()方法里用SpringApplicationBuilder精细控制这样两个框架的生命周期可以自然衔接。4. 完整排查链路从报错到定位根因只用了五步4.1 第 1 步确认 Spring Boot 版本遇到任何诡异报错第一件事永远是确认版本。我的项目升级到了 3.1.2但同事的机器上 Spring Boot 还是 2.5.6两个人跑出来的结果完全不一样。在 pom.xml 里检查 spring boot parent 版本或者直接执行mvn help:evaluate -Dexpressionproject.parent.version -q -DforceStdout这一步能让你明确当前处于“版本迁移的哪一端”。如果是 2.7 以下的旧版本报错只可能来自spring.factories如果是 3.x 版本报错文件路径大概率已经指向 imports但提示语义完全相同。4.2 第 2 步查看依赖树确认版本之后紧接着看依赖树最有效。我用的时候会把所有 Spring Boot 相关的依赖全部打出来mvn dependency:tree -Dincludesorg.springframework.boot重点确认两点spring-boot-autoconfigure是否在依赖树里scope 是否为compile是否存在多个版本的spring-boot-autoconfigure比如 2.7.x 和 3.1.x 并存版本冲突在依赖树里会显示为omitted for conflict这个信息非常关键。如果 3.1.2 的spring-boot-autoconfigure被 2.7.x 版本顶掉加载逻辑就会用老版本路径自然对不上。4.3 第 3 步解压产物检查文件第三步是最直观的物理验证——直接打开打出来的 jar 包看META-INF目录里到底有哪些文件jar tf your-app.jar | grep META-INF/spring正常的 fat jar 应该能看到类似下面的路径META-INF/spring.factories META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports如果jar tf结果为空说明打包过程中资源文件被丢弃了直接锁定打包插件配置问题。如果用的是自定义 assembly需要回头检查资源文件包含规则。4.4 第 4 步观察类加载过程有时候肉眼检查文件存在但运行时就是加载不到这种情况多半是类加载器搞的鬼。JavaFX 应用经常有自己的类加载边界尤其在使用某些自定义启动器时Thread.currentThread().getContextClassLoader()可能指向了一个不含 Spring Boot 配置的 ClassLoader导致SpringFactoriesLoader扫不到任何资源。加上 JVM 参数看启动时到底从哪里加载了AutoConfigurationImportSelectorjava -verbose:class -jar your-app.jar 21 | grep AutoConfiguration正常会输出类似[Loaded org.springframework.boot.autoconfigure.AutoConfigurationImportSelector from file:.../spring-boot-autoconfigure-3.1.2.jar]如果这里显示的是system classloader之外的东西或者压根在非 fat jar 模式下加载就要检查你的启动类结构了。4.5 第 5 步对比差异确定根因我在排查时做了一个非常关键的对比直接用 IDEA 跑main()方法没问题用spring-boot:run也没问题唯独用javafx-maven-plugin的jlink打包后再启动必现报错。这个“能跑”与“不能跑”的差异让我很快锁定了方向——Spring Boot 官方打包和 JavaFX 原生打包对META-INF/spring资源的处理策略不同。前者有完备的 repackage 逻辑会主动保留并合并资源后者不会处理这些 SPI 资源。最终我在自定义产物里发现 imports 文件确实缺失同时因为 Spring Boot 3.x 放弃了spring.factories所以报错信息仍然引用旧路径这可能是框架兼容层留下的“旧文案残留”但不影响定位。5. JavaFX Spring Boot 集成的标准姿势5.1 类和 main 方法不要混在一起我之前接手过一个项目把SpringBootApplication直接标在了继承Application的类上main()里既跑 Spring Boot 又跑launch()。这个写法在开发环境大概率能跑通但你会被各种奇怪问题折磨到怀疑人生。推荐的结构是三段式分离// 1. 纯 JavaFX 入口不要放任何 Spring Boot 注解 public class Launcher { public static void main(String[] args) { Application.launch(JavaFxApplication.class, args); } } // 2. Spring Boot 配置类不继承任何 JavaFX 类 SpringBootApplication public class SpringBootApp { } // 3. JavaFX Application 实现只负责初始化容器和拉起界面 public class JavaFxApplication extends Application { private ConfigurableApplicationContext applicationContext; Override public void init() { applicationContext new SpringApplicationBuilder(SpringBootApp.class) .headless(false) .web(WebApplicationType.NONE) .run(); } Override public void start(Stage primaryStage) { MainController controller applicationContext.getBean(MainController.class); controller.show(primaryStage); } Override public void stop() throws Exception { applicationContext.close(); } }这样每个类的职责都很清晰排查问题时能迅速定位。5.2 不要漏掉 headless(false)Spring Boot 默认的运行环境是服务器场景所以它会把java.awt.headless设为true。但 JavaFX 是需要窗口环境的应用如果不关闭 headless 模式窗口组件初始化时会直接抛HeadlessException。上面的SpringApplicationBuilder配置里.headless(false)是关键的一环。如果你习惯用SpringApplication.run()也有对应的设置方式SpringApplication app new SpringApplication(SpringBootApp.class); app.setHeadless(false); app.setWebApplicationType(WebApplicationType.NONE); ConfigurableApplicationContext context app.run(args);5.3 Spring Boot 2.7 和 3.x 怎么选说实话JavaFX 桌面应用对 Spring Boot 新特性的依赖程度远低于 Web 项目。如果你的团队已经稳定运行在 2.7.x我不建议为了升级而升级。2.7 本身处于自动配置机制迁移的过渡带既能识别新的 imports 文件又保留了对spring.factories的兼容。但如果你是新项目直接上 3.x 也没问题前提是两类坑必须提前规避第三方的 Starter 是否已经适配 Spring Boot 3很多老库还停留在 2.xjavax到jakarta的包名迁移会让老配置直接编译失败打包流程是否验证过AutoConfiguration.imports文件能正常打进产物我个人现在的倾向是新项目一律用 Spring Boot 3.1但打包流程必须先做一次“空跑验证”也就是打完包后解压检查META-INF/spring目录确认关键资源存在再交付。最后补一个我踩过的附加坑报错解决了之后我还遇到一个连带问题点击Controller里的按钮时弹出窗口的样式表加载失败原因是我把resources/static目录当普通资源打包结果自定义打包工具没把 CSS 和 FXML 文件拷贝进去。这个问题的本质和刚才的自动配置类完全一样——自定义打包流程对资源文件的理解往往只停留在.class和常规资源不会主动处理框架级元数据。所以做完修复之后我养成了一个习惯每次更换打包插件或升级 Spring Boot 版本都会在验证清单里加一条“解压产物、检查 META-INF 目录、检查 FXML/CSS 是否在位”。如果你的项目也踩到同一个报错建议按我第五节的标准姿势重构一次启动类再检查打包配置大概率能一次搞定。如果改完之后仍然报错不妨把mvn dependency:tree的输出和jar tf的结果发出来一起看看很多细节不能光靠经验猜。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →