SpringBoot中的Tomcat是如何启动的

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

添加如上 Web 的依赖,Spring Boot 就帮我们内置了 Servlet 容器,默认使用的是 Tomcat,同样也支持修改,比如可以使用 jetty、Undertow 等。

因为内置了启动容器,应用程序可以直接通过 Maven 命令将项目编译成可执行的 jar 包,通过 java -jar 命令直接启动,不需要再像以前一样,打包成 War 包,然后部署在 Tomcat 中。

那么:你知道内置的 Tomcat 在 Spring Boot 中是怎么启动的吗?

从启动入口分析

如果不知道从哪开始,那么至少应该知道 Spring Boot 其实运行的就是一个 main 方法,

本文环境:Spring Boot:2.2.2.RELEASE

@SpringBootApplication
public class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);}
}

我们点进这个 SpringApplication.run() 方法:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class[]{primarySource}, args);
}

这列的 run() 方法返回的是 ConfigurableApplicationContext 对象,我们继续跟踪这个 run() 方法:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return (new SpringApplication(primarySources)).run(args);
}

又套了一层,继续点击这个返回的 run() 方法:

public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();stopWatch.start();ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();/** 1、配置属性* 设置系统属性 java.awt.headless,为 true 则启用headless模式* headless模式是应用的一种配置模式,在服务器缺少显示设备、键盘、鼠标等外设的情况下可以使用该模式* 比如我们使用的Linux服务器就是缺少前述的这些设备,但是又需要使用这些设备提供的能力*/configureHeadlessProperty();/** 2、获取监听器,发布应用开始启动事件* 通过SpringFactoriesLoader检索META-INF/spring.factories,* 找到声明的所有SpringApplicationRunListener的实现类并将其实例化,* 之后逐个调用其started()方法,广播SpringBoot要开始执行了*/SpringApplicationRunListeners listeners = getRunListeners(args);/* 发布应用开始启动事件 */listeners.starting();try {/* 3、初始化参数 */ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);/** 4、配置环境,输出banner* 创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile),* 并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法,广播Environment准备完毕。*/ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);configureIgnoreBeanInfo(environment);/* 打印banner,如果在resources目录下创建了我们自己的banner就会进行打印,否则默认使用spring的 */Banner printedBanner = printBanner(environment);/* 5、创建应用上下文 */context = createApplicationContext();/* 通过SpringFactoriesLoader检索META-INF/spring.factories,获取并实例化异常分析器。 */exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);/** 6、预处理上下文* 为ApplicationContext加载environment,之后逐个执行ApplicationContextInitializer的initialize()方法来进一步封装ApplicationContext,* 并调用所有的SpringApplicationRunListener的contextPrepared()方法,【EventPublishingRunListener只提供了一个空的contextPrepared()方法】,* 之后初始化IoC容器,并调用SpringApplicationRunListener的contextLoaded()方法,广播ApplicationContext的IoC加载完成,* 这里就包括通过@EnableAutoConfiguration导入的各种自动配置类。*/prepareContext(context, environment, listeners, applicationArguments,printedBanner);/* 7、刷新上下文 */refreshContext(context);/* 8、再一次刷新上下文,其实是空方法,可能是为了后续扩展。 */afterRefresh(context, applicationArguments);stopWatch.stop();if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}/* 9、发布应用已经启动的事件 */listeners.started(context);/** 遍历所有注册的ApplicationRunner和CommandLineRunner,并执行其run()方法。* 我们可以实现自己的ApplicationRunner或者CommandLineRunner,来对SpringBoot的启动过程进行扩展。*/callRunners(context, applicationArguments);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}try {/* 10、发布应用已经启动完成的监听事件 */listeners.running(context);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, null);throw new IllegalStateException(ex);}return context;
}

如果觉得这个方法看的云里雾里的,那么可以概括为如下几步:

  1. 配置系统属性
  2. 获取监听,发布应用开始启动时间
  3. 初始化输入参数
  4. 配置环境,输出banner
  5. 创建上下文
  6. 预处理上下文
  7. 刷新上下文
  8. 再次刷新上下文
  9. 发布应用已经启动事件
  10. 发布应用启动完成事件

