简介:《Spring Boot启动过程(四)之内嵌Tomcat启动》是一份面向中高级Spring Boot开发者的PDF解析文档,帮助厘清启动阶段内嵌Web容器的创建、配置与初始化链路。文档以源码调用顺序为主线,先解释createEmbeddedServletContainer方法在容器为空与上下文已存在两种场景下的分支判断,再进入getEmbeddedServletContainerFactory说明框架如何通过自动装配获取工厂,并提及对Tomcat、Jetty、Undertow三种容器的定制化支持。随后聚焦TomcatEmbeddedServletContainerFactory,拆解实例化Tomcat、生成并设置BaseDir临时目录、通过ServerProperties配置端口与服务参数等步骤,同时说明getSelfInitializer准备ServletContextInitializer的意义,以及Connector静态代码块对NIO/APR协议、RECYCLE_FACADES等安全与性能参数的影响。从工厂获取到ServletContext初始化,文档均结合源码给出上下文说明,便于对照理解;全文整理为1个PDF文件,压缩包仅235KB,便于离线研读。目前已有1376人学习下载,对排查启动异常、阅读自动配置源码或备战Spring Boot原理面试均有实用价值。
1. 内嵌Tomcat在Spring Boot启动链路里处于哪一环
一个 Spring Boot 应用跑起来,8080 端口开始监听,很多人以为这是 Spring 的魔法,或者以为它偷偷在某个外置 Tomcat 里跑。实际上,在 Spring Boot 启动过程里,内嵌 Tomcat 的启动是一个完全可追踪的代码路径:Spring 容器刷新过程中会创建 Web 服务器工厂,工厂再装配出 Tomcat 实例,最后在容器刷新的收尾阶段把 Tomcat 拉起来。整个过程既不是黑匣子,也不需要你去单独部署一个 Tomcat。这个系列文章停在「内嵌 Tomcat 启动」这一环,正好是排查端口冲突、启动慢、Servlet 注册不生效和优雅停机失效的入口。适合正在做 Spring Boot 应用启动排查,或者想在容器化部署前把 Servlet 容器原理补上的人。理解了这一环,你才算真正看懂Tomcat started on port(s): 8080这行日志是谁打印的。
2. 从 run 到 getWebServer:内嵌 Tomcat 实例化走的是哪条代码路径
2.1 ServletWebServerApplicationContext 与自动配置:工厂 Bean 从哪来
Spring Boot 启动时,SpringApplication.run()最终会 refresh 一个 ApplicationContext。如果你跑的是一个 Web 应用,这个 context 的真实类型是ServletWebServerApplicationContext,而不是普通的AnnotationConfigApplicationContext。这个类型的关键重写在onRefresh()方法里:普通容器这里什么都不做,而 Web 容器在这里调用createWebServer()。
// 逻辑还原,代码来自 Spring Boot 2.x 的 ServletWebServerApplicationContext protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } } private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(this::getSelfInitializer); } }注意看这段逻辑的切入点:createWebServer()先判断webServer和servletContext是不是都不存在。因为 Spring Boot 支持外置容器部署,如果应用打 war 包放到外部 Tomcat 里跑,servletContext是存在的,内嵌逻辑直接跳过。只有在纯粹内嵌场景下,它才会去找ServletWebServerFactory。
getWebServerFactory()的查找逻辑是从当前 BeanFactory 里找类型为ServletWebServerFactory的 Bean。这个 Bean 是哪来的?来自装配类ServletWebServerFactoryAutoConfiguration。这个自动配置类会在 classpath 里检测 Tomcat、Jetty、Undertow 的类,存在哪个就条件装配哪个。默认情况你引入了spring-boot-starter-web,它会把tomcat-embed-core传进来,所以最终装配的是TomcatServletWebServerFactory。这个工厂类型一旦确定,后续getWebServer()创建出来的就是内嵌 Tomcat。整个查厂、选型、创建的过程都在 Spring 的容器生命周期内,所以你能用一个普通的@Bean方法去替换或增强它,前提是你懂它在哪里被调用。
2.2 getWebServer() 方法内部:Tomcat 对象被工厂拼装出来的顺序
TomcatServletWebServerFactory.getWebServer()是内嵌 Tomcat 的出生地。这个方法的代码在 Spring Boot 里经过多次重构,但核心骨架始终稳定:先 new 一个Tomcat实例,再配 baseDir,再创建 Connector,然后准备 Host 和 Context,最后包装成TomcatWebServer。下面这段是我根据 Spring Boot 2.x 逻辑还原的精简版,用来理解顺序足够。
// 逻辑还原,来自 Spring Boot 2.x TomcatServletWebServerFactory public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = createTempDir("tomcat."); tomcat.setBaseDir(baseDir.getAbsolutePath()); // 创建连接器,默认协议是 HTTP/1.1 Connector connector = new Connector("HTTP/1.1"); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); // engine、host、context 的装配 prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); }顺序值得反复看:先有 Connector,再 prepareContext。这意味着server.port、server.address这些配置在 Connector 创建时就生效了。new Tomcat()时默认会创建一个 StandardServer 和一个 StandardService,tomcat.getService()拿到的就是那个默认 Service,Connector 被塞进 Service 里,在后续 start 阶段由 StandardService 统一拉起。
prepareContext里的动作更关键。它会创建一个TomcatEmbeddedContext,设置 contextPath、docBase,并且为这个 Context 添加一个TomcatStarter。这个 Starter 是一个ServletContainerInitializer,它是以后内嵌 Tomcat 里所有 Servlet 注册动作的入口。很多人后面排查 Servlet 注册不生效时找不到下手点,就是没意识到所有注册动作都是从这个 Context 里的 Starter 触发的。
2.3 为什么默认是内嵌 Tomcat 而不是 Jetty 或 Undertow
不是 Tomcat 在产品能力上碾压另外两个,而是 Spring Boot 的默认选择恰好落在 Tomcat 上。spring-boot-starter-web直接依赖了spring-boot-starter-tomcat,这是 POM 层面的默认可传递依赖。如果你换用spring-boot-starter-jetty或spring-boot-starter-undertow,并排除 Tomcat starter,容器类型就会切换。切换后,上面说的工厂类型变成了JettyServletWebServerFactory或UndertowServletWebServerFactory,但createWebServer()的调用路径完全不变。这就是 Spring Boot 的 Servlet 容器抽象层做得好的地方:改变的是工厂实现,不变的是 Spring 容器对 启动时机的控制。对排查问题的人来说,你只需要知道自己项目里最终装配的是哪个工厂,就可以顺着对应工厂的getWebServer()往下看细节。
有一点容易被忽略:Spring Boot 3.2 之后支持虚拟线程,但官方配置只对 Undertow 生效,内嵌 Tomcat 和 Jetty 都不支持。如果项目要用虚拟线程跑 Web 层,不要只在配置里改spring.threads.virtual.enabled=true,还得换 Undertow。这类边界只有真正打开工厂源码才能看到。
3. 动手改内嵌 Tomcat:从连接器端口到线程池的定制点
3.1 用 WebServerFactoryCustomizer 在启动前改掉默认行为
启动时想改 Tomcat 的端口、协议、额外 Connector,核心入口是WebServerFactoryCustomizer<TomcatServletWebServerFactory>。这是 Spring Boot 专门留给开发者做容器定制的扩展点。实现方式很简单,定义一个返回该类型自定义逻辑的@Bean方法即可:
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) public class TomcatCustomConfig { @Bean public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() { return factory -> { // 修改主端口,覆盖 application.yml 里的 server.port factory.setPort(18080); // 设置 contextPath factory.setContextPath("/api"); // 修改 Connector 级参数,这里拿到的 connector 是主连接器 factory.addConnectorCustomizers(connector -> { connector.setProperty("acceptCount", "128"); connector.setProperty("maxKeepAliveRequests", "200"); }); }; } }这段逻辑要强调一下factory.setPort(18080)的作用:它会覆盖server.port配置值。凡是往TomcatServletWebServerFactory上 set 的值,优先级都高于application.yml,因为配置绑定也是通过给这个工厂对象设值实现的,定制器的执行顺序在配置绑定之后。如果你在定制器里 set 了和配置文件里不同的值,以定制器为准。这个覆盖关系,是我排查过不少次配置不生效问题后才彻底搞清楚的。
addConnectorCustomizers接收的TomcatConnectorCustomizer会在 Connector 创建完成后、start 之前回调。acceptCount是操作系统的 accept 队列长度,不是线程池大小;请求量瞬间打满时,队列长一点能缓解 503。maxKeepAliveRequests控制 keep-alive 连接上最多能复用多少次请求,HTTP/1.1 下默认是 100,改成 200 可以减少重复建连的开销。注意这里的值是字符串,Connector 的 setProperty 属性值统一走 String。
3.2 配置文件里能直接控制的 Tomcat 参数与默认值
Spring Boot 把常用的 Tomcat 参数在application.yml里做了结构化绑定,大多数场景不用写代码。下面是 Spring Boot 2.3 之后主流的配置命名,3.x 兼容沿用:
server: port: 8080 address: 0.0.0.0 tomcat: threads: max: 200 # 工作线程上限,默认 200 min-spare: 10 # 最小空闲工作线程,默认 10 accept-count: 100 # accept 队列长度,默认 100 max-connections: 8192 # 最大连接数,默认 8192 connection-timeout: 20000 # 连接超时,单位毫秒,默认 20 秒| 配置项 | 默认值 | 作用 | 常用调整建议 |
|---|---|---|---|
server.tomcat.threads.max | 200 | 处理请求的工作线程上限 | I/O 密集型可调大到 400 |
server.tomcat.threads.min-spare | 10 | 启动时预创建的线程数 | 避免突发流量时现场建线程 |
server.tomcat.accept-count | 100 | 等待被 accept 的连接队列长度 | 高并发短连接场景可设到 256 |
server.tomcat.max-connections | 8192 | 服务器接受的最大连接数 | 受文件描述符上限约束 |
server.tomcat.connection-timeout | 20000 | Connector 接受连接后的超时时间 | API 接口通常调小到 5000 |
关于命名有个坑要提醒:Spring Boot 2.3 之前线程池配置叫server.tomcat.max-threads和server.tomcat.min-spare-threads,2.3 开始改为server.tomcat.threads.max和server.tomcat.threads.min-spare。如果你把老项目的配置直接抄到新版本里,会发现线程池参数根本不生效。我见过一个服务把max-threads写成 1000,实际压测并发到 200 就上不去了,查了半天日志,最后发现是配置前缀已经变更。升级版本时,配置项的变更日志比功能特性更值得先看。
3.3 加一个 Connector:让内嵌 Tomcat 双端口监听
有些场景需要同一个 Spring Boot 应用监听两个端口,比如一个端口对外提供 API,另一个端口只在本机回环地址上暴露管理接口。Spring Boot 的TomcatServletWebServerFactory提供了addAdditionalTomcatConnectors(),专门干这个。自定义如下:
@Bean public WebServerFactoryCustomizer<TomcatServletWebServerFactory> additionalConnectorCustomizer() { return factory -> { Connector managementConnector = new Connector("HTTP/1.1"); managementConnector.setPort(19090); managementConnector.setProperty("address", "127.0.0.1"); managementConnector.setProperty("acceptCount", "64"); factory.addAdditionalTomcatConnectors(managementConnector); }; }这个 Connector 创建后会被添加到默认 Service 里,和主 Connector 一起进入后续的 start 流程,所以两个端口启动是同时生效的。需要注意两点:addr属性不要拼写错误,Connector 的属性名是address;另外,通过addAdditionalTomcatConnectors加进去的连接器不会受到server.port配置的影响,它完全由你代码里 set 的值决定。想要关闭主端口只留额外端口,思路可以是把主端口设为 -1,再额外添加实际监听端口,但你得想清楚应用对外的端口语义,避免运维同学对着两个端口困惑。
4. 时序真相:端口先开,Servlet 后注册,DispatcherServlet 在内嵌 Tomcat 里怎么挂上去
4.1 从 refresh 到 finishRefresh:内嵌 Tomcat 的 start 被谁调用
上一章看到的createWebServer()只完成了 Tomcat 对象的创建和静态装配,真正的启动动作发生在容器刷新的末尾阶段。Spring Boot 设计了一个WebServerStartStopLifecycle,它实现SmartLifecycle,在容器 refresh 完成时被回调,回调里调用webServer.start()。而 内嵌 Tomcat 场景下,这个webServer就是TomcatWebServer。
// 逻辑还原:Spring Boot 内嵌容器启动回调 class WebServerStartStopLifecycle implements SmartLifecycle { private final WebServer webServer; @Override public void start() { this.webServer.start(); // 此刻调用 TomcatWebServer.start() } }时机的精妙之处在于:当TomcatWebServer.start()执行时,Spring 容器中的大多数单例 Bean 已经被创建完毕,DispatcherServlet的 Bean 也已经被实例化,只是还没被注册到 ServletContext。内嵌 Tomcat 的启动是「先启动容器,再在启动过程中回调初始器完成 Servlet 注册」,这正好和外置 Tomcat 部署时先有 ServletContext、再由 WAF 扫描 Servlet 的时机相反。理解这个先后关系,很多 404 和注册顺序的问题就有了排查方向。
4.2 Tomcat 自身生命周期:StandardServer、StandardService、Connector 的 start 链
Tomcat 把自己定义为一个组件树,TomcatWebServer.start()最终只做一件事:调用tomcat.start()。这个调用会走 Tomcat 标准生命周期链,从最上层开始逐层启动。顺序固定如下:
| 层级 | 组件 | 启动动作 | 说明 |
|---|---|---|---|
| 1 | StandardServer | 启动 Server 生命周期 | 入口,管理全局生命周期 |
| 2 | StandardService | 启动 Service | 持有 Connector 和 Engine |
| 3a | Connector | 启动连接器 | 绑定端口,开始 accept 连接 |
| 3b | Engine | 启动引擎 | 准备处理请求的管道 |
| 4 | Host | 启动虚拟主机 | 管理 Context 集合 |
| 5 | Context | 启动应用上下文 | 触发ServletContainerInitializer |
注意 3a 排在 3b 前面:端口绑定发生在 Servlet 注册之前。也就是说,Tomcat started on port(s): 8080打印出来时,端口已经监听,但DispatcherServlet可能还没注册完。极端情况下,端口已开、请求已经能进来,但由于 Context 尚未完成初始化,先到的请求会在队列里等一会儿。这就是为什么启动刚完成那一瞬间的请求偶尔会出现短暂慢请求,不是线程池被占满,而是 Servlet 初始化还没结束。
这个顺序在排查「端口被占用但堆栈上看不到 Tomcat」时也很有用:bind()失败发生在 3a 阶段,异常会被封装成WebServerException,如果你看到启动日志里有Port 8080 was already in use,直接去查监听 8080 的进程就行,不用怀疑是 Tomcat 没走到绑定那一步。
4.3 注册两条线:TomcatStarter 与 SpringServletContainerInitializer
端口绑定完成后,启动链走到 Context.start()。Tomcat 会扫描 Context 里注册的ServletContainerInitializer,并执行它们的onStartup()。Spring Boot 装配 Context 时注册的是一个叫TomcatStarter的初始器,它身上挂着 Spring 传入的ServletContextInitializer数组。这里有两个容易混淆的概念,说清楚:
- TomcatStarter是内嵌 Tomcat 用来打通 Spring 的回调入口。
- SpringServletContainerInitializer是 spring-web 里的标准
ServletContainerInitializer,它在 Spring Boot 内嵌模式下负责收集WebApplicationInitializer。
TomcatStarter 的 onStartup 逻辑里做了两件事:先触发容器内所有 ServletContainerInitializer,再调用 Spring 传给它的 selfInitialize 回调。下面这段是还原后的代码逻辑:
// 逻辑还原:TomcatStarter 在 Context 启动时被 Tomcat 回调 public void onStartup(Set<Class<?>> classes, ServletContext servletContext) { // 1. 触发容器里的 ServletContainerInitializer for (ServletContainerInitializer initializer : initializers) { initializer.onStartup(classes, servletContext); } // 2. 回调 Spring 自己的初始化逻辑 this.rootContext.selfInitialize(servletContext); }最后selfInitialize内部会找出所有ServletContextInitializerBean,按序执行。DispatcherServletRegistrationBean就是其中之一。这个 Bean 是ServletWebServerApplicationContext在启动过程中自动注册的,它的作用是拿到前面创建的DispatcherServlet实例,把它 add 到 ServletContext 上,并映射到/。所以整个注册链路是:
Tomcat Context.start() -> TomcatStarter.onStartup() -> ServletWebServerApplicationContext.selfInitialize() -> ServletContextInitializerBeans -> DispatcherServletRegistrationBean.onStartup() -> ServletContext.addServlet("dispatcherServlet", dispatcherServlet)看到这里你就会明白,为什么 Spring Boot 里@WebServlet注解不生效——内嵌 Tomcat 场景下,TomcatStarter传给 Spring 的 selfInitialize 才是注册的主入口,而@WebServlet需要 Tomcat 扫描 classpath 才生效。Spring Boot 默认没有开启 WebServlet 注解扫描。想要绕开这个机制,你的DispatcherServletRegistrationBean顺序就变得很关键,这也是为什么自定义ServletRegistrationBean时要小心和默认注册顺序冲突。
5. 内嵌 Tomcat 启动避坑:端口、临时目录、线程池与优雅停机
5.1 端口被占用时日志指向不明,先查监听再改配置
现象:启动日志出现Web server failed to start. Port 8080 was already in use.,但紧接着的一堆堆栈里看不到 Tomcat 绑定端口的代码,容易误判成 Spring 容器初始化失败。
原因:端口绑定发生在Connector.start()里,属于 Tomcat 生命周期 3a 阶段。Spring Boot 把 bind 异常包装成WebServerException,堆栈顶部看起来是 ApplicationContext 刷新失败,实际根源是在 Tomcat 连接器绑定端口时抛出的。
解决:先执行lsof -nP -iTCP:8080 -sTCP:LISTEN找到占用进程,确认不是自己项目的旧进程残留,再决定是改server.port还是清理旧进程。排查时不要只盯着启动日志看,Tomcat started on port(s)没打印,说明 Tomcat 的启动链根本没走完。另外,如果你在配置里把server.port设为0,Spring Boot 会自动选一个空闲端口,这在多实例本地联调时是个省事招,但会让运维很难管理,生产环境不建议用。
5.2 定制器写了不生效,先检查泛型和配置类扫描范围
现象:在配置类里定义了WebServerFactoryCustomizer<TomcatServletWebServerFactory>,运行时却发现端口没变、线程池参数没变,好像定制器被直接忽略了。
原因:三种常见情况。第一,泛型参数写成了WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>,Spring Boot 在收集定制器时会做精确类型匹配,泛型不匹配会导致该定制器只作用于对应类型的工厂,而实际装配的是TomcatServletWebServerFactory,类型对不上就找不到。第二,配置类放在了启动类扫描不到的包路径下,@Configuration根本没被加载。第三,同一个工厂类型存在多个定制器,执行顺序出了问题,后执行的定制器把前面的值覆盖了。
解决:把泛型精确到TomcatServletWebServerFactory;用@Bean方法返回定制器时,配置类放在主启动类所在的包或子包下。需要控制多个定制器执行顺序时,定制器类可以实现Ordered接口,order值小的先执行。检查是否生效最快的办法是临时在定制器逻辑里加一条System.out.println,启动时看有没有打印——有打印说明定制器被加载,结果不对再查属性覆盖;没有打印直接查配置类加载。
5.3 临时目录被清理导致 Session 数据丢失和上传临时文件报错
现象:应用运行几天后,用户 Session 凭空失效,上传大文件的接口偶发 IO 异常,重启后恢复,过几天又出现。
原因:内嵌 Tomcat 的 baseDir 默认创建在系统临时目录下,格式是tomcat.<随机串>。Linux 的/tmp或系统临时目录清理任务会定期删除超过一定时间的目录,Tomcat 运行期间如果 baseDir 被删,Session 持久化和上传临时文件都会出问题。
解决:通过server.tomcat.basedir指定一个固定目录,或者直接用TomcatServletWebServerFactory.setBaseDir()指向应用自己的数据目录。生产环境建议设置为/data/app/tomcat这类独立于系统临时目录的位置,同时确认应用有该目录的读写权限。如果你确实不需要 Session 集群之外的本地持久化,也可以在开发环境忽略这个问题,但生产环境不要赌临时目录清理周期。
5.4 线程池调到 1000 还是不生效,先查配置项版本命名
现象:在配置里写了server.tomcat.max-threads: 1000,压测发现并发到 200 左右就出现排队或拒绝连接,线程数永远到不了配置值。
原因:server.tomcat.max-threads是 Spring Boot 2.3 之前的命名,Spring Boot 2.3 开始改成server.tomcat.threads.max。Spring Boot 对未知配置项不会报错,启动日志一切正常,配置在静默失效。
解决:先确认项目 Spring Boot 版本。2.3 及以上使用server.tomcat.threads.max,2.3 以下使用server.tomcat.max-threads。调大线程池之前先看一眼max-connections和accept-count,这三个值配合不当会出现「线程池还空闲,但连接已经排满」的情况。调优顺序建议是:先定 max-connections,再定 accept-count,最后调线程池。刻意压测时可以同时调这三个指标观察曲线,不要只盯线程池。
5.5 优雅停机配置不生效,请求被直接掐断
现象:配置了server.shutdown: graceful,但应用进程收到终止信号后,正在处理的请求还是被立即断开,没有等当前请求跑完。
原因:优雅停机需要 Spring Boot 2.3 以上版本,并且服务的生命周期回调要能被正确触发。在容器化环境里,发送的信号类型和spring.lifecycle.timeout-per-shutdown-phase的超时配置都会影响效果。另一个常见原因是应用里存在大量非 Web 层的异步任务,Web 层的请求等到了超时时间,但异步线程还在跑,整体停机慢,容器健康检查判定失败后被强制杀掉。
解决:先确认版本和配置:
server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s超时时间按业务里最慢请求的预期耗时去设,不要无脑设大。依赖JVM退出钩子的中间件要明确它们的资源释放逻辑是否在 Spring 的停机生命周期内完成。我自己遇到过 Redis 连接池在停机阶段被提前关闭、导致正在处理的请求抛连接异常的情况,最后还是通过调整 Bean 销毁顺序和停机阶段回调的协作才压住。这个坑调起来需要耐心,但方向是清楚的。
6. 验证内嵌 Tomcat 启动是否健康:从日志到命令行的几个实在技巧
启动完看日志是最基础的一步,但只看Tomcat started on port(s)不够。我验证内嵌 Tomcat 启动是否真的如预期,按下面这套命令来:
# 1. 确认端口监听在预期的地址上 lsof -nP -iTCP:8080 -sTCP:LISTEN # 2. 确认工作线程改名后的线程名存在,http-nio-8080-exec- 是 NIO 线程池 jstack <pid> | grep -c 'http-nio-8080-exec-' # 3. 确认响应头里能看到内嵌容器版本特征 curl -I http://127.0.0.1:8080/actuator/health第一句看端口绑定的地址是不是0.0.0.0,如果配了server.address要注意地址对不对;第二句是从线程名反向验证 Tomcat 的 NIO 接收器线程和工作线程真的干活了,线程名的数字段就是端口号,双端口场景会看到http-nio-19090-exec-另一组线程;第三句是确认请求真正能打到 DispatcherServlet 上,这一步会连带验证 Servlet 注册是否成功。
想再进一步,可以在压测时抓线程栈:并发压到接近线程池上限,然后jstack <pid> > dump.txt,重点看http-nio-8080-exec-*线程的栈顶是停在 JDBC 查询、Redis 调用还是业务代码里。这一步能直接回答「线程池参数调了到底有没有用」和「请求到底卡在哪一层」。我压测过不少接口,线程池满了但线程几乎全闲置在连接获取上,这种情况调线程池是治标不治本,该查的是连接池大小。
内嵌 Tomcat 的启动验证说到底就一件事:确认端口、线程、Servlet 三个层面都到位了。端口是入口,线程是执行力,Servlet 注册是业务可达性。我个人的习惯是启动脚本里固定放一条 lsof 和一条 curl,端口没起来就发告警,curl 不通就进线程栈定位。这套习惯帮我在一个午夜发布时抓到过线程池被写错配置现场,问题定位从半小时缩短到了五分钟。希望帮到你。
本文还有配套的精品资源,点击获取