当我们启动SpringBoot项目时,分为两步:创建SpringApplication类和执行run方法。
以下文章的阅读顺序即为方法的执行顺序
创建SpringApplication类
- 确定webApplicationType类型(创建哪种Spring容器)
关于这部分的作用,我曾经在《SpringBoot启动web项目时是如何选择上下文容器的?》一文中做了详细的解释。目的就是为了在创建容器的时候,根据webApplicationType去创建指定的容器
- 根据SPI机制获取ApplicationContextInitializer接口实现类
关于的SPI机制的介绍,我在《Spring自动装配原理代码跟踪详解及其实践》一文中做了详细的解释。我们可以通过实现
ApplicationContextInitializer接口,重写initialize方法。该方法的调用时机是在Spring容器创建之后(prepareContext阶段)。同时我们需要创建META-INF/spring.factories文件,key=
org.springframework.context.ApplicationContextInitializer,对应的value=对应实现类的权限定类名。
- 根据SPI机制获取ApplicationListener接口实现类
与
ApplicationContextInitializer同理,我们可以通过实现ApplicationListener接口,重写onApplicationEvent方法,然后创建META-INF/spring.factories文件,key=
org.springframework.context.ApplicationListener,对应的value=对应实现类的权限定类名。
该接口的调用时机在Spring容器创建之后(prepareContext阶段),通过
SpringApplicationRunListeners的contextLoaded方法进行调用(实际是
EventPublishingRunListener,下面会讲),调用时机比
ApplicationContextInitializer晚一点点。
- 获取main方法所在的主类
个人观察到的这个东西更像是在打印日志的时候才会使用到,或者在打印banner信息的时候。
调用run方法
- 获取根据SPI机制获取SpringApplicationRunListener接口实现类
在调用run方法中,这是第一个被执行的方法,通过SPI机制获取到所有的
SpringApplicationRunListener接口的实现类,并将集合信息放到
SpringApplicationRunListeners中的listeners集合中。此刻,用到了一种设计模式:观察者设计模式也叫发布订阅模式。
在之后Spring启动的各个阶段中,我们均可以看到
SpringApplicationRunListeners类的对应方法的执行。
SpringApplicationRunListeners类中对应的方法:starting()、environmentPrepared()、contextPrepared()、contextLoaded()、started()、running()、failed()。实际上调用的就是
SpringApplicationRunListener中对应的方法。
SpringApplicationRunListeners类中的方法将在Spring容器执行过程中的不同阶段被依次执行,我们可以根据被执行的时机来自定义一些我们要做的事情。
EventPublishingRunListener类实现了
SpringApplicationRunListener接口,因此在程序启动的各个阶段均能看到它对应的方法被调用,实际上我们通过SPI机制获取的ApplicationListener接口实现类的调用也是通过它来实现的,在接下来的讲解中我们将会看到。
- 调用SpringApplicationRunListeners的starting方法
实际执行的是集合中的
SpringApplicationRunListener类的starting方法,这个方法没有任何参数,可以做一些
SpringApplicationRunListener类的初始化工作。例如我们的
EventPublishingRunListener类,在starting阶段,它发布了ApplicationStartedEvent事件
- 获取spring环境
读取外部化配置环境信息是在prepareEnvironment方法执行时处理的,创建ConfigurableEnvironment环境信息,配置环境变量,包含系统属性和用户配置的属性。
外部化配置通常由application.yml文件(或者application.properties文件)提供,在application.yml文件中添加配置项也是最常用的外部化配置方式。下面是较为常用的外部化配置方式的优先级(由上到下优先级逐渐降低)。
1、命令行参数,即Command line arguments;
2、JAVA系统属性,即Java System properties(System#getProperties);
3、操作系统环境变量,即OS environment variables;
4、配置数据文件(例如application.yml文件),即Config data(such as application.properties files)jar包外指定了profile的配置数据文件:application-{profile}.ymljar包外的配置数据文件:application.ymljar包内指定了profile的配置数据文件:application-{profile}.ymljar包内的配置数据文件:application.yml
5、作用在由@Configuration注解修饰的类上的@PropertySource注解,即@PropertySourceannotations on your @Configuration classes;
6、默认属性,即Default properties(specified by setting SpringApplication#setDefaultProperties)。
ConfigurableEnvironment的实际类型为
StandardServletEnvironment,这是和Springboot的应用程序类型挂钩。
StandardServletEnvironment内部持有一个MutablePropertySources对象,该对象持有一个PropertySource的集合,Springboot加载的每一种外部化配置都会最终被解析为一个PropertySource的实现类并存放在MutablePropertySources的PropertySource集合中,PropertySource就是每一种外部化配置源在Springboot中的体现,其提供了对外部化配置的各种操作。
说一下ConfigurableEnvironment创建过程:
在SpringApplication#prepareEnvironment方法中,首先会调用getOrCreateEnvironment() 方法创建ConfigurableEnvironment对象,创建出来的ConfigurableEnvironment的实际类型会根据SpringApplication初始化时推断出来的WEB应用程序类型而定(webApplicationType),如果WEB应用程序类型为SERVLET,则创建出来的ConfigurableEnvironment实际类型为
StandardServletEnvironment,并且在初始化
StandardServletEnvironment时还会一并将JAVA系统属性和操作系统环境变量这两个外部化配置加载到
StandardServletEnvironment中。
- 调用SpringApplicationRunListener的environmentPrepared方法
实际执行的是集合中的
SpringApplicationRunListener类的environmentPrepared方法,这个方法会将ConfigurableEnvironment参数传入,因此可以对ConfigurableEnvironment对象做一些额外的工作。例如我们的yml读取是在此阶段实现的,通过
ConfigFileApplicationListener类(2.4.0版本以前),之后是
EnvironmentPostProcessorApplicationListener。然后
SpringApplicationRunListener发布environmentPrepared方法,紧接着调用
ConfigFileApplicationListener的onApplicationEvent进行读取环境信息。
而在
EnvironmentPostProcessorApplicationListener中实际上也用到了SPI机制去获取EnvironmentPostProcessor后置处理器。然后通过EnvironmentPostProcessor去读取各个环境。
- 获取banner信息(不重要)
这个其实没啥意思,有兴趣的可以看:《Banner-个性化定制控制台打印信息》
- 根据webApplicationType创建Spring容器ApplicationContext
在实例化SpringApplication阶段我们已经确定了webApplicationType的类型,这里我们便需要用到它:
这里我们可以看到,会根据webApplicationType的类型的类型去创建对应的上下文容器
当我们创建一个web项目时,这里会创建
AnnotationConfigServletWebServerApplicationContext容器,具体怎么就是个web项目了,可以看webApplicationType是怎么确定的。
接下来我们看一下
AnnotationConfigServletWebServerApplicationContext实例化的时候做了什么事情
- AnnotationConfigServletWebServerApplicationContext上下文容器实例化
在了解此章节前,可能需要你知道什么是BeanDefinition,详情请参考:
这里是
AnnotationConfigServletWebServerApplicationContext的无参构造方法,看到一共创建了两个对象
AnnotatedBeanDefinitionReader和
ClassPathBeanDefinitionScanner。
AnnotatedBeanDefinitionReader的作用除了负责注册beanDefinition以外,在创建的时候spring也默认向其中注册了最为重要的几个类
内部注册了最为重要的
ConfigurationClassPostProcessor类,当然还有其他类。
ConfigurationClassPostProcessor中负责大部分常用注解的解析过程,经过
ConfigurationClassPostProcessor的处理后,基本上我们项目中的类都会被注册beanDefinition
而
ClassPathBeanDefinitionScanner则是负责扫描类,创建beanDefinition,具体我们可以到解析@ComponentScan时看
- 通过SPI机制获取SpringBootExceptionReporter自定义打印异常信息
其实这个我并没有自己实践过,仅仅知道Spring启动过程中的异常是可以自定义的
当发生异常后try-catch后进行处理。
- prepareContext-预处理上下文容器,绑定环境信息
当上下文容器被初始化后,紧接着便需要向容器中去绑定一些信息,例如环境信息、bean名字处理器等。此后
ApplicationContextInitializer的initialize方法便会执行,这里不再赘述,最开始实例化的时候已经说了。
- prepareContext-调用SpringApplicationRunListener的contextPrepared方法
我们已经说过,
SpringApplicationRunListener中的方法会贯穿Spring启动的一生,此时在容器创建完毕后,可以对容器做一些自定义处理,contextPrepared方法会传入
ConfigurableApplicationContext,即刚实例化的容器上下文。
- prepareContext-将启动类注册到spring容器中,注册为BeanDefinition
在prepareContext中,最重要的一步大概就是将我们main方法所在的类注册为BeanDefinition了,因为启动类是我们扫描整个项目的开端。
判断当前主类上有Component注解,然后向注册器发起注册。
经过此后,我们项目中的第一个类便被注册为了BeanDefinition!
- prepareContext-调用SpringApplicationRunListener的contextLoaded方法
当主类被注册后,紧接着
SpringApplicationRunListener的contextLoaded方法便开始执行,同时依然传入我们的上下文容器,但是此时容器中已经有了我们的主类等信息。
- refreshContext-prepareRefresh刷新上下文容器之预刷新
经过上面的那些方法后我们的上下文已经准备好了,接下来refresh方法便开始扫描我们的所有类开始注册BeanDefinition并实例化BeanDefinition为bean!,同时又处理了除此之外的重要信息,我们依依来看。
我们先说prepareRefresh方法,它是refresh方法中第一个开始执行的,感觉里面做的事情也不是很重要,我们就不赘述了,给个截图吧
- refreshContext-prepareBeanFactory准备我们的bean工厂信息
我们的BeanFactory就在我们的上下文中,它作为一个成员变量存在于上下文中。如果说上下文容器是我们的核心的话,那么BeanFactory便是核心中的核心。
在此阶段,我们向
ignoredDependencyInterfaces集合中添加了参数,该set集合的作用是忽略集合中的类通过set进行注入:
例如ApplicationContextAware接口,此接口的作用可以看:《什么是spring-Aware?》
当我们实现ApplicationContextAware接口接口后,重写setApplicationContext方法,便可以获取对应的信息。但是如果我们实现了该Aware接口同时对ApplicationContext属性进行set方法注入时,此时set注入便会失效。
同时向resolvableDependencies的map中添加参数,该map的作用是当我们使用接口注入时,实际注入的类就是我们指定的value类:
举例:当我们注入BeanFactory时,由于BeanFactory是一个接口,对应有很多实现类,那么Spring应该选择哪一个进行注入呢?对应的注入值就是此map的value值。
此外还会向容器中注册一些额外的信息。
- refreshContext-postProcessBeanFactory后置处理beanFactory
可以处理一些额外的信息,例如扫描bean等。
- refreshContext-invokeBeanFactoryPostProcessors开始执行BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor
此方法在Spring启动过程中起到了至关重要的作用!我们的
ConfigurationClassPostProcessor就是此时执行的!
在此方法中,我们所有的BeanFactoryPostProcessor接口实现类和所有的
BeanDefinitionRegistryPostProcessor接口实现类将会被执行。这里是Spring扩展的一部分
首先执行的是通过监听等方式设置的
BeanDefinitionRegistryPostProcessor接口实现类的
postProcessBeanDefinitionRegistry后置处理方法。
然后是Spring内部注册的
BeanDefinitionRegistryPostProcessor接口实现类的
postProcessBeanDefinitionRegistry后置处理方法,其中
ConfigurationClassPostProcessor后置处理器的
postProcessBeanDefinitionRegistry方法就是在此时执行,该后置处理方法具有巨大的能量,经过此方法处理,我们项目中的绝大部分类都会被注册为BeanDefinition并存放到BeanFactory对应的容器中。由于我们是说启动流程,因此这里的代码实现就不再讲述了。
由于上面会将我们定义的
BeanDefinitionRegistryPostProcessor接口实现类扫描并注册为BeanDefinition,接下来就会通过getBean的方式获取对应的bean对象(没有就会创建),然后执行我们自定义的
BeanDefinitionRegistryPostProcessor接口实现类中的
postProcessBeanDefinitionRegistry后置处理方法。
BeanDefinitionRegistryPostProcessor接口调用完后接下来就是BeanFactoryPostProcessor接口实现类被调用。
首先就是通过监听等方式设置的BeanFactoryPostProcessor接口实现类的postProcessBeanFactory后置处理方法,因为
BeanDefinitionRegistryPostProcessor也是继承了BeanFactoryPostProcessor接口,因此此时
BeanDefinitionRegistryPostProcessor中的postProcessBeanFactory也会被执行。
然后就是我们自定义的BeanFactoryPostProcessor接口实现类的postProcessBeanFactory后置处理方法执行,这里也同理,
BeanDefinitionRegistryPostProcessor中的postProcessBeanFactory也会被执行。
- refreshContext-注册BeanPostProcessor后置处理器到容器中
该方法的作用是为了从BeanFactory中获取所有的BeanPostProcessor接口实现类,注意此刻所有的BeanPostProcessor还是BeanDefinition,然后通过getBean实例化所有的BeanPostProcessor,并将实例信息注册到beanPostProcessors集合中。
这里也是我们bean生命周期的一部分。
除了注册我们的BeanPostProcessor,这里也创建了几个内部的BeanPostProcessor,例如
ApplicationListenerDetector。
- refreshContext-初始化MessageSource国际化信息
这里就是创建了messageSource,然后可以支持国际化信息。
- refreshContext-初始化事件发布器ApplicationEventMulticaster
我们常用的spring的事件发布机制,对应的
ApplicationEventMulticaster发布器就是在这里创建的。它负责发布我们的事件信息。
- refreshContext-onRefresh处理化额外的bean信息
在这里,如果是web项目的话,会创建我们的tomcat、jetty等服务器信息。
同时HttpServletRequest等bean会被注入进来,请参考:
《为什么HttpServletRequest可以直接注入?》
同时ServletContextInitializer的onStartup方法也会在此时被调用,该接口被RegistrationBean实现用于往ServletContext容器中注册servelt(ServletRegistrationBean)、filter(FilterRegistrationBean)或者Listener。
- refreshContext-registerListeners注册所有的ApplicationListener到事件发布器
上面我们说了事件发布器已经被注册,这里我们将获取所有的ApplicationListener信息并实例化,然后注册到事件发布器中,用于发布事件。
- refreshContext-finishBeanFactoryInitialization方法中获取容器中的所有的BeanDefinition并开始实例化为bean对象
这已经是到refreshContext的最后了,经过上面的一系列处理后,我们的bean将会统一在这里被处理,通过beanFactory的preInstantiateSingletons将所有的非懒加载的bean进行创建。
- refreshContext-finishRefresh完成bean注册后的事件发布
这里是容器正常刷新的最后一个方法,在这里,将会发布容器创建完成事件,如果是web项目,这里也将会启动tomcat等。
- refreshContext-destroyBeans出现异常后的处理
这里是当容器创建过程中如果出现异常,bean的销毁工作将会在这里被执行。
- 调用SpringApplicationRunListener的started方法
到此时我们又回到了SpringApplication类中,经过上面的一系列处理后,我们的Spring项目已经基本完成了,这里将调用
SpringApplicationRunListener的started方法。
- callRunners-调用ApplicationRunner、CommandLineRunner接口
这里也是我们平时常用的接口,例如我们想在项目启动的时候自动加载一些缓存信息等,可以实现ApplicationRunner或者CommandLineRunner接口,重写对应的run方法即可
这俩接口的调用顺序是ApplicationRunner接口在前。
- SpringBootExceptionReporter自定义异常打印和SpringApplicationRunListener的failed方法
我们上面说了我们可以通过SPI机制来创建我们的
SpringBootExceptionReporter,如果容器执行过程中出现异常,我们可以自定义一些打印信息。将在这里执行。
同时
SpringApplicationRunListener的failed方法将会执行。
- 调用SpringApplicationRunListener的running方法
如果一切正常,那么这就到了项目启动的最后一步,Spring将调用
SpringApplicationRunListener的running方法,标志着项目正在运行。
这里也可以看到如果running方法异常,就会调用handleRunFailure方法,那么上一步的异常处理对应的信息将会执行。
总结
即便是说了这么多,依然感觉没有说全,Spring在启动过程中蕴藏了很多有意思的东西,依然还需要去探索。了解启动过程可以帮助我们定位问题,同时也可以更好的帮助我们理解Spring,去学习里面的一些编程思想。另外有什么不足之处或者哪里有说错的还请多多包涵~开发这条路还有很远要走,继续前行!