今天在翻阅代码的时候,在主启动类偶然看见了SpringApplicationBuilder这样的代码,我也是第一次接触,对SpringApplicationBuilder的作用感到好奇
@SpringBootApplication@EnableCachingpublic 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来进行指定
@SpringBootApplicationpublic 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容器,指定端口为8848ConfigurableApplicationContext 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方法
@Overridepublic 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 ServletContainerInitializersfor (Map.Entry<ServletContainerInitializer, Set<Class
这里遍历所有的ServletContainerInitializer接口,然后回调onStartup方法(这里是一个嵌套回调,这个onStartup并不是上面介绍的),而Spring通过SpringServletContainerInitializer实现了ServletContainerInitializer接口,重写了onStartup。然后一个ServletContainerInitializer接口又对应一个set集合(存放的是WebApplicationInitializer,也就是SpringBootServletInitializer的父类,也就是我们项目启动的回调类)。 所以我们回到Spring boot中先找到ServletContainerInitializer子类SpringServletContainerInitializer查看回调的具体逻辑。
@Overridepublic void onStartup(@Nullable Set<Class> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List 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的项目就足够了。