思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
有过SpringBoot
相关开发经历的都知道,SpringBoot
支持内嵌容器,其支持内嵌Tomcat、Jetty
等容器。此外,SpringBoot
应用也可在外部的服务器进行部署。换言之,在部署SpringBoot
应用时可以进行灵活的选择,既可以选择使用SpringBoot
内嵌的Tomcat
,也可使用外置Tomcat
服务器。
那这两种不同部署方式在启动SpringBoot
应用时的差异你有过了解吗?换言之,两种情况下容器又是在何时启动的呢? SpringBoot
应用又是如何启动呢?
不清楚也别着急,本文会详细拆解两种不同部署方式在启动SpringBoot
应用时的差异。
前言
在面试官:SpringBoot的启动流程熟悉吗?手写一个怎么样 中,我们通过一个不到50
行的代码,对SpringBoot
中的启动逻辑进行了模拟
。同时,我们在其中也提到,SpringBoot
启动逻辑中的核心在于:自动装配、容器刷新、服务器启动三部分。
而本文重点在于分析其中的服务器启动,再具体一点,我们要分析通过SpringBoot
内嵌服务器部署应用和外置服务器部署SpringBoot
应用间的差异。
内嵌容器的启动过程
在开始分析容器之前,不妨思考一个问题,如果要分析SpringBoot
内嵌容器的启动该从何入手呢? 我想你肯定会脱口而出,那肯定是SpringApplication
中的run
方法啦!
接下来,就让我们看看SpringApplication
的run
方法中究竟是如何来完成容器的启动的。SpringApplication
中的run
方法逻辑如下:
public ConfigurableApplicationContext run(String... args) {
// .......省略其他无关代码
listeners.starting();
try {
// 构建一个应用参数解析器
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 加载系统的属性配置信息
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 用于控制是否忽略BeanInfo的配置
configureIgnoreBeanInfo(environment);
// 打印banner信息
Banner printedBanner = printBanner(environment);
// 创建一个容器,类型为ConfigurableApplicationContext
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 容器准备工作 (可暂时忽略)
prepareContext(context, environment, listeners,
// 解析传入参数信息
applicationArguments, printedBanner);
// 容器刷新 (重点关注)
refreshContext(context);
afterRefresh(context, applicationArguments);
}
// .......省略其他无关代码
return context;
}
进一步,上述方法可抽象成下图所示内容。
可以看到,在refreshContext
之前的逻辑,大多在配置一些参数、环境
信息。而像启动容器
这样关键的信息基本全部在refreshContext
方法中进行实现。
接下来,我们就进入到refreshContext
方法内部,来看看其内部究竟是如何来完成Tomcat
容器启动的!
private void refreshContext(ConfigurableApplicationContext context) {
// ... 省略其他无关代码
refresh((ApplicationContext) context);
}
事实上,refreshContext
最终会调到ConfigurableApplicationContext
的refresh
方法,来完成"刷新"操作,此处我们就不一步步debug
了,直接给出最终调用的逻辑。
ServletWebServerApplicationContext
#refresh
public final void refresh() throws BeansException, IllegalStateException {
try {
// 此处会执行父类AbstractApplicationContext中的逻辑
super.refresh();
}
catch (RuntimeException ex) {
构建容器
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw ex;
}
}
(注:SprignBoot
在初始化容器时,会侦测当前环境下是否有DispatcherServlet
的存在,如果存在则会构建一个ServletWebServerApplicationContext
的上下文环境
,这部分逻辑可参考方法run
中的createApplicationContext()
方法。
处所执行逻辑如下图所示,其本质就是一个
Spring
容器刷新的那一套操作。重点关注其中的onRefresh
方法。因为,在Spring
框架中,onRefresh
方法是 ApplicationListener
接口的一部分,用于监听应用上下文的刷新事件。当应用上下文刷新时(例如,当ApplicationContext
被创建并初始化时),Spring
容器会调用onRefresh
方法。这个方法允许开发者在容器刷新时执行一些特定的自定义逻辑。
进一步,内嵌Tomcat
容器的启动就是在起初完成的。接下来,我们便看看ServletWebServerApplicationContext
中的onRefresh
方法。
protected void onRefresh() {
// 执行父类刷新操作
super.onRefresh();
// 创建web容器
createWebServer();
// ...省略异常捕获
}
绕了这么一大圈,终于在ServletWebServerApplicationContext
中的onRefresh
方法看到了服务器创建的相关操作了。
胜利就在眼前了,马上就能明白SpringBoot
中内嵌服务器的启动逻辑啦!其中,createWebServer
中的逻辑如下:
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 构建一个ServletWebServerFactory
ServletWebServerFactory factory = getWebServerFactory();
// 通过工程构建一个webServer
this.webServer = factory.getWebServer(getSelfInitializer());
// ... 省略无关代码
}
}
处逻辑会直接委托给
TomcatServletWebServerFactory
的getWebServer
来完成,大致逻辑如下:
public WebServer getWebServer(ServletContextInitializer... initializers) {
// 实例化一个Tomcat服务
Tomcat tomcat = new Tomcat();
// ... 省略大量配置过程
// Tomcat启动会在此完成
return getTomcatWebServer(tomcat);
}
至此,此时我们终于分析清楚了SpringBoot
中内嵌服务器的启动逻辑了。其实SpringBoot
内嵌Tomcat
的启动本质就是:实例化一个Tomcat
实例,然后调用该实例的start
方法。 总结来看无非如下两行代码:
// 实例化
Tomcat tomcat = new Tomcat();
// 启动
tomcat.start();
Tomcat
实例;Tomcat
服务。可以看到内嵌服务器的逻辑总结起来其实很容易的。但我们的分析过程容易吗?当然不容易! 我们先是从run
方法入手,然后又分析到了Spring
容器刷新,接着又分析了容器
扩展点的onRefresh
方法,经历了无数曲折才找到服务器启动的相关逻辑。
那如果我直接告诉你:“我们关注ServletWebServerApplicationContext
中的onRefresh
的方法,因为这里会完成容器的创建,而其创建过程实例化一个Tomcat
实例,然后启动”
这样的分析好吗?当然这样不好啦。这样就算你看了无数解析最终也只是记住一个结论,换个场景,换个问题便会束手无策。这样的学习不过是在自己欺骗自己,看似学了很多,能力却什么增长。
笔者希望看到的是你通过阅读笔者的文章后具有举一反三的能力,即使不能举一反三,下次遇到相似场景可以做到知识的迁移也是可以的!
因为只有这样才表明你真正将文章的知识转变为自己的,这是笔者更愿意看到的。当然,这个过程可能是艰难的,笔者也在不断努力提升自己的表达,争取将复杂的技术简单化,为了实现这一目标,笔者也在不断努力!
说了这么多,让我们回到正题。上述我们分析了SpringBoot
中内嵌服务器的启动逻辑。接下来,让我们看看外置的Tomcat
服务器,如何来启动一个SpringBoot
应用。
外嵌服务器的启动逻辑
在分析外嵌Tomcat
部署SpringBoot
应用之前,先来看一段flowable
中启动类的源码:
@SpringBootApplication
public class FlowableUiApplication
extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(FlowableUiApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(FlowableUiApplication.class);
}
}
(其中,flowable
会将应用打成war
包发布,而不是打成jar
。)
可以注意到,上述代码中用到了一个SpringBootServletInitializer
,这个类之前你可能没接触过,我们先来看下类注释信息:
上述红色方框中的大致意思是说:当项目打war
包的时候才需要SpringBootServletInitializer
这个类。此外,注释还说,实现当前类要重写configure
方法,并且调用SpringApplicationBuilder.sources
方法,同时将@Configuration
类传入。所以flowable
中才传下上述所示的代码。
明白了SpringBootServletInitializer
作用后,再看上述代码,不知道是否会有这样的疑问:就是Tomcat
中是如何来启动打成war
包的SpringBoot
应用呢?
众所周知,将应用程序打包成一个war
包并部署到Tomcat
后,应用的生命周期完全依赖于Tomcat
。而如果我们的项目是一个SpringBoot
项目,我们需要执行SpringBoot
的启动逻辑,来初始化SpringBoot
项目所需的一些组件。
而SpringBoot
的启动逻辑对于普通的jar
包项目来说很简单,只需执行启动类的main
方法中SpringApplication.run()
即可。
但是当项目部署到Tomcat
后,问题就开始变得复杂起来了。因为Tomcat
自身也是通过main
方法启动的,而且SpringBoot
应用程序也有自己的main
方法。我们知道,一个应用程序中通常只能有一个main
方法来启动,此时这两者之间的冲突是无法解决的。
不妨思考一个问题,如果你是设计者你该如何处理这个问题呢?其实这个解决方法有很多,例如可以通过反射机制来执行。而此处使用的回调
。 换句话来说,Tomcat
在启动时会加载某些接口,并在某个合适时机执行接口中方法,因此只需实现某些接口,就能将启动类逻辑加载到Tomcat
的启动逻辑中。 事实上,这样的思想在Spring
中屡见不鲜。
(注:后续的回调逻辑可能会有点绕,所以此处我们先给出一幅调用的逻辑关系图,避免读者在阅读时感到懵圈
)
sequenceDiagram
Tomcat ->> Tomcat : start
Tomcat ->> StandardContext: startInternal
StandardContext ->> StandardContext : 循环遍历ServletContainerInitializer
StandardContext ->> ServletContainerInitializer: onStartup
ServletContainerInitializer ->> ServletContainerInitializer : 遍历所有的WebApplicationInitializer
ServletContainerInitializer ->> WebApplicationInitializer:onStartup
此处我们就不一步步debug
分析了,直接给出Tomcat
启动时调用的相关逻辑。 在Tomcat
启动过程中会调用到StandardContext
中启动的生命周期startInternal
方法。其逻辑如下:
protected synchronized void startInternal()
throws LifecycleException {
// ....省略无关代码
for (Map.Entry> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
// ... 省略无关代码
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
可以看到,在 SpringServletContainerInitializer # onStartup
方法内部,会持有一个set
集合,用以存放的是WebApplicationInitializer
,也就是SpringBootServletInitializer
的父类,也就是我们项目启动的回调类,同时调用其中的onStartup
方法。更进一步, SpringBootServletInitializer类中onStartup
方法的逻辑如下:
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 构建一个web容器
WebApplicationContext rootApplicationContext = createRootApplicationContext(servletContext);
// .. 省略无关代码
}
显然,所有关键
逻辑都在createRootApplicationContext()
方法中:
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
// configure方法就是我们重写的方法,把我们当前项目的启动类传入
builder = configure(builder);
// 熟悉的SpringApplication,项目中启动类main方法中也是用这个类调用run方法启动项目
SpringApplication application = builder.build();
// 内部逻辑调用SpringApplication.run方法启动项目。
return run(application);
}
此时,我们对以上代码做一个总结:
- 通过
SpringApplicationBuilder
的构建来整合配置,即准备应用配置类SpringApplication
- 然后调用
run
方法,此处内部的逻辑也就是调用SpringApplication.run()
,这就是外置Tomcat
启动Spring boot
应用的具体逻辑。
总结
至此,我们对SpringBoot
应用部署的两种方式进行了分析总结。分别分析了SpringBoot
内置服务器的启动逻辑和使用外置Tomcat
服务器时,启动SpringBoot
应用的逻辑。可能,在分析外置Tomcat
启动SpringBoot
应用的逻辑有些繁琐,但多读几遍梳理一下就会非常清晰哦~~~