源码分析|SpringBoot启动流程

Posted 永久D指针

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码分析|SpringBoot启动流程相关的知识,希望对你有一定的参考价值。


解析开始前先给出一张图,这是springboot项目启动时,我们可以扩展的接口,以及接口调用的时机,熟悉了这个流程,我们就可以方便快速的自定义我们自己的启动逻辑。如下:

  • SpringApplicationRunListener的各个方法被调用在SpringApplication.run()的各个时刻

  • ApplicationContextInitializer---用于编程形式的应用上下文初始化,在刷新ConfigurableApplicationContext#refresh()之前调用

  • ApplicationListener监听器

  • CommandLineRunner

这是一个简单的SpringCloud项目,这是EurekaServer模块,正好看到这里,以此来分析一下SpringBoot的启动流程,可能会和单纯的SpringBoot项目不同,因为SpringCloud会实现各种自定义扩展接口以注入各种驱动其自身运行对象。

首先看一下启动入口,依然是一个main方法,调用SpringApplication的静态run方法,传入的是EurekaServerApplication类对象和入参。

/** * @author wangcheng */@EnableEurekaServer@SpringBootApplicationpublic class EurekaServerApplication {
public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }
}

经过一系列的run重载方法,最终到达SpringApplication的构造方法中。

/** * Create a new {@link SpringApplication} instance. The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} * documentation for details. The instance can be customized before calling * {@link #run(String...)}. * @param resourceLoader the resource loader to use * @param primarySources the primary bean sources * @see #run(Class, String[]) * @see #setSources(Set) */@SuppressWarnings({ "unchecked", "rawtypes" })public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));   // ①根据类路径下的存在某些特殊的类来判断是哪种类型的应用 this.webApplicationType = WebApplicationType.deduceFromClasspath(); // ②使用SpringFactoriesLoader.loadFactoryNames方法从各个jar包的/META-INF/spring.factories配置文件中 // 找到实现了ApplicationContextInitializer上下文初始化接口的类,并实例化它们。 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));   // 同上,实例化ApplicationListener.class setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // ③推断该应用程序的主类,也就是EurekaServerApplication类,怎么实现呢,很巧妙。 // 首先获取到当前程序的堆栈元素数组,依次遍历,找到运行了main方法的哪个类的名称,再反射获取主类对象。 this.mainApplicationClass = deduceMainApplicationClass();}

①确定应用类型的代码贴出来看一下

static WebApplicationType deduceFromClasspath() { if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; } } return WebApplicationType.SERVLET;}