而我们 Tomcat 的启动主要是在第5步创建上下文,以及第 7步刷新上下文实现的。

创建上下文

第5步中,创建上下文主要是调用的 createApplicationContext() 方法:

protected ConfigurableApplicationContext createApplicationContext() {/** 1. 根据Web应用类型,获取对应的ApplicationContext子类 **/Class<?> contextClass = this.applicationContextClass;if (contextClass == null) {try {switch(this.webApplicationType) {case SERVLET:contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");break;case REACTIVE:contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");break;default:contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");}} catch (ClassNotFoundException var3) {throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);}}/** 2. 实例化子类 **/return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
}

代码中主要就是通过 switch 语句,根据 webApplicationType 的类型来创建不同的 ApplicationContext:

  • SERVLET:Web类型,实例化 AnnotationConfigServletWebServerApplicationContext
  • REACTIVE:响应式Web类型,实例化 AnnotationConfigReactiveWebServerApplicationContext
  • default:非Web类型,实例化 AnnotationConfigApplicationContext

因为我们的应用是 Web 类型,所以实例化的是 AnnotationConfigServletWebServerApplicationContext,如下是该类的关系图(由Diagram截图):

我们在上图的底部触发,可以看到 AnnotationConfigServletWebServerApplicationContext > ServletWebServerApplicationContext > … AbstractApplicationContext(>表示继承),总之,最终继承到了 AbstractApplicationContext,这个类是 ApplicationContext 的抽象实现类,该抽象类实现应用上下文的一些具体操作。

至此,并没有看到 Tomcat 的相关代码,其实这一步主要就是「创建上下文」,拿到「上下文」之后需要传递给「刷新上下文」,交由刷新上下文创建 Web 服务。

刷新上下文

第7步中,刷新上下文时调用的 refreshContext(context) 方法,其中 context 就是第5步创建的上下文,方法如下:

private void refreshContext(ConfigurableApplicationContext context) {refresh(context);if (this.registerShutdownHook) {try {context.registerShutdownHook();}catch (AccessControlException ex) {// Not allowed in some environments.}}
}protected void refresh(ApplicationContext applicationContext) {Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);((AbstractApplicationContext) applicationContext).refresh();
}

refreshContext() 方法传递的 context,经由 refresh() 方法强转成父类 AbstractApplicationContext,具体调用过程如下:

public void refresh() throws BeansException, IllegalStateException {synchronized(this.startupShutdownMonitor) {this.prepareRefresh();ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();this.prepareBeanFactory(beanFactory);try {this.postProcessBeanFactory(beanFactory);this.invokeBeanFactoryPostProcessors(beanFactory);this.registerBeanPostProcessors(beanFactory);this.initMessageSource();this.initApplicationEventMulticaster();/** 主要关系 onRefresh() 方法 ------------- **/this.onRefresh();this.registerListeners();this.finishBeanFactoryInitialization(beanFactory);this.finishRefresh();} catch (BeansException var9) {if (this.logger.isWarnEnabled()) {this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);}this.destroyBeans();this.cancelRefresh(var9);throw var9;} finally {this.resetCommonCaches();}}
}

在这个方法中我们主要关心 onRefresh() 方法,onRefresh() 方法是调用其子类实现的,也就是 ServletWebServerApplicationContext,

如下是子类的 onRefresh() 方法:

protected void onRefresh() {super.onRefresh();try {this.createWebServer();} catch (Throwable var2) {throw new ApplicationContextException("Unable to start web server", var2);}
}private void createWebServer() {WebServer webServer = this.webServer;ServletContext servletContext = this.getServletContext();if (webServer == null && servletContext == null) {/** 得到Servlet工厂 **/ServletWebServerFactory factory = this.getWebServerFactory();this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});} else if (servletContext != null) {try {this.getSelfInitializer().onStartup(servletContext);} catch (ServletException var4) {throw new ApplicationContextException("Cannot initialize servlet context", var4);}}this.initPropertySources();
}

其中 createWebServer() 方法是用来启动web服务的,但是还没有真正启动 Tomcat,只是通过ServletWebServerFactory 创建了一个 WebServer,我们继续来看这个 ServletWebServerFactory:

