很多人调试Spring Boot应用时,经常只看到SpringApplication.run()一行代码,然后应用就起来了。但如果你真正想知道内嵌Web容器是怎么启动的,就绕不开ServletWebServerApplicationContext这个类。它是Spring Boot内嵌Web容器启动机制的核心调度者,今天我把它拆开揉碎讲清楚,包括源码流程、关键参数、坑点排查和扩展方式。
这篇文章适合两类人:一类是刚接触Spring Boot、想了解自动配置背后逻辑的读者,另一类是已经在做Spring Boot二次开发、想定制内嵌Tomcat/Jetty/Undertow的老手。不管你是哪一类,看完都能对“应用启动时Web容器到底经历了什么”有一个全景认识。
1. 先搞清楚ServletWebServerApplicationContext在Spring Boot里是个什么角色
1.1 一段启动日志背后的隐藏角色
我们先从最熟悉的启动日志说起。运行一个普通的Spring Boot Web应用,控制台会输出类似这样的内容:
Tomcat initialized with port(s): 8080 (http) Starting service [Tomcat] Starting Servlet engine: [Apache Tomcat/9.0.x] Tomcat started on port(s): 8080 (http) with context path '' Started DemoApplication in 2.345 seconds这里每一条日志背后都有ServletWebServerApplicationContext的身影。它不是你的业务代码,而是Spring容器体系中的一个专用ApplicationContext实现。Spring Boot在检测到classpath里有Web容器相关依赖时,就会优先创建它,而不是普通的AnnotationConfigApplicationContext。
简单理解:ApplicationContext是Spring的IoC容器,ServletWebServerApplicationContext是专门为“Servlet Web应用”场景设计的IoC容器。它除了管理Bean、处理依赖注入之外,额外承担了一件关键任务——在容器刷新过程中“顺便”创建并启动内嵌Web服务器。
1.2 为什么Spring Boot不直接使用通用ApplicationContext
这个问题值得展开。传统Spring Web应用部署到外部Tomcat时,ApplicationContext由DispatcherServlet或ContextLoaderListener创建,Web服务器由外部启动,两者通过ServletContext和web.xml建立联系。
但Spring Boot走的是“内嵌服务器”路线,WebServer(比如Tomcat、Jetty、Undertow)和Spring容器要在同一个Java进程里存活。问题来了:Web服务器初始化需要ServletContext,而Spring容器初始化又需要Web服务器先搭好环境,这种互相依赖的顺序如果不专门管理,很容易乱。
ServletWebServerApplicationContext就是解决这个“鸡生蛋”问题的关键。它在refresh()流程中插入多个钩子方法,让Web服务器的创建和Spring Bean的初始化形成明确、可控的先后顺序。这也解释了为什么Spring Boot要专门写一个上下文类型,而不是沿用传统方案。
1.3 它与GenericApplicationContext/AnnotationConfigApplicationContext的区别
通过类继承关系看更清楚。ServletWebServerApplicationContext继承自GenericApplicationContext,同时实现了ConfigurableWebApplicationContext和WebApplicationContext。这意味着它具备通用上下文的所有能力(Bean定义注册、单例管理、事件发布等),又额外实现了Web上下文必须的getServletContext()和getServletConfig()方法。
从使用角度来说,它比AnnotationConfigApplicationContext多出两件事:一是扫描并注册Web相关组件(比如DispatcherServletRegistrationBean),二是在刷新流程中创建WebServer并启动。如果你的项目里手动创建过AnnotationConfigApplicationContext,你会发现它完全没有启动Tomcat的能力,因为它的onRefresh()是空实现。而ServletWebServerApplicationContext在onRefresh()里直接调用了createWebServer(),这就是本质区别。
2. 启动流程拆解:从run到WebServer创建
2.1 SpringApplication.run之后发生了什么
看SpringApplication.run()方法内部,剔除异常处理逻辑,核心调用链可以简化成:
public ConfigurableApplicationContext run(String... args) { // ... 略 ConfigurableApplicationContext context = createApplicationContext(); // ... refreshContext(context); // ... return context; }createApplicationContext()里有一段关键逻辑:根据WebApplicationType决定创建哪种上下文。
switch (webApplicationType) { case SERVLET: contextClass = ServletWebServerApplicationContext.class; break; case REACTIVE: contextClass = ReactiveWebServerApplicationContext.class; break; default: contextClass = AnnotationConfigApplicationContext.class; }WebApplicationType是怎么判断出来的?核心逻辑是根据classpath中是否存在特定类:
private WebApplicationType deduceFromClasspath() { if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null) && !ClassUtils.isPresent("org.glassfish.jaxb.runtime.marshaller.ObjectFactory", null)) { return REACTIVE; } // ... return SERVLET; }所以当你的工程依赖了spring-boot-starter-web且没有强制走响应式路线时,最终创建的就是ServletWebServerApplicationContext。这一步是内嵌Web容器启动机制的第一个选择点,也是最容易被忽略的地方。
2.2 refresh()里关键钩子的执行顺序
AbstractApplicationContext.refresh()作为Spring容器的“总调度器”,定义了一整套模板方法。ServletWebServerApplicationContext通过覆写其中三个方法加入Web服务器的生命周期管理:
@Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } } @Override protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { // 注册WebApplicationContextUtils相关的内容 beanFactory.addBeanPostProcessor(new WebApplicationContextServletContextAwareProcessor(this)); // 忽略某些依赖接口 beanFactory.ignoreDependencyInterface(ServletContextAware.class); // ... }调用顺序是这样的:postProcessBeanFactory()执行在先,注册ServletContextAwareProcessor,让实现了ServletContextAware的Bean能活着拿到ServletContext注入。紧接着进入onRefresh(),在这里createWebServer()完成了内嵌容器的创建和启动。最后进入finishRefresh(),ServletWebServerApplicationContext又覆写了onClose()或finishRefresh()相关内容,保证容器关闭时WebServer也能一并关闭。
这里的节奏很关键:WebServer不是等所有Bean都初始化完才启动,而是先启动好,再初始化Servlet相关Bean。这样设计的原因是,后续Spring MVC的DispatcherServlet需要注册到已经存在的ServletContext上。如果容器没提前启动,Servlet注册就无从谈起。
2.3 createWebServer()到底做了什么
createWebServer()的源码可以浓缩成几个步骤:
private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); // ... } // 省略日志输出等 }第一步,从BeanFactory里获取ServletWebServerFactory。Spring Boot的自动配置类ServletWebServerFactoryAutoConfiguration会依据classpath中的容器依赖,注册TomcatServletWebServerFactory、JettyServletWebServerFactory或UndertowServletWebServerFactory。
第二步,构造getSelfInitializer()。它实际上是一个ServletContextInitializer,内部调用了selfInitialize()方法。这样做的目的,是为了把Spring Boot的Servlet注册逻辑交给WebServer启动过程中的ServletContextInitializer回调机制去执行。
第三步,调用factory.getWebServer(...)。这一步会真正创建并启动内嵌服务器。以Tomcat为例,getWebServer()内部会创建Tomcat实例、设置端口、配置Connector、把ServletContextInitializer注册进去,最后调用tomcat.start()。
这三步走完,Tomcat就已经在本机端口上监听了。此时Spring容器的Bean还处于预初始化阶段,但Tomcat已经可用。这种“先启动服务器、再注册业务处理器”的顺序,恰恰体现了内嵌容器机制的灵活之处。
2.4 getWebServerFactory()如何找到正确的工厂
getWebServerFactory()实现如下:
protected ServletWebServerFactory getWebServerFactory() { String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class); if (beanNames.length == 0) { throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean."); } // ... return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class); }Spring Boot允许只存在一个ServletWebServerFactoryBean。如果你的项目里同时依赖了spring-boot-starter-tomcat和spring-boot-starter-jetty,会发生什么?很好判断——查找到多个同类型Bean,启动会直接失败,报Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans。所以进行内嵌容器切换时,要记得排除掉默认的Tomcat starter,只保留一个容器实现。
从getBeanNamesForType这个逻辑还能得出另一个结论:如果我们想完全替换默认容器工厂,不需要改框架代码,只需要在自定义配置类里声明一个TomcatServletWebServerFactory(或其它工厂子类)的@Bean,且保证只有一个,Spring Boot就会优先使用它。这个特性在后面讲扩展时会用上。
3. 核心源码细节与参数解析
3.1 WebServerFactoryCustomizer与ServletContextInitializer的作用
ServletWebServerApplicationContext在创建WebServer之前,会先执行所有WebServerFactoryCustomizer回调,对工厂实例进行定制。这个机制说起来简单,但隐藏了一个很常见的“先后顺序”坑。
在createWebServer()里,源码有这样的处理:
// 伪代码,用于说明流程 ServletWebServerFactory factory = getWebServerFactory(); // 让所有WebServerFactoryCustomizer生效 customize(factory); this.webServer = factory.getWebServer(getSelfInitializer());customize(factory)会遍历容器内所有WebServerFactoryCustomizerBean并调用它们的customize(factory)方法。自动配置里的ServerProperties(对应server.*配置项)就是通过这种方式应用到工厂上的。例如server.port=9090,在处理时会执行factory.setPort(9090)。
ServletContextInitializer则是Web容器规范中用于初始化ServletContext的回调接口。Spring Boot在启动时把getSelfInitializer()传进去,后续容器线程在初始化ServletContext时会回调它,从而把Spring的DispatcherServlet、Filter等注册进去。重点理解:ServletContextInitializer的调用时机是在Tomcat自己初始化ServletContext阶段,而不是在Spring容器refresh()阶段。所以如果你试图在某个@Configuration类的构造器里直接访问ServletContext,一定会拿到null。正确做法是把逻辑放在ServletContextInitializer或ServletContextAwareBean里。
3.2 server.port等配置项是如何一步步生效的
以server.port为例追踪一遍,这个流程能串起前面所有概念:
第一步,ServerProperties是一个@ConfigurationProperties(prefix = "server")类,它的port字段对应配置中的server.port。Spring Boot的配置绑定机制会把值填充进这个对象。
第二步,ServletWebServerFactoryAutoConfiguration内部有一个ServletWebServerFactoryCustomizer,类型是WebServerFactoryCustomizer<ServletWebServerFactory>。它会拿ServerProperties里的值设置到工厂上。
第三步,createWebServer()调用customize(factory),触发上述定制过程。Tomcat工厂拿到port=9090后,在getWebServer()里创建Connector时使用这个端口。
配置生效的根本原因,不是“配置文件自动读取”这么玄学,而是ServerPropertiesBean存在 +WebServerFactoryCustomizer回调被触发。如果你自己定义一个WebServerFactoryCustomizer并把端口改成其他值,它和配置文件里的值谁赢?取决于自定义类和ServletWebServerFactoryCustomizer的@Order。默认情况下,后定义的回调执行顺序越靠后,也就是说你自定义的回调会覆盖掉默认值。想控制顺序就标注@Order。
3.3 TomcatServletWebServerFactory初始化过程源码级说明
把目光放到具体工厂实现上,看TomcatServletWebServerFactory.getWebServer()的核心步骤:
public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); // 初始化Connector,设置端口、协议等 Connector connector = new Connector("HTTP/1.1"); connector.setPort(getPort()); tomcat.getService().addConnector(connector); // ... prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); }这段代码里有一个非常容易被忽略的细节:连接器协议默认是HTTP/1.1,但很多性能调优场景需要改成org.apache.coyote.http11.Http11NioProtocol或者升级到Http11AprProtocol。在Spring Boot中想换协议,并不需要直接改这段逻辑,而是通过定制TomcatConnectorCustomizer修改Connector的属性。比如设置maxThreads、acceptCount、connectionTimeout都能在自定义器里做。
prepareContext(tomcat.getHost(), initializers)这一步用来创建TomcatEmbeddedContext,并把这个上下文关联到Tomcat的Host上。Spring Boot的TomcatEmbeddedContext重写了部分逻辑,例如禁用JSP默认配置(如果没引入Jasper)、设置addWebinfClassesResources等。此处还会把ServletContextInitializer包装后添加到TomcatStarter中。
TomcatStarter是实现ServletContainerInitializer的类,Tomcat启动时会回调它的onStartup()方法,进而执行Spring Boot传入的ServletContextInitializer。所以那句“内嵌Web容器启动机制”的核心闭环,就在这里打通了:WebServer的启动 -> Tomcat生命周期 -> ServletContainerInitializer回调 -> Spring Boot的ServletContextInitializer -> 注册DispatcherServlet。
3.4 从onRefresh到finishRefresh的完整时序
整个过程用一个表格呈现时序,可以帮助建立整体记忆:
| 阶段 | 执行内容 | 关键方法 |
|---|---|---|
| run()启动 | 判断Web应用类型 | SpringApplication.createApplicationContext() |
| 创建上下文 | 实例化ServletWebServerApplicationContext | 构造器 |
| prepareContext | 加载主配置类、扫描Bean定义 | load()、scan() |
| refresh()开始 | 准备BeanFactory | prepareBeanFactory() |
| postProcessBeanFactory | 注册ServletContextAwareProcessor | postProcessBeanFactory() |
| onRefresh | 创建并启动WebServer | createWebServer()→factory.getWebServer() |
| finishRefresh | 发布ContextRefreshedEvent、触发WebServer启动完成回调 | onStart() |
| 应用运行 | 外部请求进入Servlet容器 → Spring MVC处理 | DispatcherServlet |
注意onStart()这个方法。ServletWebServerApplicationContext在finishRefresh()中会调用webServer.start()和webServer.await()(通常由WebServerStartStopLifecycle处理)。虽然Tomcat在getWebServer()里已经start()过了,这里还有一次幂等处理,确保上下文完全刷新完后再对外提供服务。这种“先启动后等待”的模式,保证了启动期间不会有请求打到还没准备完毕的Spring容器上。
4. 常见问题与排查技巧实录
4.1 端口被占用或自定义端口不生效
现象:日志显示Port 8080 was already in use,或者明明配置了server.port=9090,启动日志仍是8080。
处理办法:
- 端口占用时先看监听进程:
lsof -i :8080或netstat -ano | grep 8080。在Windows下对应netstat -ano | findstr 8080,然后任务管理器结束进程或换端口。 - 自定义端口不生效,首先检查配置文件的加载位置。如果你同时存在
application.properties和application.yml,Spring Boot的加载顺序有优先级。另外确认是不是写成了server:port: 9090这种错误缩进,YAML里少一个空格就不会正确绑定。 - 更隐蔽的原因是启动时通过命令行传了参数覆盖配置:
java -jar app.jar --server.port=9090,覆盖优先级高。排查时看SpringApplication的启动参数,别只见配置文件。
从实现层理解:端口最终设置到ServletWebServerFactory的port字段。如果没生效,多半是ServletWebServerFactoryCustomizer没有拿到预期的ServerProperties值。你可以在WebServerFactoryCustomizer里临时打印factory.getPort(),看是否有异常赋值。
4.2 内嵌容器没有启动或启动失败
有些项目会把Web依赖误排除掉,启动时日志没有Tomcat、Jetty相关输出,直接显示“Started ... in x seconds”但没有端口监听。这种情况多半是classpath中缺少ServletWebServerFactory实现类。检查依赖是否正确引入spring-boot-starter-web,如果引入了,再看是否有排除操作。
启动失败的另一个典型原因是ServletWebServerFactory定义了多个。日志会报:
Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : [tomcatServletWebServerFactory, jettyServletWebServerFactory]解决方案很直接:只留一个容器依赖。如果你确实需要切换容器,比如换Undertow,请把Tomcat依赖排除干净:
<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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>4.3 ServletContextInitializer不执行怎么办
如果你在调试自定义的ServletContextInitializer,发现onStartup()一直没触发,可以先区分它注册到哪里。Spring Boot通过getSelfInitializer()注册内置的初始化器,但你若通过@Bean ServletContextInitializer注册自定义实现,正常情况也应该执行。不过有一个坑:自定义ServletContextInitializer的@Order如果排序太后,而此时某个Filter注册依赖了初始化顺序,就会出现“看似没执行”的错觉。
排查时在onStartup()里加断点或日志,确认TomcatLifecycleListener是否触发。另外,如果你直接继承了SpringBootServletInitializer并打包成WAR部署到外部Tomcat时,这个类不会走ServletWebServerApplicationContext的创建流程,此时部分自定义ServletContextInitializer的触发方式会不同。别再拿内嵌模式的经验去套外部部署场景,这是很多人踩过的坑。
4.4 如何扩展内嵌容器:实现TomcatConnectorCustomizer
实际开发中,想修改Tomcat的连接参数,最佳途径就是自定义TomcatConnectorCustomizer。
比如调整最大线程数:
@Bean public TomcatConnectorCustomizer tomcatConnectorCustomizer() { return connector -> { if (connector.getProtocolHandler() instanceof AbstractProtocol<?> protocol) { protocol.setMaxThreads(500); protocol.setAcceptCount(200); protocol.setConnectionTimeout(30000); } }; }再比如设置访问日志。虽然server.tomcat.accesslog.enabled=true可以开启,但想指定日志目录、格式,可以这样:
@Bean public TomcatConnectorCustomizer accessLogCustomizer() { return connector -> connector.setProperty("accessLog", "true"); }实际上更推荐用TomcatServletWebServerFactory的addContextCustomizers、addConnectorCustomizers方法做定制。知道这个方法,胜过乱改源码。
4.5 三种内嵌容器的对比选择
既然ServletWebServerApplicationContext不关心底层具体是哪一种WebServer,全靠ServletWebServerFactory抽象,那么自定义容器实现也完全可行。主流三种对比如下:
| 特性 | Tomcat | Jetty | Undertow |
|---|---|---|---|
| 默认依赖 | spring-boot-starter-tomcat | spring-boot-starter-jetty | spring-boot-starter-undertow |
| 内存占用 | 中等 | 较低 | 较低 |
| 启动速度 | 一般 | 较快 | 快 |
| JSP支持 | 内置较好 | 需配置 | 支持有限 |
| 线程池模型 | NIO + 线程池 | NIO | NIO / AJP |
从ServletWebServerApplicationContext的角度来看,切换容器只需要替换classpath依赖,不需要改任何启动流程逻辑。因为getWebServerFactory()按类型查找,而工厂Bean由自动配置类注册。这个设计对开发者非常友好,但也是“隐藏依赖”问题的来源——你以为是Tomcat在跑,实际依赖里可能是Undertow,排查问题会跑偏。
5. 实录:一次手动创建WebServer的探索
为了加深对启动机制的理解,我建议你亲手写一段代码,绕开Spring Boot自动配置,自己创建内嵌Tomcat并运行。这能帮你彻底理解ServletWebServerApplicationContext到底替你做了什么。
5.1 最小示例手动启动Tomcat
public class ManualTomcatDemo { public static void main(String[] args) throws Exception { Tomcat tomcat = new Tomcat(); tomcat.setPort(9999); tomcat.getHost().setAppBase("."); tomcat.addWebapp("", System.getProperty("java.io.tmpdir")); tomcat.start(); tomcat.getServer().await(); } }这个示例引用的就是org.apache.tomcat.embed:tomcat-embed-core。执行后,一个最基础的Tomcat就起来了,虽然没有任何Servlet处理请求。
然后对比Spring Boot的TomcatServletWebServerFactory.getWebServer(),你立刻会明白多出来的部分是什么:prepareContext()创建Spring Boot专用上下文,TomcatStarter布置Spring的初始化逻辑,TomcatWebServer封装了Tomcat生命周期。这些封装,正是ServletWebServerApplicationContext协调管理WebServer和Spring容器的基础。
5.2 用工厂类配合ServletContextInitializer
再进一步,你可以用工厂类启动:
ServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.setPort(9090); WebServer webServer = factory.getWebServer(servletContext -> { servletContext.addServlet("manualServlet", new HttpServlet() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.getWriter().write("hello from manual servlet"); } }).addMapping("/hello"); }); webServer.start();这段代码复现了Spring BootcreateWebServer()的核心逻辑。手动传入ServletContextInitializer,然后在回调里注册Servlet,WebServer启动后访问/hello即可看到响应。做一次这样的探索,你对源码就不再是“看过”,而是“真懂了”。
6. 个人总结与扩展建议
从ServletWebServerApplicationContext的源码里我最大的收获是:Spring Boot的自动配置并不是魔法,而是基于Spring自身扩展点做了精妙编排。onRefresh()、postProcessBeanFactory()、finishRefresh()这些模板方法早在Spring Framewok时代就存在了,Spring Boot只是在上层“填空”。
如果后续想做更深层的定制,我建议你从这几个方向入手:一是实现自己的ServletWebServerFactory,支持非标准Servlet容器或内嵌Netty的Servlet模拟层;二是扩展WebServerFactoryCustomizer做容器层面的监控埋点,比如记录启动耗时、连接数;三是研究WebServer接口和WebServerStartStopLifecycle,理解应用优雅停机时webServer.stop()的调用链。
最后再分享一个排查技巧:遇到Web容器相关问题时,先打开--debug启动,看自动配置报告里的Positive matches和Negative matches,确认ServletWebServerFactoryAutoConfiguration是否生效。这个动作能让你少走一半弯路。毕竟了解了底层机制,排查问题就不是猜谜,而是沿着调用链逐级核对,最终总能找到交点。