spring的SPI,很多地方会用到,有兴趣的同学可以看一下JAVA的SPI和Dubbo的SPI,各有千秋。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { ClassLoader classLoader = getClassLoader(); // Use names and ensure unique to protect against duplicates Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances;}

③确定主类的代码

private Class<?> deduceMainApplicationClass() { try { StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { if ("main".equals(stackTraceElement.getMethodName())) { return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) { // Swallow and continue } return null;}

接下来到真正重要的部分,SpringApplication的run方法,这里也就是整个Springboot启动的核心,当然一些隐藏的非常重要的流程是在各个扩展接口中实现的,我们稍后再说。看如下代码。

/** * Run the Spring application, creating and refreshing a new * {@link ApplicationContext}. * @param args the application arguments (usually passed from a Java main method) * @return a running {@link ApplicationContext} */public ConfigurableApplicationContext run(String... args) {   // new了一个简单的计时器,用于记录各个任务的时间相关的信息。 StopWatch stopWatch = new StopWatch();   // 启动计时器 stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();   // Headless模式是系统的一种配置模式。在系统可能缺少显示设备、键盘或鼠标这些外设的情况下可以使用该模式。 configureHeadlessProperty();   // 依旧是在spring.factories配置文件中找那些实现了SpringApplicationRunListener接口的类并实例化 SpringApplicationRunListeners listeners = getRunListeners(args); // 调用各个SpringApplicationRunListener实例的staring方法。这里SpringApplicationRunListeners类封装了一组 // SpringApplicationRunListener的实例,统一入口管理和调用,有点门面模式的意思。 listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);      // ④prepareEnvironment,顾名思义,就是准备应用所需要的环境信息 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);      // 忽略一些bean,因为这些bean就是为启动项目而准备的,spring会提前准备好 configureIgnoreBeanInfo(environment);      // 用来进行自定义启动banner图的,可以换成自己项目的名字等 Banner printedBanner = printBanner(environment);      // createApplicationContext创建应用上下文,依旧根据的是应用的类型穿件不同的上下文。       context = createApplicationContext(); // 依旧是在spring.factories配置文件中找那些实现了SpringBootExceptionReporter接口的类并实例化。当启动流程出现异常时调用。 exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);      // ⑥prepareContext,顾名思义,就是准备应用的上下文信息      prepareContext(context, environment, listeners, applicationArguments, printedBanner);      // refreshContext(context)是IOC的流程。 refreshContext(context); // afterRefresh(context, applicationArguments),留给子类实现,在上下文刷新后回调。 afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // 发出listeners.started(context)上下文启动事件。 listeners.started(context); // callRunners(context, applicationArguments)内部是调用那些实现了ApplicationRunner和CommandLineRunner接口的实例。用于在容器启动后,执行我们自定义的逻辑。 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); }
try { // listeners.running(context)发出容器正在运行事件。 listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context;}

④准备运行环境,包括

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // Create and configure the environment   // 获取或创建一个环境实例,这里是根据之前SpringApplicaton对象初始化时确定的应用类型来决定使用哪种环境实例的。 ConfigurableEnvironment environment = getOrCreateEnvironment();   // ⑤configureEnvironment,配置环境。分为配置属性源configurePropertySources和配置活跃环境文件configureProfiles。   // Spring的属性源可以适配各种类型的源,核心接口是PropertySource,有多种实现类如Map,Properties,ServletConfig等相关的属性源。   // 使用了对象适配器模式。configureProfiles就是配置spring.active.profile属性设置的配置。 configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); // 调用SpringApplicationRunListeners.environmentPrepared进行回调通知,最终处理该通知的是各个ApplicationListener实例。   // 这里的listeners中有一些还是比较重要的,如和SpringCloud相关的BootstrapApplicationListener。这个以后再说。 listeners.environmentPrepared(environment); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment;}

⑤配置各种propertySource和profiles

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { if (this.addConversionService) { ConversionService conversionService = ApplicationConversionService.getSharedInstance(); environment.setConversionService((ConfigurableConversionService) conversionService); }   // 这里面有jvm的环境变量和系统的环境变量,根据应用的类型不同还会有如servlet的web环境变量 configurePropertySources(environment, args); configureProfiles(environment, args);}

prepareContext

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment);   // postProcessApplicationContext为上下文设置beanNameGenerator,resourceLoader和conversionService。 postProcessApplicationContext(context); // applyInitializers应用初始化器,就是已开始在spring.factories中找到的那些ApplicationContextInitializer实例,依次调用其initialize方法。 applyInitializers(context); // 通知各个ApplicationListener,上下文已准备好。 listeners.contextPrepared(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // Add boot specific singleton beans // 为上下文注入一些已经创建的实例。 ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); } // Load the sources // ⑦获取所有的源并加载,这里的源就是我们初始化SpringApplication时传入的EurekaServerApplication.class, // 当然这里还有其他的source,可支持多种不同的加载类型,依次进入load方法,最终到达BeanDefinitionLoader类, // 此类是各个BeanDefinitionReader实现类的简单门面,如AnnotatedBeanDefinitionReader,XmlBeanDefinitionReader等。 // 下面的load方法就是为了支持各种不同的源而设计的。   // 在spring扫描所有bean时,会以此为起点 Set<Object> sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty"); load(context, sources.toArray(new Object[0])); listeners.contextLoaded(context);}

⑦load方法,最终,相应的source被注入到上下文中的beanFactory中成为beanDefinition。

private int load(Object source) { Assert.notNull(source, "Source must not be null"); if (source instanceof Class<?>) { return load((Class<?>) source); } if (source instanceof Resource) { return load((Resource) source); } if (source instanceof Package) { return load((Package) source); } if (source instanceof CharSequence) { return load((CharSequence) source); } throw new IllegalArgumentException("Invalid source type " + source.getClass());}

至此,SpringBoot启动流程结束。

这其中还有一些细节,如在各个阶段各个监听器都干了什么,spring的IOC过程等,这些放在其他文字中细讲。


以上是关于源码分析|SpringBoot启动流程的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 启动流程源码分析

SpringBoot启动流程分析:IoC容器的初始化过程

SpringBoot配置外部Tomcat项目启动流程源码分析(下)

SpringBoot配置外部Tomcat项目启动流程源码分析(下)

SpringBoot配置外部Tomcat项目启动流程源码分析(上)

SpringBoot配置外部Tomcat项目启动流程源码分析(上)