likes
comments
collection
share

一文讲透SpringBoot应用在内嵌Tomcat容器与外置容器下的启动原理

作者站长头像
站长
· 阅读数 21

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜


有过SpringBoot相关开发经历的都知道,SpringBoot支持内嵌容器,其支持内嵌Tomcat、Jetty等容器。此外,SpringBoot应用也可在外部的服务器进行部署。换言之,在部署SpringBoot应用时可以进行灵活的选择,既可以选择使用SpringBoot内嵌的Tomcat,也可使用外置Tomcat服务器。

那这两种不同部署方式在启动SpringBoot应用时的差异你有过了解吗?换言之,两种情况下容器又是在何时启动的呢? SpringBoot应用又是如何启动呢?

不清楚也别着急,本文会详细拆解两种不同部署方式在启动SpringBoot应用时的差异。

前言

而本文重点在于分析其中的服务器启动,再具体一点,我们要分析通过SpringBoot内嵌服务器部署应用和外置服务器部署SpringBoot应用间的差异。

内嵌容器的启动过程

在开始分析容器之前,不妨思考一个问题,如果要分析SpringBoot内嵌容器的启动该从何入手呢? 我想你肯定会脱口而出,那肯定是SpringApplication中的run方法啦!

接下来,就让我们看看SpringApplicationrun方法中究竟是如何来完成容器的启动的。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;
    }

进一步,上述方法可抽象成下图所示内容。

一文讲透SpringBoot应用在内嵌Tomcat容器与外置容器下的启动原理

可以看到,在refreshContext之前的逻辑,大多在配置一些参数、环境信息。而像启动容器这样关键的信息基本全部在refreshContext方法中进行实现。

接下来,我们就进入到refreshContext方法内部,来看看其内部究竟是如何来完成Tomcat容器启动的!


private void refreshContext(ConfigurableApplicationContext context) {
   // ... 省略其他无关代码
   
   refresh((ApplicationContext) context);
}

事实上,refreshContext最终会调到ConfigurableApplicationContextrefresh方法,来完成"刷新"操作,此处我们就不一步步debug了,直接给出最终调用的逻辑。

ServletWebServerApplicationContext # refresh

public final void refresh() throws BeansException, IllegalStateException {
   try {
      // <1> 此处会执行父类AbstractApplicationContext中的逻辑
      super.refresh();
   }
   catch (RuntimeException ex) {
      <2> 构建容器
      WebServer webServer = this.webServer;
      if (webServer != null) {
         webServer.stop();
      }
      throw ex;
   }
}

(注:SprignBoot在初始化容器时,会侦测当前环境下是否有DispatcherServlet的存在,如果存在则会构建一个ServletWebServerApplicationContext上下文环境,这部分逻辑可参考方法run中的createApplicationContext()方法。

<1>处所执行逻辑如下图所示,其本质就是一个Spring容器刷新的那一套操作。重点关注其中的onRefresh方法。因为,在Spring框架中,onRefresh 方法是 ApplicationListener 接口的一部分,用于监听应用上下文的刷新事件。当应用上下文刷新时(例如,当ApplicationContext 被创建并初始化时),Spring容器会调用onRefresh方法。这个方法允许开发者在容器刷新时执行一些特定的自定义逻辑。 一文讲透SpringBoot应用在内嵌Tomcat容器与外置容器下的启动原理

进一步,内嵌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) {
      // <1> 构建一个ServletWebServerFactory 
      ServletWebServerFactory factory = getWebServerFactory();
      // <2> 通过工程构建一个webServer
      this.webServer = factory.getWebServer(getSelfInitializer());
      // ... 省略无关代码
   }
  
}

<2>处逻辑会直接委托给TomcatServletWebServerFactorygetWebServer来完成,大致逻辑如下:

public WebServer getWebServer(ServletContextInitializer... initializers) {
 
   // 实例化一个Tomcat服务
   Tomcat tomcat = new Tomcat();
   
   // ... 省略大量配置过程
   
   // Tomcat启动会在此完成
   return getTomcatWebServer(tomcat);
}

至此,此时我们终于分析清楚了SpringBoot中内嵌服务器的启动逻辑了。其实SpringBoot内嵌Tomcat的启动本质就是:实例化一个Tomcat实例,然后调用该实例的start方法。 总结来看无非如下两行代码:

// <1> 实例化
Tomcat tomcat = new Tomcat();
// 启动
tomcat.start();
  1. 构建一个Tomcat实例;
  2. 启动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,这个类之前你可能没接触过,我们先来看下类注释信息:

一文讲透SpringBoot应用在内嵌Tomcat容器与外置容器下的启动原理

上述红色方框中的大致意思是说:当项目打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中屡见不鲜。

(注:后续的回调逻辑可能会有点绕,所以此处我们先给出一幅调用的逻辑关系图,避免读者在阅读时感到懵圈

TomcatStandardContextServletContainerInitializerWebApplicationInitializerstartstartInternal循环遍历ServletContainerInitializeronStartup遍历所有的WebApplicationInitializeronStartupTomcatStandardContextServletContainerInitializerWebApplicationInitializer

此处我们就不一步步debug分析了,直接给出Tomcat启动时调用的相关逻辑。Tomcat启动过程中会调用到StandardContext中启动的生命周期startInternal方法。其逻辑如下:


protected synchronized void startInternal() 
                            throws LifecycleException {

   // ....省略无关代码

  for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
    i   nitializers.entrySet()) {

     entry.getKey().onStartup(entry.getValue(),
                     getServletContext());
                     
  // ....省略无关代码  
}

上述代码中,会遍历所有的ServletContainerInitializer接口,然后回调onStartup方法。而Spring通过SpringServletContainerInitializer实现了ServletContainerInitializer接口,重写onStartup。其内部逻辑如下:

SpringServletContainerInitializer # onStartup


public void onStartup(@Nullable Set<Class<?>> 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应用的逻辑有些繁琐,但多读几遍梳理一下就会非常清晰哦~~~