揭秘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接口,并现实WebApplicationInitializer的onStartup方法,在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及子类),然后将其作为参数传给SpringServletContainerInitializer的onStartup方法使用。我们自定义的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||undertowjar 包依赖提供的方法,来创建 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||undertow 的 ServletWebServerFactory, 用这个 factory 对象就能创建对应的 web 容器。
而在 getWebServerFactory() 方法内是按类型从 SpringContext 中获取 ServletWebServerFactory 类型的 bean。
所以只要 SpringContext 注入什么容器的 ServletWebServerFactory,springboot 就会启动什么容器。
- 什么地方注入
ServletWebServerFactory呢?
在 ServletWebServerFactoryConfiguration 这个配置类,将tomcat||jetty||undertow的 ServletWebServerFactory 注入进 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容器(servlet 容器),不管请求是访问静态资源HTML、JSP还是java接口,对tomcat而言,都是通过servlet访问: 访问静态资源,tomcat 会交由一个叫做DefaultServlet的类来处…...
分类散点图 stripplot() 加辅助线axhline() 多图合一
分类散点图 stripplot 加辅助线axhline 多图合一 效果图代码 画图没有什么可说的,直接上图 效果图 代码 # 绘制图, 查看是否数值在阈值上 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 中,我们对流式计算、数据订阅功能都进行了再升级,帮助用户极大简化了数据架构的复杂程度,降低整体运维成本。TDengine 提供的类似消息队列产品的数据订阅、消费接口,本质上是为了帮助应用实时获取写入 TDengine 的…...
reg与wire的用法,证明reg可以在右边,wire型在左边,来作组合逻辑处理。
reg与wire的用法,证明reg可以在右边,wire型在左边,来作组合逻辑处理。 1,RTL2,生成的原理图 1,RTL 参考文献: 1,verilog 中 wire 和reg 的使用 2,解决一个assign问题&…...
Studio One6.2简体中文免费最新版本宿主软件
对于一些有创作需求的朋友来说,为自己写的歌制作伴奏是很平常的。今天要和大家分享的就是自己写的歌怎么做伴奏,自己做伴奏的软件有哪些。Studio One是宿主软件界的一个后起之秀,推出的时间不久,但是受到了大量音乐制作人的推崇。…...
算法刷题 week2
目录 week21. 二维数组中的查找题目题解(单调性扫描) O(nm) 2.替换空格题目题解(线性扫描) O(n)(双指针扫描) O(n) 3.从尾到头打印链表题目题解(遍历链表) O(n) week2 1. 二维数组中的查找 题目 题解 (单调性扫描) O(nm) 核心在于发现每个子矩阵右上角的数的性质࿱…...
子网的划分
强化计算机网络发现王道没有这一块的内容,导致做题稀里糊涂。于是个人调研补充。 子网划分是将一个大型IP网络划分成更小的子网,以实现更有效的网络管理和资源分配。 原因: 提高网络性能:子网划分可以减少广播域的大小ÿ…...
Docker安装与卸载
Docker安装与卸载 安装 yum install -y yum-utils \device-mapper-persistent-data \lvm2 --skip-broken更新本地镜像源 打开终端或 SSH 连接到 Rocky Linux 的服务器。 进入 /etc/yum.repos.d/ 目录,该目录包含 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使用
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 前言 提示:这里可以添加本文要记录的大概内容: 例如:…...
python经典百题之求前!的和
题目:求12!3!…20!的和 方法一: 使用for循环和阶乘函数计算每项的值,再将每项的值累加起来。 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 初识指针
目录 前言: 1.内存地址 2.指针的定义 3.指针的使用 4.易错点 5.思维导图 前言: 之前我们学过变量可以用来存储数据,就像一个盒子里面可以放不同的球一样。 这是一个方便大家理解专业概念的比喻。 在计算机世界里面,数据实…...
【面试必刷TOP101】删除链表的倒数第n个节点 两个链表的第一个公共结点
目录 题目:删除链表的倒数第n个节点_牛客题霸_牛客网 (nowcoder.com) 题目的接口: 解题思路: 代码: 过啦!!! 题目:两个链表的第一个公共结点_牛客题霸_牛客网 (nowcoder.com) …...
手刻 Deep Learning -第壹章 -PyTorch教学-激励函数与感知机入门(上)
一、前言 本文接续前篇教学 Pytorch 与线性回归 ,本文着重在 Activation Function ( 中文称 激励函数 ),我们会介绍激励函数 (也有人称 激活函数? 激发函数? ) 为什么会有用…...
物理内存分配
目录 内核物理内存分配接口 内存分配行为(物理上) 内存分配的行为操作 内存 三个水位线 水线计算 水位线影响内存分配行为 内存分配核心__alloc_pages 释放页 1、内核物理内存分配接口 struct page *alloc_pages(gfp_t gfp, unsigned int ord…...
RFID产线自动化升级改造管理方案
应用背景 在现代制造业中,产线管理是实现高效生产和优质产品的关键环节,产线管理涉及到生产过程的监控、物料管理、工艺控制、质量追溯等多个方面,有效的产线管理可以提高生产效率、降低成本、改善产品质量,并满足市场需求的变化…...
全量数据采集:不同网站的方法与挑战
简介 在当今数字化时代中,有数据就能方便我们做出很多决策。数据的获取与分析已经成为学术研究、商业分析、战略决策以及个人好奇心的关键驱动力。本文将分享不同网站的全量数据采集方法,以及在这一过程中可能会遇到的挑战。 部分全量采集方法 1. 撞店…...
Redis——渐进式遍历和数据库管理命令
介绍 如果使用keys * 这样的操作,将Redis中所有的key都获取到,由于Redis是单线程工作,这个操作本身又要消耗很多时间,那么就会导致Redis服务器阻塞,后续的操作无法正常执行 而渐进式遍历,通过多次执行遍历…...
如何打造可视化警务巡防通信解决方案
近年来,科学技术飞速发展,给予了犯罪分子可乘之机。当面临专业化的犯罪分子、高科技的犯罪手段,传统警务模式似乎不能满足警方打击犯罪的需要,因此当今公安工作迫切需要构建智能化、系统化、信息化的警务通信管理模式。 警务人员…...
从汽车中控到工厂流水线:HMI触摸屏的7大核心功能,你真的都用上了吗?
从汽车中控到工厂流水线:HMI触摸屏的7大核心功能深度解析 在汽车制造车间里,工程师小王正盯着流水线上的HMI触摸屏,屏幕上跳动的参数曲线让他迅速定位了焊接机器人的异常状态。这个看似简单的交互界面,背后却整合了数据采集、逻辑…...
BBDown终极指南:3分钟学会B站视频下载的完整教程
BBDown终极指南:3分钟学会B站视频下载的完整教程 【免费下载链接】BBDown Bilibili Downloader. 一个命令行式哔哩哔哩下载器. 项目地址: https://gitcode.com/gh_mirrors/bb/BBDown BBDown是一款免费开源的Bilibili视频下载工具,它能让你轻松将B…...
Windows Defender 四层防护解除技术深度解析:defender-control 开源项目完全指南
Windows Defender 四层防护解除技术深度解析:defender-control 开源项目完全指南 【免费下载链接】defender-control An open-source windows defender manager. Now you can disable windows defender permanently. 项目地址: https://gitcode.com/gh_mirrors/d…...
别再只盯着SENet了!手把手教你用PyTorch复现SKNet和CBAM(附完整代码)
深度学习注意力机制实战:从SKNet到CBAM的PyTorch实现精要 在计算机视觉领域,注意力机制已经成为提升模型性能的关键技术。不同于传统的卷积神经网络平等对待所有特征通道,注意力机制让模型学会"关注"最重要的信息。本文将带您深入…...
Acunetix安装后第一步做什么?新手避坑指南与Pikachu靶场扫描实战
Acunetix安装后第一步做什么?新手避坑指南与Pikachu靶场扫描实战 当你第一次打开Acunetix时,面对满屏的专业术语和复杂功能,很容易陷入"我该从哪里开始?"的困惑。这篇文章将带你跨过从安装完成到首次成功扫描的关键一步…...
暗黑2自动化终极指南:告别手动刷怪,用Botty轻松获取稀有装备
暗黑2自动化终极指南:告别手动刷怪,用Botty轻松获取稀有装备 【免费下载链接】botty D2R Pixel Bot 项目地址: https://gitcode.com/gh_mirrors/bo/botty 还在为暗黑2中重复刷怪感到疲惫吗?Botty是一款专为《暗黑破坏神2:重…...
数字视频录像机(DVR):从技术革新到行业应用的全面解析
目录 一、技术演进 1. 基础架构的革新 2. 编码技术的迭代 3. 智能化与云化融合 二、市场格局 1. 全球市场规模与区域分布 2. 竞争格局:头部企业技术壁垒深厚 3. 替代风险与差异化竞争 三、应用场景 1. 传统安防领域 2. 工业与能源领域 3. 智能家居与消费级市场 …...
Windows Cleaner:拯救C盘空间的桌面医生
Windows Cleaner:拯救C盘空间的桌面医生 【免费下载链接】WindowsCleaner Windows Cleaner——专治C盘爆红及各种不服! 项目地址: https://gitcode.com/gh_mirrors/wi/WindowsCleaner 不知道你有没有过这样的经历:打开电脑准备开始一天…...
别再只怪网络了!深入Gradle依赖树,揪出导致kotlin-stdlib-jdk8:1.3.72解析失败的真凶
深入Gradle依赖树:破解kotlin-stdlib-jdk8解析失败的底层逻辑 遇到Gradle构建失败时,许多开发者第一反应是网络问题,但真正的问题往往隐藏在复杂的依赖关系中。本文将带你深入Gradle依赖解析机制,揭示那些被忽视的关键细节。 1. 依…...
SAP ABAP开发避坑:用BAPI_OUTB_DELIVERY_CONFIRM_DEC发货过账后,为什么VL09冲销不了?
SAP ABAP开发实战:BAPI发货过账后VL09冲销失败的深度解析与修复方案 在SAP SD/MM模块的日常开发中,交货单的发货过账和冲销操作是供应链管理的关键环节。许多ABAP开发者在实现自定义发货过账逻辑时,会遇到一个令人头疼的问题:使用…...
