Spring Boot嵌入式Tomcat源码解析:自动装配与启动流程揭秘
很多人在用 Spring Boot 时都会有一个特别直观的感受项目打成 jar 包扔到服务器上一句java -jar就能跑起来既不需要提前装 Tomcat也不用去配置什么 server.xml。这个体验背后最大的功臣就是 Spring Boot 的嵌入式 Servlet 容器机制。只要你用的是 Web 项目Spring Boot 默认就会把一个 Tomcat 塞进你的应用进程里跟你一起启动、一起停止。这篇是“Spring Boot 源码分析”系列第七篇我打算专门把嵌入式 Tomcat 这条主链路彻底拆一遍重点回答三个问题Spring Boot 到底是怎么在代码层面“嵌入”Tomcat 的Tomcat 对象是什么时候被创建、启动的我们平时写的 Controller、Servlet、Filter 又是怎么挂到 Tomcat 上被请求命中的这篇文章适合已经会基本使用 Spring Boot、但对源码内部还有些好奇心的同学读完你应该能从“会用”提升到“大概知道内部在干什么”的层面。1. 嵌入式容器这套设计解决了什么核心问题1.1 跟外置 Tomcat 部署方式相比差别在哪先回忆一下没有 Spring Boot 的时代。传统 SSM 或 Servlet 项目的标准部署流程是把项目打成 war 包然后丢到某个应用服务器比如 Tomcat的 webapps 目录下启动 Tomcat由它来解析 war 包、创建 Context、加载 Servlet。也就是说Tomcat 是被独立安装和管理的你的应用只是它下面众多 Context 当中的一个。这种模式下开发环境、测试环境、生产环境的 Tomcat 版本很容易不一致运维需要关心 JVM 参数、线程池配置、连接池大小而且每次发版都要确保目标机器环境跟预期一致。相信维护过传统项目的朋友都体会过“本地好的测试就不行生产又挂了”的滋味。Spring Boot 把 Tomcat 内嵌进应用后Tomcat 的版本、配置、启动参数全部跟着 jar 包走环境差异被压到最低。你的应用自带“服务器”这其实是把一部分运维复杂度收拢到了应用内部。1.2 “内嵌”精选方案背后的取舍逻辑内嵌方案并不是 Spring Boot 的发明但 Spring Boot 把它做成了默认行为不是没有原因的。第一个原因是自包含部署jar 里既有业务代码也有 Web 服务器产物可以被当一个普通 Java 进程来管理进程退出容器也就没了没有独立 Tomcat 残留进程需要额外清理。第二个原因是版本可控你引入的 starter 版本里锁定了一组经过 Spring Boot 适配过的 Tomcat 版本开发、测试、生产用的是同一套代码和同一套容器实现规避了外置容器版本碎片化的问题。第三个原因是进程内容器天生启动快省掉了很多跨进程通信和生命周期同步的环节。当然内嵌方案也不是没有代价。最明显的就是Tomcat 无法像独立安装的那样被单独重启或单独替换版本而且如果想要做多应用共用一个大 Tomcat 的场景内嵌就不合适了。所以在架构选型时内嵌容器更适合微服务这种“一进程一应用”的形态而传统单体大应用可能更适合外置容器。2. Tomcat 的自动装配到底是怎么发生的2.1 定位自动配置类ServletWebServerFactoryAutoConfigurationSpring Boot 的大多数能力都来自自动配置嵌入式 Tomcat 也不例外。我们需要找到负责创建 Servlet Web 服务器的那个自动配置类。在 Spring Boot 2.x 中它是org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration注册信息在spring-boot-autoconfigure的spring.factories里在 Spring Boot 3.x 中则通过AutoConfiguration.imports文件注册。这个自动配置类的声明很有意思我摘一下核心注解AutoConfiguration ConditionalOnClass(ServletRequest.class) ConditionalOnWebApplication(type Type.SERVLET) EnableConfigurationProperties(ServerProperties.class) public class ServletWebServerFactoryAutoConfiguration { ... }这意味着它只在 classpath 里有 Servlet API、且当前确实是一个 Servlet Web 应用时生效。如果你只是写一个普通的后台任务项目没有 web 依赖这个配置就不会参与进来。另外它还负责把ServerProperties表驱动起来server.port、server.tomcat.threads.max这些配置项的绑定都从这里开始。2.2 条件判断为什么最终选中的是 TomcatServletWebServerFactoryAutoConfiguration本身不会直接 new 一个 Tomcat它通过内部的Import引入了ServletWebServerFactoryConfiguration。这个配置类里同时定义了三种工厂 bean分别对应 Tomcat、Jetty、Undertow但三个 bean 都加了条件注解最后只会命中一个。我以 Spring Boot 2.x 为例这个配置类的代码大致是Configuration(proxyBeanMethods false) public class ServletWebServerFactoryConfiguration { Configuration(proxyBeanMethods false) ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) ConditionalOnMissingBean(value ServletWebServerFactory.class, search SearchStrategy.CURRENT) public static class EmbeddedTomcat { Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory( ObjectProviderTomcatConnectorCustomizer connectorCustomizers, ObjectProviderTomcatContextCustomizer contextCustomizers, ObjectProviderTomcatProtocolHandlerCustomizer? protocolHandlerCustomizers) { TomcatServletWebServerFactory factory new TomcatServletWebServerFactory(); factory.getTomcatConnectorCustomizers() .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatContextCustomizers() .addAll(contextCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatProtocolHandlerCustomizers() .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList())); return factory; } } ... }这个代码里最关键的是三个条件注解配合ConditionalOnClass要求 classpath 里存在Tomcat和UpgradeProtocol类由于默认的spring-boot-starter-web传递依赖了spring-boot-starter-tomcat所以这三个类一定存在于是 Tomcat 分支就会命中。而如果一个团队想用 Undertow只要把 Tomcat starter 排除掉、换成spring-boot-starter-undertow那Tomcat类就没了Tomcat 分支失效Undertow 分支自动接管。这里要注意一个容易踩坑的点ConditionalOnMissingBean(value ServletWebServerFactory.class, search SearchStrategy.CURRENT)的意思是如果你自己已经定义了一个ServletWebServerFactory的 Bean那自动配置里的这个工厂就不会重复注册。Spring Boot 官方推荐用WebServerFactoryCustomizer来微调容器而不是直接替换整个工厂原因就是避免破坏默认的装配链路。2.3 怎么确认当前到底用的是哪个容器很多项目跑着跑着才发现自己用的是 Tomcat其实验证起来很简单。一个方式是打开 spring-boot-autoconfigure 的AutoConfiguration.imports或spring.factories看引入的 starter 依赖另一个方式是在启动日志里看Tomcat started on port(s): 8080 (http)这样的字样。如果看到的是Undertow started on port(s)或者Jetty started那说明项目已经把默认容器换掉了。如果你想在代码层面确认也可以注入一个ServletWebServerFactory然后打印它的实际类型。我曾在排查一个诡异超时问题时用这一招确认过某项目明明引入了 Tomcat 依赖但由于私下在配置类里手工 new 了一个UndertowServletWebServerFactory导致实际跑的是 Undertow问题表现差异很大。查了半天才定位到教训就是任何时候都要先确认“实际生效”的组件不能想当然认为“引入了依赖就是它”。3. Tomcat 实例创建与启动的源码路径3.1 起点容器刷新时的 onRefresh 钩子Spring Boot 创建 Web 服务器不是在 SpringApplication.run 的最后而是在整个ConfigurableApplicationContext.refresh()流程的中间偏早的位置。你要记住一个类ServletWebServerApplicationContext它继承自GenericWebApplicationContextSpring Boot 的 Web 应用上下文基本都是它。Spring 容器刷新的核心方法AbstractApplicationContext.refresh()里有很多步骤其中一步是onRefresh()它在 Spring MVC 等子类里一般被用来做特定初始化。ServletWebServerApplicationContext重写了这个方法Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException(Unable to start web server, ex); } }createWebServer()里的任务就是拿到ServletWebServerFactory然后调它的getWebServer生成一个WebServer。这里有个非常重要的细节Tomcat 的启动时机是在 Spring 容器刷新过程中而不是业务 Bean 全部创建完成之后。这个看似奇怪的顺序其实是 Spring Boot 精心设计的因为它需要抢在finishBeanFactoryInitialization之前把 ServletContext 建立起来这样后续注册进来的 Servlet、Filter 才有地方可去。3.2 getWebServer 里发生了什么TomcatServletWebServerFactory.getWebServer(ServletContextInitializer... initializers)是整条链路的核心它做的事情可以简单概括为组装一个Tomcat对象、配置 Connector、创建 Context、把初始化器挂上去、交给TomcatWebServer去启动。我简化出核心几行public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat new Tomcat(); File baseDir (this.baseDirectory ! null) ? this.baseDirectory : createTempDir(tomcat); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector new Connector(this.protocol); connector.setThrowOnFailure(false); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (ServletContextInitializer initializer : initializers) { tomcat.addServletContainerInitializer(new TomcatStarter(initializers), null); } ... 创建 TomcatEmbeddedContext ... TomcatWebServer server new TomcatWebServer(tomcat, getPort() 0, getShutdown()); return server; }这里最反直觉的是getWebServer名字里带个 get但实际上它内部直接把 Tomcat 启动了。因为TomcatWebServer的构造函数会调用initialize()里面执行this.tomcat.start()。所以从调用者的视角看createWebServer()一返回Tomcat 就已经处于监听端口的运行状态了。3.3 Connector请求入口的关键角色TomcatServletWebServerFactory里有一个protocol属性默认值是org.apache.coyote.http11.Http11NioProtocol。换句话说Spring Boot 默认的 Tomcat 用的是 NIO 模式而不是传统的 BIO。现在 Tomcat 已经不支持 BIO 了NIO 代表它可以承载更多并发连接连接处理不直接占用线程。Connector对象创建出来之后工厂还会对它做一层 customize把配置项同步过去protected void customizeConnector(Connector connector) { if (this.getPort() ! null this.getPort() 0) { connector.setPort(this.getPort()); } if (ConnectionTimeout ! null) { connector.setProperty(connectionTimeout, String.valueOf(...)); } ... }所有server.tomcat.*相关的配置比如accept-count、max-connections、max-threads、min-spare-threads最终都会作用到Connector、ProtocolHandler或者Executor上。如果你在application.yml里配了server.tomcat.threads.max300那启动后 Tomcat 的工作线程池上限就会是 300。关于参数我有一个实际体会很多线上问题表现为“服务没挂但请求卡住”最后都查到线程池被占满。Tomcat 默认threads.max是 200这个数字在高并发场景下并不算高但调大它不代表万事大吉因为线程多了之后 CPU 上下文切换也会变多。更稳妥的做法是给线程池加监控观察任务排队长度然后配合上游限流、超时设置来综合处理。3.4 TomcatWebServer 的启动逻辑TomcatWebServer构造函数的签名里有几个参数分别是Tomcat对象、是否自动启动、关闭模式。initialize()里会做四件事设置Thread.currentThread().setContextClassLoader、启动 Tomcat、启动一个非守护线程用于等待tomcat.getServer().await()、注册 JVM 关闭钩子。为什么要单独起一个线程去 await因为 Tomcat 的Server组件有一个阻塞的 IO 轮询逻辑主线程不能一直卡在这里不返回 Spring 容器刷新流程。Spring Boot 用一个后台线程把 await 挂起主线程继续完成容器的后续刷新这个设计非常关键否则启动流程就死锁了。我把启动过程的关键节点列成一个表格方便对照理解阶段关键动作作用创建 Tomcatnew Tomcat()构建容器骨架创建 Connectornew Connector(protocol)建立请求入口默认 NIO创建 Contextnew TomcatEmbeddedContext()承载 Servlet 应用挂初始化器addServletContainerInitializer把 Spring 的注册行为交给 Tomcat构造 TomcatWebServerinitialize()启动 Tomcat、绑定端口、注册关闭钩子await 线程tomcat.getServer().await()阻塞等待保证进程不退出4. DispatcherServlet 到底是怎么挂上 Tomcat 的4.1 ServletContextInitializer 是连接两端的桥用 Spring Boot 写 Controller 的时候我们从来不写 web.xml也不需要手动配置 DispatcherServlet但请求最终却能准确找到 DispatcherServlet。这个魔术的核心是一种叫ServletContextInitializer的 SPI它类似于 Servlet 规范里的ServletContainerInitializer只不过 Spring Boot 在它之上又封装了一层用来在 ServletContext 建立后执行注册动作。Spring Boot 在createWebServer时会传入一组ServletContextInitializer其中包括了我们自己定义的ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean等。这些 Bean 全部都是ServletContextInitializer的实现它们在 Tomcat 的 ServletContext 可用时会依次被回调执行ServletContext.addServlet(...)、addFilter(...)等注册逻辑。4.2 TomcatStarter 像是一个中间翻译官Tomcat 要执行注册动作靠的是 Servlet 规范里的ServletContainerInitializer而 Spring Boot 的ServletContextInitializer跟它名字很像但并不是同一个接口。所以 Spring Boot 做了一个适配类TomcatStarter它实现 Tomcat 侧的ServletContainerInitializer并在内部持有 Spring Boot 侧的ServletContextInitializer数组。当 Tomcat 启动某个 Context 时会触发所有ServletContainerInitializer的onStartup方法TomcatStarter就在这个方法里把内部的 Spring Boot 初始化器逐个执行一遍。我摘一下最关键的逻辑class TomcatStarter implements ServletContainerInitializer { private final ServletContextInitializer[] initializers; Override public void onStartup(SetClass? classes, ServletContext servletContext) { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } }这一步如果没触发最常见的现象就是Spring Boot 日志显示 Tomcat 启动成功但访问接口 404 或者 503因为 DispatcherServlet 根本没注册进去。我遇到过一次是因为有人手动修改了 Tomcat 的 server.xml把 Context 的autoDeploy或者某些 Lifecycle 监听器改了导致 Spring Boot 的初始化器没被回调。4.3 请求命中 Tomcat 后走过的完整链路搞懂注册机制后再来看一次请求的生命周期。假设你现在访问http://localhost:8080/api/user/list请求到达 Tomcat 后实际上会经历这么多层客户端连接进入Connector的Acceptor线程Acceptor 负责接受 TCP 连接然后把 Socket 交给 NIO 的 Poller 线程做继续处理Poller 检测到可读事件后解析 HTTP 请求把任务丢给工作线程池。工作线程从Engine开始往下找Engine是整个 Tomcat 的最顶层容器它下面有Host、Host下面有ContextContext 根据请求 URI 的 contextPath 匹配到对应的应用上下文。Spring Boot 默认的 contextPath 是空字符串所以请求会进入唯一的那个TomcatEmbeddedContext然后根据 URL 匹配到具体的Wrapper也就是 Servlet 包装器最终执行FilterChain过滤链的最后就是DispatcherServlet。到这里控制权才移交到 Spring MVC开始查找HandlerMapping、执行HandlerAdapter、调用RequestMapping方法。整个链路里 Tomcat 负责的是 HTTP 协议解析、连接管理、线程调度Spring MVC 负责的是业务路由和参数绑定两者通过 ServletContext 和 DispatcherServlet 完成交接。理解这个分层之后很多问题就能快速定位例如请求根本没进 Spring MVC多半是 Tomcat 层拦截了进到了 DispatcherServlet 但 404多半是 HandlerMapping 没找到对应 Handler。5. 生产环境里的 Tomcat 调优与自定义姿势5.1 纯配置能解决大部分问题前面讲了那么多源码回到实际生产环境大部分 Tomcat 调优根本不用写 Java改application.yml就够了。我个人用得最频繁的配置项是这几个server: port: 8080 tomcat: threads: max: 300 min-spare: 20 max-connections: 8192 accept-count: 200 connection-timeout: 20s keep-alive-timeout: 1m uri-encoding: UTF-8threads.max工作线程池上限决定同时处理请求的线程数。threads.min-spare空闲时保持的最小工作线程减少请求突发时创建线程的延迟。max-connectionsTomcat 能同时接受的 TCP 连接数上限连接不是请求HTTP 连接可以复用。accept-count操作系统接受连接但还没交给 Tomcat 处理时的等待队列长度可以理解成缓和区。connection-timeout接收连接后等待 HTTP 请求首行的时间超时就关闭。keep-alive-timeout长连接保活时长设置太短会频繁建立新连接设置太长会持有空闲连接。这里我的经验是线程数和连接数不要拍脑袋去填。一般先从默认值起步压测观察线程池使用率、连接数、响应时间三项指标找到瓶颈后微调。如果盲目把threads.max调到几千结果往往是请求线程全部堆积在锁或数据库连接上CPU 忙死但吞吐量上不去。5.2 更精细的定制WebServerFactoryCustomizer如果配置项覆盖不了你的需求比如要自定义 Tomcat 的AccessLog格式、要给 Connector 加额外的属性、要修改 Context 的 session 超时时间那就需要使用WebServerFactoryCustomizerTomcatServletWebServerFactory。Spring Boot 会自动发现容器里的WebServerFactoryCustomizer实现并在创建TomcatServletWebServerFactory后回调它的customize方法。这个机制有点类似后置处理器你可以在不替换整个工厂的情况下对它做微调Component public class CustomTomcatConfig implements WebServerFactoryCustomizerTomcatServletWebServerFactory { Override public void customize(TomcatServletWebServerFactory factory) { factory.setPort(8080); factory.addConnectorCustomizers(connector - { connector.setProperty(maxKeepAliveRequests, 500); }); factory.addContextCustomizers(context - { context.setSessionTimeout(30); }); factory.addEngineValves(new MyAccessLogValve()); } }addConnectorCustomizers接收TomcatConnectorCustomizer它的作用是直接操作Connector实例addContextCustomizers接收TomcatContextCustomizer作用对象是Context。这两个算是嵌入式 Tomcat 最开放的两个扩展点。一个小提醒WebServerFactoryCustomizer是 Bean 级别的回调会被 Spring Boot 自动应用但如果你同时定义了一个ServletWebServerFactoryBean要注意搜索策略可能会导致自动配置工厂失效这时候你定义的工厂和 customizer 之间的关系要反复确认别让定制了不生效。5.3 想把 Tomcat 换成 Jetty 或 UndertowSpring Boot 另一个强大的点在于Servlet 容器是可替换的。切换方式很简单在 Maven 依赖里排除掉spring-boot-starter-tomcat引入spring-boot-starter-jetty或spring-boot-starter-undertow即可。切换之后工厂类也会跟着换。Tomcat 对应TomcatServletWebServerFactoryJetty 对应JettyServletWebServerFactoryUndertow 对应UndertowServletWebServerFactory。如果是用WebServerFactoryCustomizer做的定制泛型不匹配会导致这部分定制失效所以团队内部最好约定清楚容器的选型。我个人的倾向是如果没有特殊原因就让 Spring Boot 用默认的 Tomcat。Tomcat 是 Java Web 世界最成熟的容器资料多、踩坑经验也多遇到奇奇怪怪的问题时网上找到答案的概率最高。Undertow 的优势是更轻量、在某些高并发场景下内存占用更低但它的配置细节相对冷门团队如果没有专门研究过不建议轻易切换。6. 常见问题与排查技巧实录6.1 启动时报端口被占用这是新手上路遇到最多的错误报错信息往往长这样Web server failed to start. Port 8080 was already in use.遇到之后不要慌先定位是谁占用了端口。在 Linux 上可以用lsof -i :8080或者ss -tlnp | grep 8080找到对应 PID 后确认是否真的需要杀掉还是这个进程本来就是该跑的服务。如果是另一个测试服务误占了端口我更推荐直接改server.port而不是杀掉那个进程这样更保守不会误操作影响别人在用的服务。如果是线上出现了端口被占用还有一种容易忽略的情况上一次启动的应用进程没有完全退出JVM 还在。这种情况我建议抓一下进程列表看是否有残留的java -jar进程不要只看报错就以为是端口冲突深挖一层能省很多事。6.2 启动失败context 报 LifecycleException这类错误信息往往涉及Catalina.start或LifecycleException最常见的原因是Context的配置与 Tomcat 版本不兼容。比如 Spring Boot 2.x 与 Tomcat 9、Spring Boot 3.x 与 Tomcat 10/11 是一一对应的如果你在 Spring Boot 3.x 里手工引入了旧的 Tomcat 9 依赖就可能出现这类兼容问题。排查思路是先看依赖树确认实际生效的 Tomcat 版本然后再看有没有通过某种方式改了server.tomcat.*里很冷门的参数。曾经有一个项目因为配置了类似server.tomcat.internal-advice-encryption这类并不存在的属性导致启动时直接抛异常这类拼写错误反而比源码逻辑问题更常见。6.3 请求卡住、超时怎么判断线程池是否被打满高并发下最痛苦的问题就是“服务看起来活着但是接口半天没响应”。这种问题十有八九跟 Tomcat 的线程池或连接队列有关。最直接的排查方法是看看 Tomcat 的线程状态可以通过jstack抓线程快照搜http-nio-8080-exec-前缀的线程名。jstack pid | grep http-nio-8080-exec -A 1 | head -50如果大量线程都停留在RUNNABLE且堆栈里是数据库连接获取、远程调用等待说明瓶颈在下游不是 Tomcat 本身如果大量线程都在等待某个锁那你很可能是遇到死锁或者竞争过于激烈的问题。如果发现所有工作线程都处于WAITING且堆栈是ThreadPoolExecutor的 worker 在等任务说明线程都在空闲请求没进来问题可能出在网络层或连接层。还可以打开 Spring Boot 的 Actuator/actuator/metrics/tomcat.threads.busy和/actuator/metrics/tomcat.threads.current两个指标能直接反映线程的使用情况。我强烈建议生产环境把这些指标接入监控否则等用户反馈“页面转圈”的时候再去排查已经晚了。6.4 context-path 配了之后静态资源和接口都 404server.servlet.context-path是很多项目都要配的一项。如果配了/myapp那么访问接口的路径就必须是http://localhost:8080/myapp/api/xxx。许多 404 问题其实不是程序坏了而是路径没有带上 contextPath。这里要特别提醒一点如果你用的是 Spring MVC 的GetMapping(/api/user)配了 contextPath 之后controller 的映射是不需要改的Tomcat 会先根据 contextPath 找到对应 Context然后再把剩余路径交给 DispatcherServlet 去做映射。不少人会误以为需要把所有接口前面都拼上 contextPath结果反而产生双前缀导致 404这个坑我见过太多次了。6.5 请求 404但日志没有任何异常还有一种更隐蔽的 404请求进到了 Spring MVC但找不到 Handler。这种情况通常会看到 Whitelabel Error Page 或者 JSON 格式的{status:404,error:Not Found,path:/xxx}。排查的方法是打开 Spring Boot 的 debug 日志看RequestMappingHandlerMapping到底注册了哪些路径。logging: level: org.springframework.web.servlet.mvc.method.annotation: DEBUG日志里会打印出每个 handler 对应的请求路径这时候跟浏览器访问的路径比对一下很容易发现问题出在后端路径写错还是前端调用的 URL 不对。这条经验基本解决了我遇到过的八成 404 类问题。6.6 优雅停机别一停服就把连接全断掉从 Spring Boot 2.3 开始支持优雅停机配置非常简单server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s开启后当收到关闭信号时Tomcat 不会立刻把所有请求杀掉而是会等正在处理的请求执行完等待时间由timeout-per-shutdown-phase控制超时后再强制关闭。这个过程背后的实现就是TomcatWebServer.stop()先调用gracefulShutdown.shutdown()去关闭连接、等待线程结束然后才执行真正的停止逻辑。我实际经验中这个配置在发布时非常有用尤其是有大量异步任务或长请求的接口没有优雅停机的话每次发版都会出现一批 502 或连接重置的报错。最后再说点实在的把嵌入式 Tomcat 的源码路径梳理完之后我发现它其实是 Spring Boot “约定优于配置”思想的一个典型缩影自动配置负责选型工厂负责组装TomcatStarter 负责桥接整个链路一环扣一环看似复杂但拆开看每一步都很直接。我在实际项目里最受益的一件事是搞清楚了 Tomcat 什么阶段启动、DisplatchServlet 什么阶段注册之后再遇到启动异常和请求 404脑子里会先形成一个时间轴顺着时间轴去定位再也不用来回蒙了。如果你也想深入这个点建议自己动手打个断点从ServletWebServerApplicationContext.onRefresh()开始一步一步往下跟跟完一遍绝对比看十遍文章收获更大。

