☰
missing ServletWebServerFactory 报错解析:Spring Boot 内嵌容器与依赖排查实战
2026/10/8 20:24:21 网站建设 项目流程

如果你刚在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 bean

Spring Boot 3.x 的包名从javax换成jakarta,但异常名字基本一样。本质就是:Spring 在启动过程中尝试创建一个内嵌的 Servlet 容器(Tomcat、Jetty、Undertow),结果容器上下文里根本找不到对应的ServletWebServerFactory这个 Bean,于是直接放弃启动。

1.2 正常启动应该是什么样的

要理解这个错误,得先知道 Spring Boot 启动一个 Web 应用时到底做了什么。简化到关键三步:

  1. SpringApplication.run(...)启动后,根据 classpath 里的类型判断当前应用是 Servlet Web 应用、Reactive Web 应用,还是非 Web 应用。
  2. 如果判定为 Servlet Web 应用,Spring 会创建ServletWebServerApplicationContext,而不是普通的AnnotationConfigApplicationContext。这个ApplicationContext负责管理业务 Bean,同时还要创建内嵌 Web 服务器。
  3. 创建 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.TomcatTomcatServletWebServerFactory
org.eclipse.jetty.server.ServerJettyServletWebServerFactory
io.undertow.UndertowUndertowServletWebServerFactory
什么都没匹配到无,启动时缺 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实现类。

排查时可以问自己三个问题:

  1. 我现在要跑的是 war 包还是 jar 包?
  2. 我是打算丢到外部 Tomcat,还是用 IDEA 直接内嵌启动?
  3. 如果是内嵌启动,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、清缓存、换个姿势启动

如果上面几步都查不出问题,我建议执行一套组合操作:

  1. Maven 面板点小刷新,Reload All Maven Projects;
  2. 命令行执行mvn clean,把target目录清干净;
  3. 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 再硬加回来,这会破坏部署约定。更合理的做法是下面几种:

  1. 给这个启动类专门保留内嵌环境:单独维护一个开发用的启动类,让它在本地依赖内嵌 Tomcat,生产包保持外部部署结构。
  2. 别用 main 直接启动,改成本地 Tomcat 配置或者远程 Debug,让运行方式与部署方式保持一致。
  3. 如果就是想本地快速验证接口,可以把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 长期预防:五个值得养成的小习惯

踩过几次这类坑之后,我给自己定了几条规矩,分享给你:

  1. 每次改 pom 后主动 reimport,不要等 IDEA 自己刷新。手动点 Maven 面板右上角的刷新图标,也就一秒的事。
  2. 不在 pom 里随意排除spring-boot-starter-tomcat。如果真要外部部署,把相关变更放到专门的 profile 里,并确保开发运行方式另有出路。
  3. 启动前先查一次依赖树,特别是隔离环境、换机器、切分支之后。命令就那两行,花不了多少时间。
  4. 多模块项目里保持依赖可见性。尽量少用optional=true来偷懒,它会直接斩断传递依赖,坑到后面接手的人。
  5. 用自动配置报告作为最终裁判。任何“我觉得应该有依赖”的争论,打印一份 Conditions Report 出来,看到具体的匹配点和失败原因,谁也狡辩不了。

最后再分享一个个人习惯:我遇到missing ServletWebServerFactory这类问题,一定会先分清楚“我在用什么方式启动”“这个项目原本打算怎么部署”“运行 classpath 里到底有什么”。这三个问题想清楚,绝大多数情况下,你连代码都不用改,只是改了一种与该模块设计匹配的启动方式而已。希望这篇东西能帮你少烧几根头发。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询