从源码理解SpringBootServletInitializer的作用

2023-05-16

写在前面:

各位读友们好,最近已经很久没有更新文章了,并不是觉得写文章没意思之类的,笔者很希望能在"乱七八糟"的互联上做一些开源(能力有限,先做现有技术和思想开源。除了靠编程赚钱以外,这可能是支撑我一直学习的动力,希望能学到更多的内容开源出去)。之所以没有持续更新的原因——真的没时间。白天上班,晚上回去学习(为了以后给各位读者写更有深度的文章)。至于每天在学习什么内容,这个以后会无条件分享给大家(大概是偏底层方面的,各种中间件的源码和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源码能够完美闭环。

最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从源码理解SpringBootServletInitializer的作用 的相关文章

随机推荐