ServletWebServerFactory 有4个实现类,其中我们最常用的是 TomcatServletWebServerFactory和JettyServletWebServerFactory,而默认的 Web 环境就是 TomcatServletWebServerFactory。

而到这总算是看到 Tomcat 相关的字眼了。

来看一下 TomcatServletWebServerFactory 的 getWebServer() 方法:

public WebServer getWebServer(ServletContextInitializer... initializers) {if (this.disableMBeanRegistry) {Registry.disableRegistry();}/** 1、创建Tomcat实例 **/Tomcat tomcat = new Tomcat();File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");tomcat.setBaseDir(baseDir.getAbsolutePath());Connector connector = new Connector(this.protocol);connector.setThrowOnFailure(true);tomcat.getService().addConnector(connector);this.customizeConnector(connector);/** 2、给创建好的tomcat设置连接器connector **/tomcat.setConnector(connector);/** 设置不自动部署 **/tomcat.getHost().setAutoDeploy(false);/** 3、配置Tomcat容器引擎 **/this.configureEngine(tomcat.getEngine());Iterator var5 = this.additionalTomcatConnectors.iterator();while(var5.hasNext()) {Connector additionalConnector = (Connector)var5.next();tomcat.getService().addConnector(additionalConnector);}/*** 准备Tomcat的StandardContext,并添加到Tomcat中,同时把initializers 注册到类型为* TomcatStarter的ServletContainerInitializer中**/this.prepareContext(tomcat.getHost(), initializers);/** 将创建好的Tomcat包装成WebServer返回**/return this.getTomcatWebServer(tomcat);
}public Engine getEngine() {Service service = this.getServer().findServices()[0];if (service.getContainer() != null) {return service.getContainer();} else {Engine engine = new StandardEngine();engine.setName("Tomcat");engine.setDefaultHost(this.hostname);engine.setRealm(this.createDefaultRealm());service.setContainer(engine);return engine;}
}

getWebServer() 这个方法创建了 Tomcat 对象,并且做了两件重要的事情:

  1. 把连接器 Connector 对象添加到 Tomcat 中;
  2. 配置容器引擎,configureEngine(tomcat.getEngine());

首先说一下这个 Connector 连接器,Tomcat 有两个核心功能:

  1. 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
  2. 加载和管理 Servlet,以及具体处理 Request 请求。

针对这两个功能,Tomcat 设计了两个核心组件来分别完成这两件事,即:连接器(Connector)和容器(Container)。

整个过程大致就是:Connector 连接器接收连接请求,创建Request和Response对象用于和请求端交换数据,然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connector将响应返回给客户端。

这里面提到了 Engine,这个是 Tomcat 容器里的顶级容器(Container),我们可以通过 Container 类查看其他的子容器:Engine、Host、Context、Wrapper

4者的关系是:Engine 是最高级别的容器,Engine 子容器是 Host,Host 的子容器是 Context,Context 子容器是 Wrapper,所以这4个容器的关系就是父子关系,即:Wrapper > Context > Host > Engine (>表示继承)

至此我们了解了 Engine 这个就是个容器,然后我们再看一下这个 configureEngine(tomcat.getEngine()) 具体干了啥:

private void configureEngine(Engine engine) {engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay);Iterator var2 = this.engineValves.iterator();while(var2.hasNext()) {Valve valve = (Valve)var2.next();engine.getPipeline().addValve(valve);}
}

其中 engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay) 是指定背景线程的执行间隔,例如背景线程会在每隔多长时间后判断session是否失效之类。

再回到 getWebServer() 方法,最终 getWebServer() 方法返回了 TomcatWebServer。

return this.getTomcatWebServer(tomcat);

通过 getTomcatWebServer() 方法,继续下沉:

