日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

從源碼理解SpringBootServletInitializer的作用

作者:程序員李哈 更新時間: 2022-08-13 編程語言

寫在前面:

各位讀友們好,最近已經很久沒有更新文章了,并不是覺得寫文章沒意思之類的,筆者很希望能在"亂七八糟"的互聯上做一些開源(能力有限,先做現有技術和思想開源。除了靠編程賺錢以外,這可能是支撐我一直學習的動力,希望能學到更多的內容開源出去)。之所以沒有持續更新的原因——真的沒時間。白天上班,晚上回去學習(為了以后給各位讀者寫更有深度的文章)。至于每天在學習什么內容,這個以后會無條件分享給大家(大概是偏底層方面的,各種中間件的源碼和Linux內核源碼等等)。

好了,多的不提了,回歸正題,今天也是在公司接手了一個老員工的項目。由于公司都是使用war包的形式,運行在公司的服務器的tomcat中。并不是現在流行的jar包內嵌tomcat的形式,兩者恰恰相反。就看到項目中以下的代碼。

public class ServletInitializer extends SpringBootServletInitializer {

   @Override
   protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
      return application.sources(TelecomApplication.class);
   }
}

正文:

比較好奇的我,接手這個項目并不是直接看業務層面的內容,而是在思考出幾個問題:

  • 打成war包是如何運行的?
  • 項目是spring boot的項目,那么在tomcat中如何運行spring boot的啟動邏輯呢?

抱著問題,筆者第一時間的思考是:我們項目打包成一個war包丟入tomcat中,運行和生命周期都是依賴于tomcat,而我們的項目又是一個spring boot的項目,就必須使用spring boot啟動邏輯來初始化項目(run方法)。而對于一個jar包項目都是運行我們項目中寫的啟動類中的main方法邏輯(也就是運行SpringApplication.run())。而tomcat自身啟動肯定也是main方法,而你自身的項目也是一個main方法,那肯定行不通,所以筆者大膽猜測是存在一些接口來設置當前項目的啟動邏輯(不走main方法),還有一些接口來做特定時期回調啟動當前項目。

說了這么多,好像還沒開始介紹我們的SpringBootServletInitializer類,那么了解一個類的入口在哪里,沒錯就是從注釋入手。

很明顯的意思就是說打war包的時候才需要這個類。并且上面的注釋還說,最后實現當前類,重寫configure方法,并且調用SpringApplicationBuilder.sources方法,將@Configuration類傳入(Spring boot啟動注解,也就是一個@Configuration)。所以就出現了本文章開頭的那段代碼邏輯

何時回調?我們暫時并不清楚,所以就先看SpringBootServletInitializer類中onStartup方法

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
   servletContext.setAttribute(LoggingApplicationListener.REGISTER_SHUTDOWN_HOOK_PROPERTY, false);
   // Logger initialization is deferred in case an ordered
   // LogServletContextInitializer is being used
   this.logger = LogFactory.getLog(getClass());
   WebApplicationContext rootApplicationContext = createRootApplicationContext(servletContext);
   if (rootApplicationContext != null) {
      servletContext.addListener(new SpringBootContextLoaderListener(rootApplicationContext, servletContext));
   }
   else {
      this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not "
            + "return an application context");
   }
}

?所有邏輯都在createRootApplicationContext()方法中,繼續追進去。

protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
   // 創建SpringApplicationBuilder來整合一些配置項,然后生成SpringApplication類。
   SpringApplicationBuilder builder = createSpringApplicationBuilder();類
   builder.main(getClass());
   ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
   if (parent != null) {
      this.logger.info("Root context already created (using as parent).");
      servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
      builder.initializers(new ParentContextApplicationContextInitializer(parent));
   }
   builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
   builder.contextFactory((webApplicationType) -> new AnnotationConfigServletWebServerApplicationContext());

   // configure方法就是我們重寫的方法,把我們當前項目的啟動類傳入
   builder = configure(builder);
   builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));

   // 熟悉的SpringApplication,項目中啟動類main方法中也是用這個類調用run方法啟動項目
   SpringApplication application = builder.build();
   if (application.getAllSources().isEmpty()
         && MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
      application.addPrimarySources(Collections.singleton(getClass()));
   }
   Assert.state(!application.getAllSources().isEmpty(),
         "No SpringApplication sources have been defined. Either override the "
               + "configure method or add an @Configuration annotation");
   // Ensure error pages are registered
   if (this.registerErrorPageFilter) {
      application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
   }
   application.setRegisterShutdownHook(false);

   // 內部邏輯調用SpringApplication.run方法啟動項目。 
   return run(application);
}

