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

2023年 9月 28日 76.7k 0

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

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

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

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

前言

在面试官:SpringBoot的启动流程熟悉吗?手写一个怎么样 中,我们通过一个不到50行的代码,对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;
    }

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

image.png

可以看到,在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 {
      //  此处会执行父类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方法。这个方法允许开发者在容器刷新时执行一些特定的自定义逻辑。
0aa9152865078cbe6de8ad81f8a0254.jpg

进一步,内嵌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());
      // ... 省略无关代码
   }
  
}

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

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,这个类之前你可能没接触过,我们先来看下类注释信息:

    image.png

    上述红色方框中的大致意思是说:当项目打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应用的逻辑有些繁琐,但多读几遍梳理一下就会非常清晰哦~~~

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论