如果你刚在IDEA里点下那个绿色的启动按钮,控制台没刷两行就给你抛出一行带着missing ServletWebServerFactory的红色异常,心态多半已经裂开了。这个关键词翻译过来就是:你想让 Spring Boot 跑一个 Web 应用,但运行时 classpath 里找不到任何能创建 Servlet 容器的工厂类。它既不是代码编译错误,也不是端口被占,而是依赖和作用域在启动前就出了问题。这篇内容就是围绕这个报错的完整拆解,从异常含义、Spring Boot 启动原理,到 IDEA 里的实操排查和修复,适合正在被这个错误折磨、或者想搞清楚以后怎么避免的朋友。
1. 现象定位:missing ServletWebServerFactory到底卡在哪一步
1.1 报错长什么样
完整异常通常长得像这样,Spring Boot 2.x 和 3.x 在堆栈细节上略有差异,但核心信息一致:
*************************** APPLICATION FAILED TO START *************************** Description: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.再往上翻,往往还能看到:
org.springframework.boot.web.server.WebServerException: Unable to start web server at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onStartup(ServletWebServerApplicationContext.java:...) ... Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory beanSpring Boot 3.x 的包名从javax换成jakarta,但异常名字基本一样。本质就是:Spring 在启动过程中尝试创建一个内嵌的 Servlet 容器(Tomcat、Jetty、Undertow),结果容器上下文里根本找不到对应的ServletWebServerFactory这个 Bean,于是直接放弃启动。
1.2 正常启动应该是什么样的
要理解这个错误,得先知道 Spring Boot 启动一个 Web 应用时到底做了什么。简化到关键三步:
SpringApplication.run(...)启动后,根据 classpath 里的类型判断当前应用是 Servlet Web 应用、Reactive Web 应用,还是非 Web 应用。- 如果判定为 Servlet Web 应用,Spring 会创建
ServletWebServerApplicationContext,而不是普通的AnnotationConfigApplicationContext。这个ApplicationContext负责管理业务 Bean,同时还要创建内嵌 Web 服务器。 - 创建 Web 服务器时,
ServletWebServerApplicationContext会从 IoC 容器里获取一个ServletWebServerFactory类型的 Bean。拿到工厂之后,再调用工厂的getWebServer(ServletContextInitializer...)方法,实例化 Tomcat、Jetty 或 Undertow,绑定端口并启动。
而ServletWebServerFactory并不是某个业务 Bean,它是 Spring Boot 的自动配置在运行期“看情况”注册的。什么情况下注册?classpath 里正好有某个内嵌容器的实现类,且当前项目被判定为 Servlet Web 应用时才注册。一旦没有这个 Bean,第三步必然抛错。
所以,这个错误和你的业务代码几乎无关,它出在“依赖结构”和“Spring Boot 自动配置是否被触发”的交界处。排查方向应该锁定 pom 文件、依赖作用域、模块关系和 IDE 的 classpath 配置,而不是对着@SpringBootApplication发呆。
2. 根因拆解:为什么 Spring Boot 就是找不到这个工厂 Bean
2.1 自动配置的触发条件没那么简单
ServletWebServerFactory这个 Bean 的注册,核心在ServletWebServerFactoryAutoConfiguration这个自动配置类里。Spring Boot 的自动配置有大量条件控制,可以看这几个关键点:
@Configuration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(ServletRequest.class) @ConditionalOnWebApplication(type = Type.SERVLET) @EnableConfigurationProperties(ServerProperties.class) public class ServletWebServerFactoryAutoConfiguration { ... }这里有两个硬条件:
@ConditionalOnClass(ServletRequest.class):classpath 里必须有javax.servlet.http相关的 Servlet API 类。@ConditionalOnWebApplication(type = Type.SERVLET):当前应用必须被判定为 Servlet Web 应用。
别小看第二个条件。如果一个项目里只有spring-boot-starter,没有任何 spring-web 依赖,那它会被判定成非 Web 应用,根本走不到ServletWebServerApplicationContext这一步,自然也不会报这个错。换句话说,报出missing ServletWebServerFactory,说明你的 classpath 里已经有 Servlet API 相关类,却又缺少实际的容器实现,这就比单纯的非 Web 应用要尴尬一些。
2.2 具体工厂 Bean 是怎么“长”出来的
ServletWebServerFactoryAutoConfiguration内部并没有写死TomcatServletWebServerFactory,而是进一步委托给内部的ServletWebServerFactoryConfiguration(Spring Boot 1.x 里叫EmbeddedServletContainerAutoConfiguration)。这个配置类会根据 classpath 里有哪些容器类,注册对应的工厂 Bean:
| classpath 里存在的关键类 | 被注册的工厂 Bean |
|---|---|
org.apache.catalina.startup.Tomcat | TomcatServletWebServerFactory |
org.eclipse.jetty.server.Server | JettyServletWebServerFactory |
io.undertow.Undertow | UndertowServletWebServerFactory |
| 什么都没匹配到 | 无,启动时缺 Bean |
所以,如果 classpath 里连Tomcat这个类都找不到,Spring Boot 自然无法得知该用哪种容器来创建ServletWebServerFactory。它宁可报错,也不会替你猜。
2.3 “缺失”和“被排除”是两种不同境遇
很多人以为“缺依赖”和“依赖被排除”结果一样,其实排查路径不一样。
- 真缺失:pom 里从头到尾就没有
spring-boot-starter-web,或者连 Servlet API 都没有。这种情况往往直接判定为非 Web 应用,不一定会报这个错。 - 被排除:你确实引入了
spring-boot-starter-web,但里面通过<exclusions>把spring-boot-starter-tomcat排掉,或者某个上层模块把 Tomcat 标记成了provided。这时 Servlet API 还在,spring-webmvc 也在,但内嵌容器类不在运行 classpath,自动配置里@ConditionalOnClass(org.apache.catalina.startup.Tomcat.class)匹配不上,工厂 Bean 就没了。
我见过最典型的 case 是:一个原本要打包成 war 部署到外部 Tomcat 的项目,pom 里排除了spring-boot-starter-tomcat,平时用外置 Tomcat 调试没毛病。某天你想在 IDEA 里直接右键 main 方法跑一下,就会撞上这个错。不是代码有问题,是你把启动环境从“外部容器”偷偷换成了“内嵌容器启动”,而 classpath 根本没跟上。
2.4 别忽略依赖作用域:provided 这个隐形杀手
Spring Boot 的spring-boot-starter-tomcat依赖中,tomcat-embed-core等组件默认是 compile 作用域,会正常进入 IDEA 运行时的 classpath。一旦你为了部署到外部 Tomcat,把它改成provided,IDEA 在运行 Application 配置时默认不会把 provided 依赖放进运行 classpath,除非你手动调整。于是启动时和“被 exclusion”的表现完全一样。
注意,Maven 的dependency:tree默认也能看到标记为 provided 的依赖,可能会给你一种“依赖明明在啊”的错觉。但运行时不见得带上。这也是后文要专门强调“以运行 classpath 为准”的原因。
3. 依赖与模块排查:四类最常见的翻车点
3.1 场景一:Web 依赖链是“半成品”,没有完整容器实现
如果你新建模块时没有主动引入spring-boot-starter-web,但某个传递依赖又把 spring-web 带了进来,就会出现这种半吊子状态:Servlet API 在,内嵌容器不在,ServletRequest能被类加载器找到,于是 Spring Boot 把你判定为 Web 应用,却拿不出容器工厂。
这种情况下,最干净的解法是在启动模块的 pom 里补上:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>spring-boot-starter-web会同时引入 spring-webmvc、内嵌 Tomcat、默认 JSON 处理等,问题基本烟消云散。
3.2 场景二:排除了 spring-boot-starter-tomcat,却仍然用 main 直接启动
这是我在实际项目中遇到最多的场景。pom 长这样:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>这段代码本身没有错,它常用于“war 包部署到外部 Tomcat”的场景。但如果你之后直接运行带main方法的启动类,问题就来了。IDEA 跑 main 时走的是内嵌容器路径,而排除 Tomcat 之后,项目里又没有 Jetty、Undertow 等替代容器,classpath 里根本没有ServletWebServerFactory实现类。
排查时可以问自己三个问题:
- 我现在要跑的是 war 包还是 jar 包?
- 我是打算丢到外部 Tomcat,还是用 IDEA 直接内嵌启动?
- 如果是内嵌启动,classpath 里到底有没有一个可用的容器工厂?
如果代码里还背着SpringBootServletInitializer,说明这个项目很可能同时保留着外置部署能力。IDEA 直接启动 main 时,走的却是内嵌路径,二者对容器的诉求完全不同。
3.3 场景三:多模块 Maven 项目中,web 依赖落在非运行模块
多模块项目的报错更隐蔽。假设你有common、service、web、app四个模块,app是最终启动模块,web是控制层模块。如果 Web 相关 starter 放在common模块里,common又被service依赖,service又被app依赖,理论上依赖会传递,但一旦有模块标记了<optional>true</optional>,传递链就断了。
更常见的是,web模块明明引入了spring-boot-starter-web,但app模块的 pom 里并没有依赖web模块;或者你依赖的是web模块编译后的 jar,但那个 jar 里没有把 web 依赖传递出来。这时 IDEA 编译时能用到 Servlet API,运行时的 classpath 却捞不到完整容器链,启动必然失败。
用 Maven 依赖树一眼可以看出端倪:
mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-web,org.springframework.boot:spring-boot-starter-tomcat这个命令会明确告诉你,web starter 和 tomcat starter 是否真的出现在主模块的运行依赖链上。如果没有,顺着模块关系逐个 pom 查,多半能发现某个<optional>或缺少的<dependency>。
3.4 场景四:IDEA 本地缓存与 target 残留导致的“幽灵依赖”
还有一种情况,pom 看起来全对,mvn dependency:tree里也清晰无误,但 IDEA 启动就是报错。原因可能是 IDEA 的 Maven 索引没有同步,项目结构还是旧的。比如你刚在 pom 里加了一个依赖,IDEA 却没有自动 reimport;或者旧target目录里残留了一些旧的 Spring Boot jar,IDE 里显示的 Maven dependencies 和真实的运行 classpath 不一致。
这不算严格的依赖错误,但确实是missing ServletWebServerFactory在 IDEA 环境里的常见诱因。处理方式很朴素:先执行一次 IDEA 的 Reload All Maven Projects,再执行一次mvn clean,然后再启动。如果项目长时间没清过缓存的,还可以在 IDEA 里选择 File -> Invalidate Caches / Restart,把索引全部重建一遍。
4. 别急着加依赖:先按这个顺序自查 IDEA 工程配置
4.1 第一步:用 Maven 依赖树验证 classpath
接到这个报错,第一件事不要是加spring-boot-starter-web,而是先看清当前 classpath 里有什么、没什么。
在项目根目录执行:
mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-tomcat,org.apache.tomcat.embed:tomcat-embed-core,org.springframework:spring-webmvc如果看到类似输出:
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.18:compile [INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.7.18:compile [INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.83:compile说明容器依赖是完整的。
如果dependency:tree里只有spring-boot-starter-web、spring-webmvc,却没有spring-boot-starter-tomcat或tomcat-embed-core,那大概率是依赖被排除了,或者没有传递过来。
更进一步,可以用-Dverbose看依赖引入路径:
mvn dependency:tree -Dverbose -Dincludes=org.apache.tomcat.embed:tomcat-embed-core它会打出“由谁引入”“经过哪些排除规则”的完整链路,排查 exclusion 非常管用。
4.2 第二步:检查 IDEA 的 Maven 面板和运行 classpath
如果你更习惯用 IDEA,打开右侧 Maven 面板,展开主模块的 Dependencies,直接搜tomcat-embed-core。如果没搜到,但命令行里dependency:tree有,多半是 IDEA 的依赖没 reimport。右键主模块 -> Reload Maven Project,让它重新解析一次。
IDEA 的运行配置默认会用“模块的 classpath”,你在 Run Configuration 里看一眼:
- Main class 是不是你要运行的那个带
main方法的类? - Use classpath of module 是不是选到了正确的模块?
尤其是在多模块项目里,如果启动类在app模块,但 Use classpath of module 误选成了某个纯工具模块,缺失一大堆依赖,报这个错就太正常了。
另外要注意 IDEA 对 provided scope 的处理。老版本 IDEA 运行 Application 时,默认不会带上 provided 依赖。如果你恰好把 Tomcat 相关依赖标成了 provided,可以在 Run Configuration 里调整模块依赖的 Scope,但最干净的还是在 pom 层解决,别靠改 IDE。
4.3 第三步:看自动配置报告,确认哪一步没成功
Spring Boot 允许你在启动时打印自动配置报告。最省事的方法,在启动参数里加上--debug:
--debug或者临时在application.properties里:
debug=true启动后,日志里会打印 “CONDITIONS EVALUATION REPORT”。重点找 Negative matches 下面的ServletWebServerFactoryAutoConfiguration,以及ServletWebServerFactoryConfiguration.EmbeddedTomcat。如果条件不满足,报告会明确提示 did not find any classes with:org.apache.catalina.startup.Tomcat。
这份报告比任何猜测都直观,一句话就能告诉你到底缺哪个类。排查完记得把debug=true关掉,不然日志刷得怀疑人生。
4.4 第四步:清理 target、清缓存、换个姿势启动
如果上面几步都查不出问题,我建议执行一套组合操作:
- Maven 面板点小刷新,Reload All Maven Projects;
- 命令行执行
mvn clean,把target目录清干净; - IDEA 里 File -> Invalidate Caches / Restart,重启 IDEA。
然后不要急着点启动按钮,先在 Maven 面板的 Plugins 里找到spring-boot:run,用mvn spring-boot:run启动一次。这样能区分是 IDEA 的 classpath 问题,还是项目本身依赖问题。
- 如果用
mvn spring-boot:run启动成功,说明 pom 其实没问题,问题出在 IDEA 的工程配置或缓存。 - 如果
mvn spring-boot:run也报同样的错,那就是 pom 依赖本身不对,继续回到第 3 部分排查。
这一招很管用,逻辑上相当于做了一次双盲比对,能避免被 IDE 误导。
5. 修复方案与验证:从临时止血到长期预防
5.1 纯 Web 应用:直接补 spring-boot-starter-web
如果你本身就是要跑一个标准的 MVC Web 服务,最稳的操作是在启动模块的 pom 里加入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>加上后,不要急着启动。先执行mvn clean,再执行一次mvn dependency:tree -Dincludes=org.apache.tomcat.embed:tomcat-embed-core确认容器类已经被带进来,最后重新启动。
注意:Spring Boot 3.x 里使用的还是spring-boot-starter-web这个坐标,名字没变,底层从javax.servlet换成了jakarta.servlet,不影响这里的大原则。
5.2 外部部署型项目:用 Spring Boot 外置容器部署的正确姿势
如果项目真正的部署方式是 war 包扔到外部 Tomcat,那么在 IDEA 里直接跑 main 确实不是最佳选择。但我不建议为了能在 IDEA 跑,把本来排掉的 Tomcat 再硬加回来,这会破坏部署约定。更合理的做法是下面几种:
- 给这个启动类专门保留内嵌环境:单独维护一个开发用的启动类,让它在本地依赖内嵌 Tomcat,生产包保持外部部署结构。
- 别用 main 直接启动,改成本地 Tomcat 配置或者远程 Debug,让运行方式与部署方式保持一致。
- 如果就是想本地快速验证接口,可以把
spring-boot-starter-tomcat的 scope 从 provided 改回 compile,打包时再用 profile 动态排除。具体要结合团队构建流水线,别自己拍脑袋。
个人经验是,在 Spring Boot 项目里,除非确实需要把 web 容器交给外部管理,否则尽量保留内嵌容器。现在很多微服务都以 jar 包方式独立部署,内嵌容器的运维成本和复杂度低得多。
5.3 如果你真的想用非 Web 容器启动怎么办
有一种情况是:你只是想跑一个 Spring Boot 后台任务、定时调度器,并不需要 HTTP 端口。那正确的做法不是去补容器,而是确保 classpath 里没有引发 Web 判定的 Servlet API。
检查一下 pom 里有没有spring-boot-starter-web、spring-boot-starter-webflux这类 Web starter。没有的话,Spring Boot 会启动为非 Web 应用,自然也不需要ServletWebServerFactory。但如果你只是想暂时跳过 Web 环境,可以临时把启动方式改成:
SpringApplication app = new SpringApplication(Application.class); app.setWebApplicationType(WebApplicationType.NONE); app.run(args);注意,这是一个“临时手段”。如果包依赖里有 Servlet API,手动指定 NONE 后会跳过 Web 服务器创建,但某些自动配置仍然会尝试绑定 Web 相关属性,所以不如从 pom 层去掉 Web 依赖来得干净。
5.4 修复后的启动日志长什么样
修复后,启动日志里会有一行很关键的信息。Spring Boot 2.x 通常是:
Tomcat started on port(s): 8080 (http) with context path ''Spring Boot 3.x 则会看到:
Tomcat initialized with port(s): 8080 (http)看到这行,说明内嵌容器已经成功创建,ServletWebServerFactory这个 Bean 已经被自动配置注册并使用。如果你之前临时加了debug=true,再去看自动配置报告,会发现原来的 Negative matches 变成了 Positive matches 里的ServletWebServerFactoryAutoConfiguration、ServletWebServerFactoryConfiguration.EmbeddedTomcat,心里就有底了。
5.5 长期预防:五个值得养成的小习惯
踩过几次这类坑之后,我给自己定了几条规矩,分享给你:
- 每次改 pom 后主动 reimport,不要等 IDEA 自己刷新。手动点 Maven 面板右上角的刷新图标,也就一秒的事。
- 不在 pom 里随意排除
spring-boot-starter-tomcat。如果真要外部部署,把相关变更放到专门的 profile 里,并确保开发运行方式另有出路。 - 启动前先查一次依赖树,特别是隔离环境、换机器、切分支之后。命令就那两行,花不了多少时间。
- 多模块项目里保持依赖可见性。尽量少用
optional=true来偷懒,它会直接斩断传递依赖,坑到后面接手的人。 - 用自动配置报告作为最终裁判。任何“我觉得应该有依赖”的争论,打印一份 Conditions Report 出来,看到具体的匹配点和失败原因,谁也狡辩不了。
最后再分享一个个人习惯:我遇到missing ServletWebServerFactory这类问题,一定会先分清楚“我在用什么方式启动”“这个项目原本打算怎么部署”“运行 classpath 里到底有什么”。这三个问题想清楚,绝大多数情况下,你连代码都不用改,只是改了一种与该模块设计匹配的启动方式而已。希望这篇东西能帮你少烧几根头发。