当前位置: 首页 > news >正文

揭秘Spring Boot内嵌Tomcat原理

tomcat 介绍

tomcat 是 web容器(servlet 容器),不管请求是访问静态资源HTML、JSP还是java接口,对tomcat而言,都是通过servlet访问:

  • 访问静态资源,tomcat 会交由一个叫做DefaultServlet的类来处理。
  • 访问 JSP,tomcat 会交由一个叫做JspServlet的类来处理。
  • 访问 Servlet ,tomcat 会交由一个叫做 InvokerServlet的类来处理。

所谓 jsp 就是 html 加上 java 代码片段,JspServlet 最终输出的也是 html 而已。

tomcat 启动 spring 项目

了解 springboot 内嵌 tomcat 启动原理之前,应该了解“祖先” spring 怎么启动和加载上下文的,对 springboot 的理解才深刻。

web.xml配置

spring 启动必须依赖 web 容器,这里以 tomcat 举例。

spring 项目中简单的 web 配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/root-context.xml</param-value> </context-param> <servlet> <servlet-name>app1</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/app1-context.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>app1</servlet-name> <url-pattern>/app1/*</url-pattern> </servlet-mapping> 
</web-app>

spring 怎么启动?

tomcat 通过调用 spring 中的 servlet 对象(DispatcherServlet),然后调用该对象的 init() 方法,作为入口启动spring。

如何调用到 spring 中的 servlet 对象,分为以下两种:

  • web.xml 配置文件方式:tomcat 通过 web.xml 配置文件中 servlet 标签指定的 servlet 类路径,反射生成 servlet 对象。
  • java config 方式:没有 web.xml 配置文件了,项目中必须实现 WebApplicationInitializer 接口,并现实 WebApplicationInitializeronStartup 方法,在 onStartup 方法中创建 dispacherServlet 并指定使用。

上述 java config 方式衍生出另一个问题:WebApplicationInitializer 的实现类怎么被 tomcat 调用到呢?

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {@Overridepublic void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)throws ServletException {......}
}
  • tomcat 会通过SPI机制,找到 ServletContainerInitializer 接口的所有实现类,然后反射生成对象,并轮流执行实现类中 onStartup 方法。SpringServletContainerInitializer 就是 ServletContainerInitializer 实现类之一。
  • SpringServletContainerInitializer 类上有@HandlesTypes({WebApplicationInitializer.class})注释,这个注释是 servlet 规范中的,tomcat 会通过字节码加载技术(ASM),找到注解中指定类及子类(WebApplicationInitializer.class 及子类),然后将其作为参数传给 SpringServletContainerInitializeronStartup方法使用。我们自定义的WebApplicationInitializer 实现类就这样被调用到了 。

所以我们实现了 WebApplicationInitializer 接口,就会被 tomcat 加载。

怎么加载 springContext?

tomcat 通过调用 DispatcherServlet 对象的 init() 方法,加载 springContext 上下文,所以 DispatcherServlet 对象内有一个字段存放 springContext。

若配置多个 servlet,springContext 有几个?

  • 每个 servlet 创建过程中,都会创建 springcontext,各 servlet 都有自己的上下文。
  • 所以各 servlet 的 init-param 标签中,指定的扫描路径如果有重复的,重复的 bean 对象也会在各 servlet 的 springcontext 中新建,不会共用。
  • 那么重复的 bean 对象是不是有点浪费内存呢?确实如此,所以在多个servlet的情况下,需要配 listener 标签,表示配置父容器(root context)。可以合理配置父容器加载 service 和 repository 层的 bean,这部分可以共用,而 controller 层的 bean 是在各 servlet 中的子容器中加载,因为涉及到servlet路由嘛。

不过一般都不会有多个servlet,通常常规项目中 web.xml 中 listener 标签根本就不需要配置。

springboot 启动 tomcat

启动 web 容器

上述说的 spring 启动,必须依赖 web 容器启动。由 web 容器通过 SPI 机制,加载 spring 自己的 servlet DispatcherServlet,再通过 servlet 对象创建 springContext

而 springboot 不需要外部 web 容器了,那它怎么监听端口接收请求呢,难道 springboot 内部又重新写了一个 servlet?当然不是,看下面代码:

@Override
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// Prepare this context for refreshing.prepareRefresh();// Tell the subclass to refresh the internal bean factory.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.prepareBeanFactory(beanFactory);try {// Allows post-processing of the bean factory in context subclasses.postProcessBeanFactory(beanFactory);// Invoke factory processors registered as beans in the context.invokeBeanFactoryPostProcessors(beanFactory);// Register bean processors that intercept bean creation.registerBeanPostProcessors(beanFactory);// Initialize message source for this context.initMessageSource();// Initialize event multicaster for this context.initApplicationEventMulticaster();// Initialize other special beans in specific context subclasses.// 同时会创建 web 容器。onRefresh();// Check for listener beans and register them.registerListeners();// Instantiate all remaining (non-lazy-init) singletons.finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.finishRefresh();}......}......
}

上述代码片很熟悉吧,如果不熟的查查spring初始化context流程,这里不做详细描述。
主要看 onRefresh() 方法,该方法启动容器的流程大概如下,以 tomcat 为例:

  • onRefresh() 方法内,会调用 createWebServer() 方法,createWebServer() 方法内会调用 tomcat||jetty||undertow jar 包依赖提供的方法,来创建 web 容器并启动。
  • 比如 tomcat,它是这样创建:new Tomcat()
  • 创建容器后,对 tomcat 容器的Connector进行配置,并将 DispatcherServlet 添加到 tomcat 容器中。
  • 最后 tomcat.start() 启动容器。

tomcat\jetty\undertow springboot用哪个?

我们了解到了 springboot 会调用 createWebServer() 方法,创建“合适”的 web 容器。

  • 那 springboot 怎么判断该创建 tomcat、jetty 还是 undertow 容器呢?

实际上就是在 createWebServer() 方法里面判断的,该方法代码如下:

private void createWebServer() {WebServer webServer = this.webServer;ServletContext servletContext = getServletContext();if (webServer == null && servletContext == null) {// 选择用哪个容器ServletWebServerFactory factory = getWebServerFactory();// 创建及启动 web 容器this.webServer = factory.getWebServer(getSelfInitializer());getBeanFactory().registerSingleton("webServerGracefulShutdown",new WebServerGracefulShutdownLifecycle(this.webServer));getBeanFactory().registerSingleton("webServerStartStop",new WebServerStartStopLifecycle(this, this.webServer));}else if (servletContext != null) {try {getSelfInitializer().onStartup(servletContext);}catch (ServletException ex) {throw new ApplicationContextException("Cannot initialize servlet context", ex);}}initPropertySources();
}

上述代码片,getWebServerFactory() 方法会获得 tomcat||jetty||undertowServletWebServerFactory, 用这个 factory 对象就能创建对应的 web 容器。

而在 getWebServerFactory() 方法内是按类型从 SpringContext 中获取 ServletWebServerFactory 类型的 bean。

所以只要 SpringContext 注入什么容器的 ServletWebServerFactory,springboot 就会启动什么容器。

  • 什么地方注入 ServletWebServerFactory 呢?

ServletWebServerFactoryConfiguration 这个配置类,将tomcat||jetty||undertowServletWebServerFactory 注入进 springContext,配置类伪代码如下:

@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {   // tomcat 的 factory@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)static class EmbeddedTomcat {@BeanTomcatServletWebServerFactory tomcatServletWebServerFactory( ... ) { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();... return factory;}}// Jetty 的 factory@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)static class EmbeddedJetty {@BeanJettyServletWebServerFactory JettyServletWebServerFactory( ... ) { JettyServletWebServerFactory factory = new JettyServletWebServerFactory();... return factory;}}// Undertow 的 factory.......
}

可以看出只要引入了某 web 容器的依赖,对应的 @ConditionalOnClass 就能满足,该 web 容器的 ServletWebServerFactory 就会被注入进 springboot。

  • 那项目如果引入了多个 web 容器依赖,springboot 使用哪一个?

还想使用哪个??getWebServerFactory() 方法内就直接报错了:Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : xxx

  • 我们平时使用 tomcat ,那 jetty、undertow 等容器依赖便不会引入。那上述 ServletWebServerFactoryConfiguration 类的代码,@ConditionalOnClass 中某些类肯定找不到,运行时不会报错吗?

首先应该知道 spring 怎么判断是否是 bean 对象?常人理解 spring 是通过 JVM 反射获取类注解信息,来确定是否反射生成 bean 对象注入 SpringContext 中。

但实际 spring 并不是这样判断的,如果通过 JVM 获取类信息,那不是启动前要把所有类都加载一次,这和 JVM 用时加载的思想冲突了。所以spring 是通过 ASM 技术从 class 字节码文件中获取注解信息,来判断是否是需要的 bean。

之所以没有依赖也不会报错,是因为spring 会通过 ASM 技术取出 @ConditionalOnClass 注解中所有的 values后,会用 ClassLoader 尝试加载这些 values,如果加载不到,catch住异常使其不会报错,同时这个类也被认为不符合注入条件,不会生成对象注入 springcontext。所以没引入所有 web 容器依赖也不会报错。

相关文章:

揭秘Spring Boot内嵌Tomcat原理

tomcat 介绍 tomcat 是 web容器&#xff08;servlet 容器&#xff09;&#xff0c;不管请求是访问静态资源HTML、JSP还是java接口&#xff0c;对tomcat而言&#xff0c;都是通过servlet访问&#xff1a; 访问静态资源&#xff0c;tomcat 会交由一个叫做DefaultServlet的类来处…...

分类散点图 stripplot() 加辅助线axhline() 多图合一

分类散点图 stripplot 加辅助线axhline 多图合一 效果图代码 画图没有什么可说的&#xff0c;直接上图 效果图 代码 # 绘制图&#xff0c; 查看是否数值在阈值上 plt.figure(figsize(30, 18)) n 0 for header, value_list in info_dict.items():ref_value_list ref_info_dic…...

一文告诉你为什么时序场景下 TDengine 数据订阅比 Kafka 好

在 TDengine 3.0 中&#xff0c;我们对流式计算、数据订阅功能都进行了再升级&#xff0c;帮助用户极大简化了数据架构的复杂程度&#xff0c;降低整体运维成本。TDengine 提供的类似消息队列产品的数据订阅、消费接口&#xff0c;本质上是为了帮助应用实时获取写入 TDengine 的…...

reg与wire的用法,证明reg可以在右边,wire型在左边,来作组合逻辑处理。

reg与wire的用法&#xff0c;证明reg可以在右边&#xff0c;wire型在左边&#xff0c;来作组合逻辑处理。 1&#xff0c;RTL2&#xff0c;生成的原理图 1&#xff0c;RTL 参考文献&#xff1a; 1&#xff0c;verilog 中 wire 和reg 的使用 2&#xff0c;解决一个assign问题&…...

Studio One6.2简体中文免费最新版本宿主软件

对于一些有创作需求的朋友来说&#xff0c;为自己写的歌制作伴奏是很平常的。今天要和大家分享的就是自己写的歌怎么做伴奏&#xff0c;自己做伴奏的软件有哪些。Studio One是宿主软件界的一个后起之秀&#xff0c;推出的时间不久&#xff0c;但是受到了大量音乐制作人的推崇。…...

算法刷题 week2

目录 week21. 二维数组中的查找题目题解(单调性扫描) O(nm) 2.替换空格题目题解(线性扫描) O(n)(双指针扫描) O(n) 3.从尾到头打印链表题目题解(遍历链表) O(n) week2 1. 二维数组中的查找 题目 题解 (单调性扫描) O(nm) 核心在于发现每个子矩阵右上角的数的性质&#xff1…...

子网的划分

强化计算机网络发现王道没有这一块的内容&#xff0c;导致做题稀里糊涂。于是个人调研补充。 子网划分是将一个大型IP网络划分成更小的子网&#xff0c;以实现更有效的网络管理和资源分配。 原因&#xff1a; 提高网络性能&#xff1a;子网划分可以减少广播域的大小&#xff…...

Docker安装与卸载

Docker安装与卸载 安装 yum install -y yum-utils \device-mapper-persistent-data \lvm2 --skip-broken更新本地镜像源 打开终端或 SSH 连接到 Rocky Linux 的服务器。 进入 /etc/yum.repos.d/ 目录&#xff0c;该目录包含 Rocky Linux 的 yum 配置文件。 cd /etc/yum.repo…...

【Davinci开发】:开发过程问题记录及总结

开发过程问题总结 1、SWC访问系统OS Timer返回值异常a、代码发现,RTE接口为未连接状态b、连接后,仍然有问题,单步调试,发现没有访问权限当新平台基于之前平台的代码而延续开发时(应用代码相同,但是芯片已经更换),记录开发过程中遇所到的问题,单步调试,逐一排查。 1、…...

数据结构——排序算法——冒泡排序

冒泡排序1 void swap(vector<int> arr, int i, int j) {int temp arr[i];arr[i] arr[j];arr[j] temp;}void bubbleSort1(vector<int> arr) {for (int i 0; i < arr.size() - 1; i){for (int j 0; j < arr.size() - 1 - i; j){if (arr[j] > arr[j 1…...

vscode使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 例如&#xff1a;…...

python经典百题之求前!的和

题目&#xff1a;求12!3!…20!的和 方法一&#xff1a; 使用for循环和阶乘函数计算每项的值&#xff0c;再将每项的值累加起来。 def factorial(n):if n 0:return 1else:return n * factorial(n-1)sum 0 for i in range(1, 21):sum factorial(i) * iprint(sum)优点&#…...

C语言入门Day_22 初识指针

目录 前言&#xff1a; 1.内存地址 2.指针的定义 3.指针的使用 4.易错点 5.思维导图 前言&#xff1a; 之前我们学过变量可以用来存储数据&#xff0c;就像一个盒子里面可以放不同的球一样。 这是一个方便大家理解专业概念的比喻。 在计算机世界里面&#xff0c;数据实…...

【面试必刷TOP101】删除链表的倒数第n个节点 两个链表的第一个公共结点

目录 题目&#xff1a;删除链表的倒数第n个节点_牛客题霸_牛客网 (nowcoder.com) 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 题目&#xff1a;两个链表的第一个公共结点_牛客题霸_牛客网 (nowcoder.com) …...

手刻 Deep Learning -第壹章 -PyTorch教学-激励函数与感知机入门(上)

一、前言 本文接续前篇教学 Pytorch 与线性回归 &#xff0c;本文着重在 Activation Function &#xff08; 中文称 激励函数 &#xff09;&#xff0c;我们会介绍激励函数 &#xff08;也有人称 激活函数&#xff1f; 激发函数&#xff1f; &#xff09; 为什么会有用&#xf…...

物理内存分配

目录 内核物理内存分配接口 内存分配行为&#xff08;物理上&#xff09; 内存分配的行为操作 内存 三个水位线 水线计算 水位线影响内存分配行为 内存分配核心__alloc_pages 释放页 1、内核物理内存分配接口 struct page *alloc_pages(gfp_t gfp, unsigned int ord…...

RFID产线自动化升级改造管理方案

应用背景 在现代制造业中&#xff0c;产线管理是实现高效生产和优质产品的关键环节&#xff0c;产线管理涉及到生产过程的监控、物料管理、工艺控制、质量追溯等多个方面&#xff0c;有效的产线管理可以提高生产效率、降低成本、改善产品质量&#xff0c;并满足市场需求的变化…...

全量数据采集:不同网站的方法与挑战

简介 在当今数字化时代中&#xff0c;有数据就能方便我们做出很多决策。数据的获取与分析已经成为学术研究、商业分析、战略决策以及个人好奇心的关键驱动力。本文将分享不同网站的全量数据采集方法&#xff0c;以及在这一过程中可能会遇到的挑战。 部分全量采集方法 1. 撞店…...

Redis——渐进式遍历和数据库管理命令

介绍 如果使用keys * 这样的操作&#xff0c;将Redis中所有的key都获取到&#xff0c;由于Redis是单线程工作&#xff0c;这个操作本身又要消耗很多时间&#xff0c;那么就会导致Redis服务器阻塞&#xff0c;后续的操作无法正常执行 而渐进式遍历&#xff0c;通过多次执行遍历…...

如何打造可视化警务巡防通信解决方案

近年来&#xff0c;科学技术飞速发展&#xff0c;给予了犯罪分子可乘之机。当面临专业化的犯罪分子、高科技的犯罪手段&#xff0c;传统警务模式似乎不能满足警方打击犯罪的需要&#xff0c;因此当今公安工作迫切需要构建智能化、系统化、信息化的警务通信管理模式。 警务人员…...

Docker 离线安装指南

参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性&#xff0c;不同版本的Docker对内核版本有不同要求。例如&#xff0c;Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本&#xff0c;Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

java_网络服务相关_gateway_nacos_feign区别联系

1. spring-cloud-starter-gateway 作用&#xff1a;作为微服务架构的网关&#xff0c;统一入口&#xff0c;处理所有外部请求。 核心能力&#xff1a; 路由转发&#xff08;基于路径、服务名等&#xff09;过滤器&#xff08;鉴权、限流、日志、Header 处理&#xff09;支持负…...

Opencv中的addweighted函数

一.addweighted函数作用 addweighted&#xff08;&#xff09;是OpenCV库中用于图像处理的函数&#xff0c;主要功能是将两个输入图像&#xff08;尺寸和类型相同&#xff09;按照指定的权重进行加权叠加&#xff08;图像融合&#xff09;&#xff0c;并添加一个标量值&#x…...

sqlserver 根据指定字符 解析拼接字符串

DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

OPENCV形态学基础之二腐蚀

一.腐蚀的原理 (图1) 数学表达式&#xff1a;dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一&#xff0c;腐蚀跟膨胀属于反向操作&#xff0c;膨胀是把图像图像变大&#xff0c;而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

C/C++ 中附加包含目录、附加库目录与附加依赖项详解

在 C/C 编程的编译和链接过程中&#xff0c;附加包含目录、附加库目录和附加依赖项是三个至关重要的设置&#xff0c;它们相互配合&#xff0c;确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中&#xff0c;这些概念容易让人混淆&#xff0c;但深入理解它们的作用和联…...

Webpack性能优化:构建速度与体积优化策略

一、构建速度优化 1、​​升级Webpack和Node.js​​ ​​优化效果​​&#xff1a;Webpack 4比Webpack 3构建时间降低60%-98%。​​原因​​&#xff1a; V8引擎优化&#xff08;for of替代forEach、Map/Set替代Object&#xff09;。默认使用更快的md4哈希算法。AST直接从Loa…...

省略号和可变参数模板

本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...

STM32---外部32.768K晶振(LSE)无法起振问题

晶振是否起振主要就检查两个1、晶振与MCU是否兼容&#xff1b;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容&#xff08;CL&#xff09;与匹配电容&#xff08;CL1、CL2&#xff09;的关系 2. 如何选择 CL1 和 CL…...