/*** 构造函数实例化 TomcatWebServer**/
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {this.monitor = new Object();this.serviceConnectors = new HashMap();Assert.notNull(tomcat, "Tomcat Server must not be null");this.tomcat = tomcat;this.autoStart = autoStart;this.initialize();
}private void initialize() throws WebServerException {/** 我们在启动 Spring Boot 时经常看到打印这句话 **/logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false));synchronized(this.monitor) {try {this.addInstanceIdToEngineName();Context context = this.findContext();context.addLifecycleListener((event) -> {if (context.equals(event.getSource()) && "start".equals(event.getType())) {this.removeServiceConnectors();}});/** 启动 tomcat **/this.tomcat.start();this.rethrowDeferredStartupExceptions();try {ContextBindings.bindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());} catch (NamingException var5) {}this.startDaemonAwaitThread();} catch (Exception var6) {this.stopSilently();this.destroySilently();throw new WebServerException("Unable to start embedded Tomcat", var6);}}
}

至此,Tomcat 启动过程就很清晰了,总结一下。

总结

SpringBoot的启动主要是通过实例化SpringApplication来启动的,启动过程主要做了如下几件事情:

  1. 配置系统属性
  2. 获取监听,发布应用开始启动时间
  3. 初始化输入参数
  4. 配置环境,输出banner
  5. 创建上下文
  6. 预处理上下文
  7. 刷新上下文
  8. 再次刷新上下文
  9. 发布应用已经启动事件
  10. 发布应用启动完成事件

而启动 Tomcat 是在第7步 刷新上下文 这一步。

从整个流转过程中我们知道了 Tomcat 的启动主要是实例化两个组件:Connector、Container。

  • Spring Boot 创建 Tomcat 时,会先创建一个根上下文,将 WebApplicationContext 传给 Tomcat;

  • 启动 Web 容器,需要调用 getWebserver(),因为默认的 Web 环境就是 TomcatServletWebServerFactory,所以会创建 Tomcat 的 Webserver,这里会把根上下文作为参数给 TomcatServletWebServerFactory 的 getWebServer();

  • 启动 Tomcat,调用 Tomcat 中 Host、Engine 的启动方法。

博客园:https://www.cnblogs.com/niceyoo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/413875.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IDEA社区版(Community)和付费版(UItimate)的区别

比对类型Ultimate(终极版,付费)Community(社区版,免费)语言支持JavaJavaGroovyGroovyKotlinKotlinScala&#xff08;通过插件&#xff09;Scala&#xff08;通过插件&#xff09;Python 和 Jython&#xff08;通过插件&#xff09;Python 和 Jython&#xff08;通过插件&#x…

从使用传统Web框架到切换到Spring Boot后的总结

1、前言 其实我接触 Spring Boot 的时间并不长&#xff0c;所以还算一个初学者&#xff0c;这篇文章也算是我对 Spring Boot 学习以及使用过程中的复盘&#xff0c;如果文章出现描述错误或表达不清晰的地方&#xff0c;欢迎大家在评论区留言互动。 没想到 Spring Boot 这两年…

前端学习(2859):简单秒杀系统学习之前端界面布局

<html><head><meta charset"utf-8"><link rel"stylesheet" type"text/css" href"miao.min.css" charset"utf-8"><title>秒杀系统</title></head><body><div class"…

Spring Boot 项目瘦身指南,瘦到不可思议!129M->1.3M

之前在 从使用传统Web框架到切换到Spring Boot后的总结 中提到关于 Spring Boot 编译打包&#xff0c;Spring Boot 应用程序不用额外部署到外部容器中&#xff0c;可以直接通过 Maven 命令将项目编译成可执行的 jar 包&#xff0c;然后通过 java -jar 命令启动即可&#xff0c;…

基于Docker搭建RabbitMQ(多图)

1、一点废话&#xff08;可直接跳转至标题2&#xff09; 通常在拉取镜像之前&#xff0c;除了通过命令执行 docker search xxx 之外&#xff0c;我们还可以通过 Docker 镜像仓库查询指定的镜像。 如下是 rabbitmq 镜像的搜索结果&#xff1a; 一般拉取下载数 stars 数相对比…

基于Docker搭建Redis集群(主从集群)

最近陆陆续续有不少园友加我好友咨询 redis 集群搭建的问题&#xff0c;我觉得之前写的这篇 《基于Docker的Redis集群搭建》 文章一定是有问题了&#xff0c;所以我花了几分钟浏览之前的文章总结了下面几个问题&#xff1a; redis 数量太少&#xff0c;只创建了 3 个实例&…

