资讯动态

Spring Boot内嵌Web容器启动机制源码解析

发布时间:2026/10/22 6:03:12 来源:尧图企业网站定制
拿一次真实的故障来说线上环境某服务明明表单提示已启动日志也打了“Started Application in 8.2 seconds”但测试同事拿请求一压全部超时。查了下端口才发现Tomcat 根本没有监听在预期的 8080真正在跑的是一个随机端口。编码接口倒是返回正常路由也注册了但容器没跟上。那次我翻源码翻了两个小时最终定位到问题出在ServletWebServerApplicationContext刷新阶段对 WebServer 的创建和启动时机和我自定义的一个ServletContextInitializer冲突了。自那之后我把 Spring Boot 内嵌容器从上下文创建、BeanFactory 刷新到ServletContext真正可用这条链路彻底读了一遍也算把这块“黑盒”给补上了。这篇文章就专门拆这块围绕ServletWebServerApplicationContext深入解析 Spring Boot 内嵌 Web 容器Tomcat / Jetty / Undertow的启动机制。我不会从“怎么用 Spring Boot”这种基础开讲而是直接从源码层告诉你容器是什么时候被创建的、refresh 哪些钩子参与了启动决策、DispatcherServlet是怎么被注册进容器的、以及为什么你的配置在某些场景下会“像是失效了”。如果你准备深入理解 Spring Boot 的自动配置与生命周期或者正在排查启动异常、端口不监听、Servlet 未注册这类疑难杂症这篇文章可以直接当作源码导读来用。1. 先看清链路全貌SpringApplication 是如何选定并创建容器的1.1 WebApplicationType 推断三种类型的区分逻辑Spring Boot 启动的第一件大事并不是急着创建 Spring 容器而是先判断“我到底该往哪个方向走”。这个判断入口在SpringApplication的构造阶段就出现了通过WebApplicationType.deduceFromClasspath()完成。static WebApplicationType deduceFromClasspath() { if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; } } return WebApplicationType.SERVLET; }这段逻辑的意思很直白类路径里有org.springframework.web.reactive.DispatcherHandler同时没有DispatcherServlet和 Jersey判定为REACTIVE后续创建ReactiveWebServerApplicationContext。类路径里包含javax.servlet.Servlet、org.springframework.web.context.ConfigurableWebApplicationContext这些判定为SERVLET走ServletWebServerApplicationContext。两者都不存在比如纯 Job 项目或分离出来的 service 模块判定为NONE创建普通的AnnotationConfigApplicationContext。这段代码看起来简单但有一个很值得注意的坑如果项目同时引入了 WebFlux 和 WebMVC 依赖但缺少DispatcherServlet类型推断也可能跑偏。所以尽量让 starter 干净不要混装spring-boot-starter-web和spring-boot-starter-webflux除非你真知道自己在干什么。1.2 上下文类型的选择与 ServletWebServerApplicationContext 的继承链当SpringApplication最终确认要创建哪种类型后它调用createApplicationContext()protected ConfigurableApplicationContext createApplicationContext() { return this.applicationContextFactory.create(this.webApplicationType); }对于 SERVLET 类型默认实例化的是AnnotationConfigServletWebServerApplicationContext。注意这个类跟ServletWebServerApplicationContext的关系是这样的AbstractApplicationContext - GenericApplicationContext - GenericWebApplicationContext - ServletWebServerApplicationContext - AnnotationConfigServletWebServerApplicationContext整套继承设计最大的意义在于Servlet 相关的 web 能力并不写在自己的具体子类里而是全部沉淀在ServletWebServerApplicationContext这一层。也就是说无论你用的是注解版上下文还是 Xml 版WebServer 的创建、启动、关闭逻辑都是复用同一套代码。ServletWebServerApplicationContext的核心职责就一句话在标准 Spring 上下文刷新流程基础上额外接住“创建内嵌 WebServer”这个动作。它通过重写几个关键的钩子方法将一个普通的 ApplicationContext 升级成具备内嵌容器的 Web 上下文。2. 自动装配的秘密ServletWebServerFactory 是怎么被选出来的2.1 ServletWebServerFactoryAutoConfiguration 的装配逻辑ServletWebServerApplicationContext在创建 WebServer 时并不是自己去 new 一个 Tomcat而是从BeanFactory中获取一个ServletWebServerFactory类型的 Bean再通过这个工厂来创建真正的 WebServer。那这个工厂从哪来答案是自动配置。在spring-boot-autoconfigure里ServletWebServerFactoryAutoConfiguration负责兜底。它内部效果类似这样AutoConfiguration ConditionalOnClass(ServletRequest.class) EnableConfigurationProperties(ServerProperties.class) public class ServletWebServerFactoryAutoConfiguration { Bean ConditionalOnMissingBean public ServletWebServerFactoryServletContextInitializer servletContextInitializer() { ... } Bean ConditionalOnMissingBean public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer() { ... } }真正决定“用哪个具体容器”的是它的内部静态类Configuration(proxyBeanMethods false) ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) ConditionalOnMissingBean(value ServletWebServerFactory.class, search SearchStrategy.CURRENT) static class EmbeddedTomcat { Bean TomcatServletWebServerFactory tomcatServletWebServerFactory() { ... } }类似的还有EmbeddedJetty、EmbeddedUndertow。核心匹配策略是ConditionalOnMissingBean意味着只要你曾经自己声明过任意一个ServletWebServerFactory类型的 Bean整个自动配置的默认工厂就不再生效。这就是为什么自定义容器配置最优雅的方式其实是定义自己的ServletWebServerFactoryBean。2.2 自定义容器配置的两个实用方向这里讲两种常见的自定义做法也是面试高频考点。第一种是在 Configuration 类里手动声明ServletWebServerFactory第二种是保留自动配置只通过WebServerFactoryCustomizer修改默认工厂。Bean public WebServerFactoryCustomizerTomcatServletWebServerFactory portCustomizer() { return factory - { factory.setPort(9090); factory.getTomcatConnectorCustomizers() .add(connector - connector.setAttribute(maxConnections, 20000)); }; }WebServerFactoryCustomizer会被ServletWebServerFactoryAutoConfiguration里定义的ServletWebServerFactoryCustomizer统一调用传入时机是在ServletWebServerFactoryBean 创建完成后、getWebServer()方法执行前。这个扩展点非常安全因为你没有动工厂本身的注册逻辑只是在标准流程里往工厂里塞参数。而如果你选择第一种方式直接声明TomcatServletWebServerFactory就得自己保证所有属性设置正确并且一旦声明自动配置里的条件判断全部失效很多默认行为可能被无意间覆盖。我会建议优先用WebServerFactoryCustomizer只有在需要完全替换容器实现比如换成 Undertow时才考虑直接声明工厂。2.3 工厂的延迟定位为什么 WebServer 创建不在构造阶段这里有个你可能没注意的时序ServletWebServerFactory本身作为一个 Bean是在refresh()过程中的invokeBeanFactoryPostProcessors()或finishBeanFactoryInitialization()阶段才被完整创建。而创建 WebServer 是在onRefresh()钩子里。这就意味着如果在invokeBeanFactoryPostProcessors()阶段回调去提前访问ServletWebServerFactoryBean很可能触发 Bean 的提前初始化导致一批 Bean 在完整生命周期处理前被“抢跑”进而引发循环依赖或初始化顺序问题。我见过有人用BeanFactoryPostProcessor的代码读取TomcatServletWebServerFactory配置结果把自己坑进去报错一直在提示BeanCurrentlyInCreationException。记住一条经验不要在任何BeanFactoryPostProcessor里主动 getBean 一个ServletWebServerFactory。3. 进入 refreshServletWebServerApplicationContext 在哪些钩子里动了手脚3.1 postProcessBeanFactoryServletContextInitializer 的单例化包装如果你在 Spring Boot 项目里写过自定义的ServletContextInitializer有没有想过它注册时机是怎么来的它其实是在AbstractApplicationContext的刷新早期被特殊处理掉的。ServletWebServerApplicationContext重写了postProcessBeanFactoryOverride protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { beanFactory.addBeanPostProcessor(new WebApplicationContextServletContextAwareProcessor(this)); beanFactory.ignoreDependencyInterface(ServletContextAware.class); registerWebApplicationScopes(); ... }接着在onRefresh里调用的createWebServer()之前容器已经通过ServletContextInitializerBeans收集器把所有实现ServletContextInitializer接口的 Bean 找出来并且做了排序和去重。这里有一个相当隐蔽的细节ServletContextInitializer并不是在onRefresh阶段才扫描注册而是在postProcessBeanFactory阶段就把它们标记为单例并注册到了BeanFactory中。这样做的原因是Spring 容器在finishBeanFactoryInitialization()阶段创建单例 Bean 时ServletContextInitializer类型的 Bean 也会被一并创建之后才能安全地在 WebServer 启动时调用它们的onStartup()方法。3.2 onRefresh 钩子WebServer 的创建时机refresh()流程走到onRefresh()时Spring 提供了一个模板方法供子类实现扩展逻辑。普通容器这里多半是空的但对ServletWebServerApplicationContext来说这正是创建内嵌容器的关键时刻。Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException(Unable to start web server, ex); } }createWebServer()的整体逻辑分三块如果当前没有WebServer实例且也没有ServletContext说明不是外部容器部署场景则从 BeanFactory 中解析ServletWebServerFactory并用getWebServer(getSelfInitializer())创建 WebServer。如果已经存在外部ServletContext比如部署到外部 Tomcat则跳过创建逻辑直接进入初始化。创建完成后把当前上下文关联到WebServerInitializedEvent的发布路径上同时将ServletContext注入到 Spring 的ApplicationContext属性中。注意这块的时序定位onRefresh()是在所有 BeanFactoryPostProcessor 执行完毕、但绝大多数普通单例 Bean 尚未实例化之前运行的。这就解释了为什么 WebServer 对象虽然已经创建出来端口却还没开始监听。因为真正的WebServer.start()被推迟到了刷新流程的更后面。3.3 finishRefresh在事件发布之后才真正启动端口标准的AbstractApplicationContext.refresh()最后几步是这样onRefresh(); registerListeners(); finishBeanFactoryInitialization(); finishRefresh();ServletWebServerApplicationContext重写finishRefresh()做的事情是Override protected void finishRefresh() { super.finishRefresh(); WebServer webServer startWebServer(); if (webServer ! null) { webServer.start(); } }这里有一个反直觉的点super.finishRefresh()会先发布ContextRefreshedEvent然后才启动 WebServer。也就是说如果你的某个监听器监听了ContextRefreshedEvent在这个监听器执行时Tomcat 实际上还没开始监听端口。如果你写了一些自以为“容器已经就绪”的检查逻辑比如在这个事件里去请求本机端口就会失败。我当年排查线上问题时就撞上过这个细节。那次的核心原因是一个自定义的ApplicationListenerContextRefreshedEvent里提前发起了 HTTP 健康检查请求而这个请求恰好打到自己的端口结果端口尚未打开逻辑直接走入了异常分支。从日志表面看启动失败原因是“连接被拒绝”但根本原因其实是时序判断错误。在 Spring Boot 3.2 之后的版本引入了WebServerStartStopLifecycle类通过SmartLifecycle管理 WebServer 的启动。默认情况下它仍然是注册到LifecycleProcessor中执行顺序在finishRefresh()内部由生命周期的start()触发。注意这个类的作用是当上下文配置为lazy-init时WebServer 不会立即启动而是等第一次请求或显式触发时才初始化。如果你在生产环境开了spring.main.lazy-initializationtrue一定要知道 WebServer 的行为也会跟着变后面我会在问题排查章节再展开。4. Tomcat 实例化全流程从 ServletWebServerFactory 到 ServletContext4.1 TomcatServletWebServerFactory.getWebServer 内部过程以最常见的 Tomcat 为例TomcatServletWebServerFactory.getWebServer(initializer)在内部做了这几件事Override public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat new Tomcat(); // 1. 基本目录与连接器设置 tomcat.setBaseDir(createTempDir(tomcat)); // 2. 配置 Connector configureConnector(tomcat); // 3. 准备 TomcatEmbeddedContext TomcatEmbeddedContext context new TomcatEmbeddedContext(); context.setName(); context.setDisplayName(server); // 4. 配置 Host、Port、Session 等 ... // 5. 把所有 ServletContextInitializer 封装并放入 context TomcatStarter starter new TomcatStarter(initializers); context.addServletContainerInitializer(starter, NO_CLASSES); // 6. 返回 Tomcat WebServer return getTomcatWebServer(tomcat); }这一步的重点在于getWebServer返回的WebServer对象虽然已经持有完整的 Tomcat 生命周期状态但 Tomcat 此时并没有start()。端口仍然未被监听Connector也只是待命状态。这属于 Spring Boot 有意为之因为 WebServer 的启动必须放在绝对安全的时机确保所有 Spring Bean 都创建完成、ApplicationContext已发布基础事件。4.2 TomcatStarter程序化注册 Servlet 的桥梁ServletContextInitializer是 Spring Boot 抽象出来的一套标准化接口作用是在 Servlet 容器启动时回调注册 Servlet、Filter、Listener。但 Tomcat 原生并不认识 Spring Boot 的这套接口它只认识 JSR 规范的ServletContainerInitializer。于是就有了桥接类TomcatStarterclass TomcatStarter extends ServletContainerInitializer { private final ServletContextInitializer[] initializers; Override public void onStartup(SetClass? classes, ServletContext servletContext) { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } }这里需要特别说明一点getSelfInitializer()返回的并不是某个具体的 spring 内部类而是当前ServletWebServerApplicationContext自身的方法引用。它会调用selfInitialize(ServletContext)逻辑大体是private void selfInitialize(ServletContext servletContext) { prepareWebApplicationContext(servletContext); registerApplicationScope(servletContext); ... WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext); for (ServletContextInitializer bean : getServletContextInitializerBeans()) { bean.onStartup(servletContext); } }这段代码保证了DispatcherServlet、CharacterEncodingFilter、隐藏的ServletContext工具 Bean 都能拿到真实的ServletContext并完成注册。顺序也是严格有序的先准备上下文环境再注册作用域最后逐个执行各个ServletContextInitializer。4.3 DispatcherServlet 是如何进入容器的DispatcherServletAutoConfiguration在自动配置里有两个核心 BeanBean(name DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet(...) { return new DispatcherServlet(...); } Bean(name DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) public ServletRegistrationBeanDispatcherServlet dispatcherServletRegistration( DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties) { ServletRegistrationBeanDispatcherServlet registration new ServletRegistrationBean(dispatcherServlet); registration.setUrlMappings(webMvcProperties.getServlet().getPath()); return registration; }ServletRegistrationBean继承了RegistrationBeanRegistrationBean实现了ServletContextInitializer。于是当selfInitialize()中循环执行ServletContextInitializerBean 时会自动调用registration.onStartup(servletContext)这会把DispatcherServlet实例通过Dynamic注册方式加入容器并绑定上/路径映射。这就引出另一个坑如果你自己新建了一个ServletRegistrationBeanDispatcherServlet又把它注册为 Bean那么与自动配置里的 registration Bean 会冲突导致DispatcherServlet被注册两次出现Servlet 名称冲突或请求分发混乱。正确的做法应该是直接修改 WebMvcProperties 配置或者彻底关闭自动配置再手动注册。5. 生命周期辅助机制启动失败分析器与 WebServer 关闭流程5.1 FailureAnalyzer 如何帮我们在启动时报出人话Spring Boot 的启动失败提示之所以那么友好很大程度依赖FailureAnalyzer。比如端口占用时你会在控制台看到类似*************************** APPLICATION FAILED TO START *************************** Description: Web server failed to start. Port 8080 was already in use.这个提示来自PortInUseFailureAnalyzer。它的触发时机在SpringApplication.run()的 catch 逻辑中Spring 会分析抛出的PortInUseException并展开成因链。这对定位问题很有用但也比较容易忽略一个场景某些情况下端口被占用并不会直接抛这个异常而是表现为启动时间异常拉长之后才报错比如防火墙环境下的连接超时。此时要结合SocketException的 message 和连接地址一起分析。5.2 WebServerGracefulShutdown 与停机处理标准的关闭路径会经由SpringApplication.shutdown()或 JVM 的关闭钩子。ServletWebServerApplicationContext如果持有未启动的WebServer在销毁阶段会调用webServer.stop()并释放端口。Spring Boot 2.3 后支持的优雅停机是另一条路。配置server.shutdowngraceful后内嵌容器会进入“停止接受新请求处理完在途请求后退出”的状态。实现的原理是给 Tomcat 注册了一个WebServerGracefulShutdownLifecycle在LifecycleProcessor回调 stop 阶段先暂停Connector并设置tomcat.getService().getConnectors()[?].pause()再等待活跃请求计数回归零。这段逻辑如果在高并发场景下配置不合理很容易发生“等了很久还在停”的现象。你需要保证server.lifecycle-timeout合理并且理解这个超时只是信号层的最大等待时间如果超时触发后仍有请求在跑容器也会继续等待 Tomcat 返回只是日志上会出现超时提示。真正常见的做法还是配合负载均衡器摘流再加一段暂停时间。5.3 WebServerInitializedEvent什么时候可以开始对外服务ServletWebServerApplicationContext在createWebServer阶段会注册一个WebServerInitializedEvent发布动作。这个事件的具体类是ServletWebServerInitializedEvent。它的发布时机是 WebServer 成功start()之后也就是说你可以在该事件监听器里安全地拿走端口和ServletContext。Component public class WebServerReadyListener implements ApplicationListenerServletWebServerInitializedEvent { Override public void onApplicationEvent(ServletWebServerInitializedEvent event) { int port event.getWebServer().getPort(); // 此时可以请求自身端点 } }这也是我目前比较推荐的“就绪判断”方式比在ContextRefreshedEvent里轮询端口要可靠得多。如果你在写接口自动化测试、冒烟脚本、健康检查逻辑优先利用这个事件而不是依赖 Spring 上下文刷新完成的通用事件。6. 实战排查记录内嵌容器启动中的高频问题6.1 端口冲突与随机端口策略端口冲突最直接的解法是 更换端口或者让框架自动选择可用端口。后者有一个标准写法就是把端口显式设置为 0server.port0此时ServletWebServerFactory会为 Connector 设置端口 0由系统分配一个随机可用端口。启动日志里会打印实际端口ServletWebServerInitializedEvent里也能拿到。这种方案适合多实例本地联调或自动化测试但不适合生产环境。生产环境使用固定端口的前提是你的端口资源规划合理并且系统层面启用了端口复用等机制。排查端口冲突时不要只查netstat -ano | grep 8080内嵌容器有些情况下占用端口的是 IPv6 地址用netstat -tulnp配合lsof -i往往才能看到全部情况。如果启动日志提示 Failed to start Connector检查bind()被拒绝时的具体IOException类型才能确定是同机端口占用还是容器无权限绑定低端口。6.2 端口没监听但启动成功的“伪启动”日志显示启动成功但端口无法访问这种问题最容易甩锅给网络层但实际原因往往出在容器没有真正启动。常见原因有自定义了ServletWebServerFactoryBean但误把 Tomcat 实例的Connector设置成port-1。某个SmartLifecycle的phase值太低在LifecycleProcessor启动时 WebServer 的start()被延后或短路。开启了spring.main.lazy-initializationtrueWebServer 的启动被推迟到首次访问才触发。最简单直接的定位手段就是抓启动日志中Tomcat started on port(s)这一行。如果这行日志缺失基本可以断定webServer.start()没有走到正常路径或根本没执行。# 通过启动时打印的所有监听端口判断 ss -tlnp | grep java6.3 ServletContextInitializer 排序导致的 Filter 顺序异常ServletContextInitializerBeans在收集阶段会统一处理排序排序核心是Ordered接口和Order注解。如果你定义了一个 Filter通过FilterRegistrationBean注册同时又用Component方式声明了一个 Filter 类那么这两个 Filter 的先后顺序会遵循 registration Bean 的order属性而不是按照类加载顺序。我曾经遇到一个诡异现象自定义 Filter 在请求里加了一个 traceId但一部分请求的 traceId 始终为空。排查后发现是项目里有两个不同 jar 包都声明了 Filter一个通过 SPI 自动装配一个通过显式 Bean 声明自动装配的 Filter 优先级更低在所有显式 Bean 之后执行而 traceId 的写入逻辑恰好排在第二个 Filter 的处理链路上。这个问题的根治方法就是统一 Filter 的声明方式并检查ServletContextInitializer的排序规则。6.4 Java 17 / Spring Boot 3.x 下的类加载差异Spring Boot 3.x 要求基础包切换为jakarta.*javax.servlet.Servlet变成了jakarta.servlet.Servlet。这导致如果你在代码里混用了旧版本 Tomcat 或旧 servlet API容器启动时会有各种找不到类的诡异异常。另一个比较隐晦的点是Spring Boot 3.x 对反射的访问限制更严格。Tomcat 内部为了设置库目录或反射调用某些 setter需要--add-opens参数。如果你在 JDK 17 下遇到InaccessibleObjectException一般不是 Spring Boot 的问题是 JDK 强封装导致的。官方文档建议加 JVM 参数但我个人更建议优先升级依赖版本因为高版本的 Tomcat 已适配 JDK 17 的内省逻辑很多场景其实不需要额外加参数。7. 源码阅读与断点调试建议7.1 推荐断点位置如果你想自己动手走一遍完整的启动链路以下断点是我实测非常高效的几个位置ServletWebServerApplicationContext.onRefresh - 容器创建入口 ServletWebServerApplicationContext.createWebServer - 核心判断与工厂调用 TomcatServletWebServerFactory.getWebServer - 工厂内部流程 TomcatStarter.onStartup - 从容器原生回调进入 Spring Boot ServletRegistrationBean.onStartup - DispatcherServlet 注册 ServletWebServerApplicationContext.finishRefresh - 启动 WebServer TomcatWebServer.start - Tomcat 实际启动打断点时要注意不要全量同时启用很多调用在运行时是循环或者多线程回调的断点太多容易让人陷进去。建议先断createWebServer和finishRefresh把整体框架摸熟再往里看细节。7.2 用日志辅助验证时序如果你不想用 IDE 调试也可以靠日志来验证时序。在application.yml里开启动态日志后关注这几个关键输出点logging.level.org.springframework.boot.web.embedded.tomcatTRACE logging.level.org.springframework.boot.web.servlet.contextDEBUG正常情况下你会看到createWebServer相关日志先出现之后才是各种 Bean 创建日志最后才是Tomcat started on port。中间如果出现顺序颠乱往往意味着有 Bean 在onRefresh之前被提前初始化了。7.3 一个小型复现实验自定义 ServletContextInitializer为了更直观地验证ServletContextInitializer的执行顺序可以在启动类里加上如下代码Bean public ServletContextInitializer myInitializer() { return servletContext - { System.out.println(myInitializer start: servletContext.getServerInfo()); servletContext.setAttribute(customKey, hello); }; } Bean public ServletContextInitializer secondInitializer() { return servletContext - { Object value servletContext.getAttribute(customKey); System.out.println(secondInitializer get customKey: value); }; }如果你的 two initializers 没有指定Order第一次执行时会发现顺序可能是随机的但再次重启后表现稳定。这说明排序稳定并不等于显式可控真正需要控制顺序时必须为 initializer 实现Ordered或加Order注解。这个小小实验花不了五分钟但对理解整个注册链路的帮助相当大。另外一个小实验是在ContextRefreshedEvent监听器里尝试访问WebServer的端口。EventListener public void onRefresh(ContextRefreshedEvent event) { ServletWebServerApplicationContext ctx (ServletWebServerApplicationContext) event.getApplicationContext(); System.out.println(port in ContextRefreshedEvent: ctx.getWebServer().getPort()); }此时你很可能拿到的端口号是真实配置的端口但连接该端口会失败因为webServer.start()尚未执行。这个实验可以完美解释我之前说的时序问题也适合拿来做团队内部培训演示。8. 我的实操体会与几个防坑建议把这条链路读透之后我最直观的感受是Spring Boot 的内嵌容器启动机制本质上就是“在一个标准的 Spring 上下文刷新流程上精准地插入 WebServer 的创建和启动两个动作”。所以理解它的路径越清晰遇到启动类问题就越不慌。如果你接下来要深入 Spring Boot 的自动配置、生命周期或者做 debug 级别的问题排查建议按照“上下文创建 - BeanFactory 准备 - 工厂选择 - WebServer 创建 - WebServer 启动”这条主线去阅读源码。不要一开始就扎进 Tomcat 内部那样容易只见树木不见森林。最后分享一个每次排查询问前必做的小动作把启动过程完整日志保留下来尤其是Tomcat started on port(s)和Root WebApplicationContext相关的行。很多问题的根源其实在日志里已经写得很直白只是被淹没在大量 INFO 中而已。学会先通读这段日志再动手改代码会少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