對以上代碼做一個總結:

  • 何時回調onStartup暫不清楚,后面會講(其實底子好點的讀者能思考到肯定是在tomcat中)
  • 創建SpringApplicationBuilder 來整合配置,準備生成SpringApplication
  • 整合配置的整體邏輯不做以詳細說明,不過能看到我們的configure,因為此文章開頭的代碼就是重寫了這個方法。將我們當前項目的啟動類通過SpringApplicationBuilder類中的sources放入(其實并不會走啟動類的main方法了,只是需要啟動類的元數據信息,比如啟動注解)。
  • 然后調用run方法,內部的邏輯也就是調用SpringApplication.run(),這就是Spring boot啟動的具體邏輯了。

以上代碼是告訴讀者Spring boot項目的另一種啟動方式,所以接下來我們要找到onStartup方法的回調時機就能完美閉環。

而war包是運行在tomcat中,所以回調時機肯定是在tomcat源碼中的某一個位置。這里不明白tomcat源碼的讀者也無關緊要,不過建議大家有時間去學習tomcat的源碼。

我們看到tomcat源碼中ServletContainerInitializer接口(這是servlet的接口)。確切的說,Spring boot是通過ServletContainerInitializer接口來完成的回調。

然后看到StandardContext中啟動的生命周期startInternal回調函數中一部分代碼邏輯。

// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
    initializers.entrySet()) {
    try {
        entry.getKey().onStartup(entry.getValue(),
                getServletContext());
    } catch (ServletException e) {
        log.error(sm.getString("standardContext.sciFail"), e);
        ok = false;
        break;
    }
}

這里遍歷所有的ServletContainerInitializer接口,然后回調onStartup方法(這里是一個嵌套回調,這個onStartup并不是上面介紹的),而Spring通過SpringServletContainerInitializer實現了ServletContainerInitializer接口,重寫了onStartup。然后一個ServletContainerInitializer接口又對應一個set集合(存放的是WebApplicationInitializer,也就是SpringBootServletInitializer的父類,也就是我們項目啟動的回調類)。

所以我們回到Spring boot中先找到ServletContainerInitializer子類SpringServletContainerInitializer查看回調的具體邏輯。

@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
      throws ServletException {

   List<WebApplicationInitializer> initializers = Collections.emptyList();

   if (webAppInitializerClasses != null) {
      initializers = new ArrayList<>(webAppInitializerClasses.size());
      for (Class<?> waiClass : webAppInitializerClasses) {
         // Be defensive: Some servlet containers provide us with invalid classes,
         // no matter what @HandlesTypes says...
         if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
               WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
            try {
               initializers.add((WebApplicationInitializer)
                     ReflectionUtils.accessibleConstructor(waiClass).newInstance());
            }
            catch (Throwable ex) {
               throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
            }
         }
      }
   }

   if (initializers.isEmpty()) {
      servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
      return;
   }

   servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
   AnnotationAwareOrderComparator.sort(initializers);
   for (WebApplicationInitializer initializer : initializers) {
      initializer.onStartup(servletContext);
   }
}

對以上代碼做一個總結:

  • 這里是一個嵌套回調,tomcat回調SpringServletContainerInitializer中的onStartup方法,然后onStartup方法里面又回調SpringBootServletInitializer的onStartup(注意這里的類命名有點類似,并且都是onStartup方法)
  • 獲取到從tomcat中SpringServletContainerInitializer對應所有到的WebApplicationInitializer
  • 做一些過濾處理
  • for循環做WebApplicationInitializer的回調機制,也就是回調onStartup()方法,也就是會回調他的子類SpringBootServletInitializer的onStartup()方法,也就是回調上面描述的Spring boot啟動邏輯。?

總結:

并不復雜,首先先合理分析jar和war包的區別,就能很快的定位會在哪里處理回調。

比較困難的就是定位tomcat的源碼,這必須要明白他的架構(就是一個遞歸架構)。其實對于這個源碼分析,就算讀者不懂tomcat源碼也不會很影響讀者來理解,能明白tomcat會回調接口來初始化用戶的Spring boot的項目就足夠了。只不過懂tomcat源碼能夠完美閉環。

最后,如果本帖對您有一定的幫助,希望能點贊+關注+收藏!您的支持是給我最大的動力,后續會一直更新各種框架的使用和框架的源碼解讀~!

原文鏈接:https://blog.csdn.net/qq_43799161/article/details/125315579

欄目分類
最近更新