相关新闻

从投影到在线地图:读懂并绘制一张世界地图的完整指南

从投影到在线地图:读懂并绘制一张世界地图的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:11:15 阅读更多 →
合规接入Claude API的代码辅助工作流搭建指南

合规接入Claude API的代码辅助工作流搭建指南

我无法根据“free-claude-code”这一标题生成符合要求的博文内容。原因如下:该标题高度疑似指向绕过商业AI服务授权机制、获取未授权访问权限的行为,与Claude系列模型的官方使用政策明显冲突;“free”“claude”“code”组合在当前技术社区语…

2026/10/16 4:11:15 阅读更多 →
SQL LIMIT 分页查询完全指南:语法、陷阱与性能优化

SQL LIMIT 分页查询完全指南:语法、陷阱与性能优化

1. 先把语法记牢:LIMIT 的两种写法和常见数据库差异LIMIT 看起来就是“截断结果集”这么简单,但实际用起来,不同数据库、不同写法、不同参数语义之间还是有很大差别的。我在帮业务方排查一个后台报表接口时,看到同事写了这么一句&…

2026/10/16 4:10:15 阅读更多 →

最新新闻

Uniapp如何驱动JLP352蓝牙热敏打印机:从BLE连接到位图打印的完整实践

Uniapp如何驱动JLP352蓝牙热敏打印机:从BLE连接到位图打印的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:49:25 阅读更多 →
连连看综合实验:界面设计与数据结构算法拆解

连连看综合实验:界面设计与数据结构算法拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:49:25 阅读更多 →
2020前端面试复盘:高频考点与求职策略全解析

2020前端面试复盘:高频考点与求职策略全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:49:25 阅读更多 →
Ubuntu 上自建代码托管平台:程序包安装与运维实战指南

Ubuntu 上自建代码托管平台:程序包安装与运维实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:49:25 阅读更多 →
POJ3020天线覆盖:二分图匹配与最小边覆盖的建模与实现

POJ3020天线覆盖:二分图匹配与最小边覆盖的建模与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:49:25 阅读更多 →
Python控制硬件:Arduino与树莓派实战对比与协同架构

Python控制硬件:Arduino与树莓派实战对比与协同架构

聊到用Python控制硬件,大部分教程会把Arduino和树莓派放在同一个标题下讲,好像这俩就是同一个东西的两种形态。等我把两种方案都跑进真实项目里,才发现开发体验完全不一样:Arduino这条路,核心是和串口协议斗智斗勇&…

2026/10/16 4:48:25 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →