SpringBoot的启动方式,以及SpringApplicationBuilder
今天在翻阅代码的时候,在主启动类偶然看见了SpringApplicationBuilder这样的代码,我也是第一次接触,对SpringApplicationBuilder的作用感到好奇查阅了一些资料,明白了它的作用,首先SpringBoot的启动方式,大家都很了解了但这类方式启动会有一个问题,那就是它默认的读取配置文件用的是application.yml或者application.properti
今天在翻阅代码的时候,在主启动类偶然看见了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的项目就足够了。
更多推荐
所有评论(0)