洛达AirPods鉴别检测工具AB153x_UT,支持1562a 1562f

推荐阅读&#xff1a;AB1562_UT软件分辨真假洛达1562A&#xff0c;洛达1562a怎么鉴别&#xff1f; 洛达 1536u 1562a 1562f 1562m 检测工具&#xff0c; 可根据协议检测出实际使用芯片&#xff0c;免拆机检测华强北airpods使用芯片 一、使用方法 蓝牙连接安卓手机后&#x…

AB1562_UT软件分辨真假洛达1562A,洛达1562a怎么鉴别?

一、使用方法 1、下载并安装 AB1562_UT&#xff0c;下载链接 → https://t.1yb.co/kQIC 2、如上图所示&#xff0c;点击顶部的「选择设备」&#xff0c;弹出窗口中选择你的耳机。 3、点击「连接耳机」 4、显示耳机信息 更多测试方法请点击该链接&#xff1a;https://www.cnblo…

MacOS中安装Consul(启动及关闭)

PS&#xff1a;非 brew 安装方式。 官网下载链接&#xff1a;https://www.consul.io/downloads.html 懒得去官网下载&#xff0c;或者官网下载速度慢的可以试一下下方蓝奏云链接。 蓝奏云v1.9.0下载&#xff1a;https://niceyoo.lanzous.com/iUBgwjyd6xa 1、解压并放置指定…

华强北AirPods洛达1562A固件升级教程,带空间音频(艾创力+东莞豪锐)

Hello 大家好&#xff0c;这里是 niceyoo&#xff0c;我是你们的小源。 今天分享的是洛达1562A的固件升级教程。其实一直不想写这篇&#xff0c;主要是怕出现问题&#xff0c;​怎么讲呢&#xff1f;本身刷机这件事是有风险的&#xff0c;就跟手机系统刷机一样&#xff0c;一旦…

集群部署中解决定时任务重复执行的问题-redis分布式锁应用

背景描述 有小伙伴私信我&#xff0c;关于存在定时任务的项目在集群环境下部署如何解决重复执行的问题&#xff0c;PS&#xff1a;定时任务没有单独拆分。 概述&#xff1a;之前的项目都是单机器部署&#xff0c;所以定时任务不会重复消费&#xff0c;只会执行一次。而在集群…

洛达检测软件AB1562UT_1.4.4新版本下载,适用洛达全系列

络达芯片检测调试工具&#xff0c;2020年12月发布的版本&#xff0c;仅支持Android系统手机&#xff1b; 适用于AB1536U,1562A,1562M,1562F。不是络达芯片的不能用&#xff01; 真洛达推荐链接&#xff1a;https://item.taobao.com/item.htm?ftt&id641479139773 一、软件…

支付宝新版SDK-PC扫码支付-手机浏览器H5支付

一、前言 支付宝支付—沙箱环境使用 支付宝新版SDK-PC端扫码支付 手机浏览器H5支付「本文」 PC端扫码支付&#xff0c;其实就是就是 电脑网站支付&#xff0c;本文基于支付宝沙箱环境&#xff0c;不了解的可以看一下上边的链接。 PS&#xff1a;本文是基于支付宝新版 SDK …

华强北耳机修改序列号|支持中英文|自定义修改|傻瓜式一键修改序列号~

软件下载链接>>>&#xff1a;https://t.1yb.co/kQIu 你还在纠结买到的华强北耳机序列号查询不到吗&#xff1f; 你还在纠结因为刷机导致的序列号与充电仓序列号不一致吗&#xff1f; 你还在纠结自己的序列号不够有面吗… 先看一张效果图&#xff1a; 今天分享一下…

CentOS离线安装gcc环境(附安装包+图文并茂)

1、关于gcc linux内核本身不依赖gcc&#xff0c;gcc只是一个编译软件&#xff0c;是在kernel的源码变成可执行文件的时候起作用&#xff0c;真正使用起来就没有什么关系。 查看 gcc 版本 gcc -v 如果没有则显示&#xff1a; 2、安装步骤 2.1、下载gcc安装包 gcc下载地址&…