今天在翻阅代码的时候,在主启动类偶然看见了SpringApplicationBuilder这样的代码,我也是第一次接触,对SpringApplicationBuilder的作用感到好奇

@SpringBootApplication
@EnableCaching
public class DemoApplication extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder springApplicationBuilder) {
        return springApplicationBuilder.sources(DemoApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

查阅了一些资料,明白了它的作用,首先

SpringBoot的启动方式,大家都很了解了

 public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

        但这类方式启动会有一个问题,那就是它默认的读取配置文件用的是application.yml或者application.properties,这就会导致,如果一个项目中有多个配置文件,比如需要配置测试环境,线上环境,运行环境的时候,对库进行切换,这种情况下就需要使用SpringApplicationBuilder来进行指定

@SpringBootApplication
public class TestProfiles {
 
	public static void main(String[] args) {
		ConfigurableApplicationContext context = new SpringApplicationBuilder(TestProfiles.class)
				.properties("spring.config.location=classpath:/test-profiles.yml")
				.properties("spring.profiles.active=oracle")
				.run(args);
		// 输出变量
		System.out.println(context.getEnvironment().getProperty("jdbc.driver"));
 
		// 启动第二个Spring容器,指定端口为8848
		ConfigurableApplicationContext context2 = new SpringApplicationBuilder(TestProfiles.class)
				.properties("spring.config.location=classpath:/test-profiles.yml")
				.properties("spring.profiles.active=mysql")
				.properties("server.port=8848")
				.run(args);
		// 输出变量
		System.out.println(context2.getEnvironment().getProperty("jdbc.driver"));
	}
}

这类方法指定过后,可以启动多个容器,也可以去更改配置文件

好了,第二个点就是SpringBootServletInitializer 这个类的作用,对于这个类,我查找了一些资料

 意思就是说打war包的时候才需要这个类。并且上面的注释还说,最后实现当前类,重写configure方法,并且调用

也就是代码最开始那个@Override重写方法的逻辑

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

何时回调?我们暂时并不清楚,所以就先看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);   //此处调用了createRootApplicationContext方法
   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暂不清楚
创建SpringApplicationBuilder 来整合配置,准备生成SpringApplication
整合配置的整体逻辑不做说明,不过能看到我们的configure,因为此文章开头的代码就是重写了这个方法。将我们当前项目的启动类通过SpringApplicationBuilder类中的sources放入(其实并不会走启动类的main方法了,只是需要启动类的元数据信息,比如启动注解)。
然后调用run方法,内部的逻辑也就是调用SpringApplication.run(),这就是Spring boot启动的具体逻辑了。
以上代码是告诉读者Spring boot项目的另一种启动方式,所以接下来我们要找到onStartup方法的回调时机就能完美闭环。

而war包是运行在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会回调接口来初始化用户的Spring boot的项目就足够了。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