SpringBoot工作机制概述

1 SpringBoot简介

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
SpringBoot并不是要成为Spring平台里面众多“Foundation”层项目的替代者。SpringBoot的目标不在于为已解决的问题域提供新的解决方案,而是为平台带来另一种开发体验,从而简化对这些已有技术的使用。
SpringBoot主要有如下核心特点:
  • 包含执行所需的一切的可执行jar包。包含了运行所需的一切,包括内嵌应用服务器等,并打包为一个可执行jar文件部署,这点在微服务概念里非常重要。
  • 约定大于配置理念的完美践行,自动配置模块
  • 提供各种各样的starter简化初始配置过程
  • 提供各种扩展机制等等

2 一个简单的例子

下面先通过一个简单的例子看一下使用SpringBoot开发项目能有多快捷。我们打算编写一个web服务器,设置启动端口并提供一个简单的页面/index,当浏览该页面时会显示hello world。为了达到这样的效果只需要简单的几步就可以做到:


配置pom.xml

在其中添加如下依赖:
[html]  view plain  copy
  1. <dependency>  
  2.     <groupId>org.springframework.boot</groupId>  
  3.     <artifactId>spring-boot-starter-web</artifactId>  
  4. </dependency>  

编写项目的启动类
[java]  view plain  copy
  1. @SpringBootApplication  
  2. public class SBApplication {  
  3.     public static void main(String args[]) throws Exception{  
  4.         SpringApplication.run(SBApplication.class, args);  
  5.     }  
  6. }  

添加Controller
最后,编写页面/index对应的Controller:
[java]  view plain  copy
  1. @RestController  
  2. public class TestController {  
  3.     @RequestMapping("/index")  
  4.     public String index(){  
  5.         return "hello world";  
  6.     }  
  7. }  

默认端口为8080,为了设置自定义端口,我们在项目的/src/main/resources下新建application.properties文件,并在其中加上:
[plain]  view plain  copy
  1. server.port=7668  

如此一来,一个最简单的SpringBoot Web程序就完成了,由于启动类有main方法,我们直接run as application就可以运行这个程序,控制台输出结果为:
[plain]  view plain  copy
  1. ...................  
  2. 2017-12-30 00:50:35.873  INFO 14033 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup  
  3. 2017-12-30 00:50:35.931  INFO 14033 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 7668 (http)  
  4. 2017-12-30 00:50:35.937  INFO 14033 --- [           main] springbootext.SBApplication              : Started SBApplication in 3.752 seconds (JVM running for 4.089)  

看到输出的最后几句就表示web服务器启动成功了,端口为我们定义的7668。接下来访问
[plain]  view plain  copy
  1. http://localhost:7668/index  

就能看到页面上显示的hello world,一个基本的web服务器就这么简简单单创建完成了,简直是简单的令人发指。
上面的SpringBoot程序跟一个普通java程序的两点不同在于@SpringBootApplication注释以及SpringApplication类。下面我们介绍这两者的功能及其背后的SpringBoot框架的实现原理。

3 @SpringBootApplication注释

前面例子中为了简洁,直接将@SpringBootApplication打在了主类上,其实更加清晰的写法应该是将主类和SpringBoot配置类分开,如下所示:
[java]  view plain  copy
  1. @SpringBootApplication  
  2. public class SBConfiguration {  
  3. }  
[java]  view plain  copy
  1. public class SBApplication {  
  2.     public static void main(String args[]) throws Exception{  
  3.         SpringApplication.run(SBConfiguration.class, args);  
  4.     }  
  5. }  

如此一来,就能比较清晰的看出主类SBApplication只是程序的入口,没有什么特殊的。调用了SpringApplication的静态方法run,并使用SpringBoot主配置类SBConfigration.class作为参数。
主配置类就是打上@SpringBootApplication注释的类,首先看一下注释SpringBootApplication的代码:

[java]  view plain  copy
  1. @Target(ElementType.TYPE)  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. @Documented  
  4. @Inherited  
  5. @SpringBootConfiguration  
  6. @EnableAutoConfiguration  
  7. @ComponentScan(excludeFilters = {  
  8.         @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),  
  9.         @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })  
  10. public @interface SpringBootApplication {  
  11.     ...........................  
  12. }  

可以看出,这是一个复合注释,其中@SpringBootConfiguration代表了SpringBoot的配置类,除了测试时有些区别,大体上就是Spring标准@Configuration的替代品。
@EnableAutoConfiguration用于启动SpringBoot的自动配置机制,这是SpringBoot的核心特色之一,自动对各种机制进最大可能的进行配置。
@ComponentScan是Spring原来就有的注释,用于对指定的路径进行扫描,并将其中的@Configuration配置类加载。接下来分别对其一一介绍。

3.1 @SpringBootConfiguration

先看该注释的定义:
[java]  view plain  copy
  1. @Target(ElementType.TYPE)  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. @Documented  
  4. @Configuration  
  5. public @interface SpringBootConfiguration {  
  6. }  

从代码可见,其本质上就是一个@Configuration。唯一不同的地方是在测试时,如果打上了@SpringBootConfiguration注释,那么SpringBootTest中并不需要指定就可以自动加载该配置类;而当打上@Configuration时,需要通过@SpringBootTest(classes = SBConfiguration.class)来指定加载的SpringBoot配置类。
若不考虑测试时非要省略指定Configuration类的话,该注释可有可无。因为在作为参数传递给SpringApplication.run方法后,只要其中配置了@Bean方法,就会直接被认为是一个配置类进行加载处理,并不需要@Configuration来标识。
应用启动时在springcontext经典的refresh主流程中,当beanFactory创建完成后会执行invokeBeanFactoryPostProcessors阶段,该阶段会调用前面注册的BeanFactoryPostProcessors中的postProcessBeanDefinitionRegistry方法来对beanFactory进一步处理。
其中ConfigurationClassPostProcessor是专门用来处理Configuration类的启动加载进程的。其processConfigBeanDefinitions方法中首先使用:

[java]  view plain  copy
  1. String[] candidateNames = registry.getBeanDefinitionNames();  

获得所有已注册到registry中的beanDefinitionNames然后对其中的每一个beanDefinitionName判断其是否属于配置类:
[java]  view plain  copy
  1. for (String beanName : candidateNames) {  
  2.         BeanDefinition beanDef = registry.getBeanDefinition(beanName);  
  3.         if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||  
  4.                 ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {  
  5.             if (logger.isDebugEnabled()) {  
  6.                 logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);  
  7.             }  
  8.         }  
  9.         else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {  
  10.             configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));  
  11.         }  
  12.     }  

其中把判断beanDef是否为Configuration类委托给ConfigurationClassUtils来处理,ConfigurationClassUtils类通过两个方法来确定一个类是否是配置类:
[java]  view plain  copy
  1. public static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) {  
  2.     return metadata.isAnnotated(Configuration.class.getName());  
  3. }  
以及
[java]  view plain  copy
  1. public static boolean isLiteConfigurationCandidate(AnnotationMetadata metadata) {  
  2.     // Do not consider an interface or an annotation...  
  3.     if (metadata.isInterface()) {  
  4.         return false;  
  5.     }  
  6.   
  7.     // Any of the typical annotations found?  
  8.     for (String indicator : candidateIndicators) {  
  9.         if (metadata.isAnnotated(indicator)) {  
  10.             return true;  
  11.         }  
  12.     }  
  13.   
  14.     // Finally, let's look for @Bean methods...  
  15.     try {  
  16.         return metadata.hasAnnotatedMethods(Bean.class.getName());  
  17.     }  
  18.     catch (Throwable ex) {  
  19.         if (logger.isDebugEnabled()) {  
  20.             logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);  
  21.         }  
  22.         return false;  
  23.     }  
  24. }  

从上述代码可知,当一个类没有标注@Configuration但是其中定义了@Bean方法时,也会被判断为lite configuration(轻量级配置)类。判断过程完成后,委托ConfigurationClassParser来解析前面找到的每一个配置类,具体解析过程在此就不展开了,回头会专门写一篇关于spring context的介绍文章详细介绍。
由此我们可知,如果在springcontext refresh主流程的invokeBeanFactoryPostProcessors阶段之前就将beanDefinition注册到beanDefinitionRegistry,那么这个beanDefinition并不需要非得打上@Configuration注释,也可以通过其内部定义的@Bean方法被认定是一个Configuration类。
而在SpringApplication的run方法中以参数形式传递进去,就是方法之一。
例如,当我们把上述代码修改为:

[java]  view plain  copy
  1. @EnableAutoConfiguration  
  2. @ComponentScan  
  3. public class SBConfiguration {  
  4. }  
并不会有任何影响。

3.2 @EnableAutoConfiguration

EnableAutoConfiguration自动配置机制是SpringBoot的核心特色之一。可根据引入的jar包对可能需要的各种机制进进行默认配置。
该注释的定义如下:

[java]  view plain  copy
  1. @SuppressWarnings("deprecation")  
  2. @Target(ElementType.TYPE)  
  3. @Retention(RetentionPolicy.RUNTIME)  
  4. @Documented  
  5. @Inherited  
  6. @AutoConfigurationPackage  
  7. @Import(EnableAutoConfigurationImportSelector.class)  
  8. public @interface EnableAutoConfiguration {  
  9.     ..........  
  10. }  

其中@AutoConfigurationPackage用来指示包含打了该注释的类的包(package)应该被注册到AutoConfigurationPackages中,以备后续扩展机制(例如JPA或Mybatis等)的实体扫描器使用。
@EnableAutoConfiguration真正核心的动作就是通过Import机制加载EnableAutoConfigurationImportSelector.selectImports函数返回的配置类:

[java]  view plain  copy
  1. public String[] selectImports(AnnotationMetadata annotationMetadata) {  
  2.     if (!isEnabled(annotationMetadata)) {  
  3.         return NO_IMPORTS;  
  4.     }  
  5.     try {  
  6.         AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader  
  7.                 .loadMetadata(this.beanClassLoader);  
  8.         AnnotationAttributes attributes = getAttributes(annotationMetadata);  
  9.         List<String> configurations = getCandidateConfigurations(annotationMetadata,  
  10.                 attributes);  
  11.         configurations = removeDuplicates(configurations);  
  12.         configurations = sort(configurations, autoConfigurationMetadata);  
  13.         Set<String> exclusions = getExclusions(annotationMetadata, attributes);  
  14.         checkExcludedClasses(configurations, exclusions);  
  15.         configurations.removeAll(exclusions);  
  16.         configurations = filter(configurations, autoConfigurationMetadata);  
  17.         fireAutoConfigurationImportEvents(configurations, exclusions);  
  18.         return configurations.toArray(new String[configurations.size()]);  
  19.     }  
  20.     catch (IOException ex) {  
  21.         throw new IllegalStateException(ex);  
  22.     }  
  23. }  

其中比较核心的动作为getCandidateConfigurations(annotationMetadata, attributes),代码如下:
[java]  view plain  copy
  1. protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,  
  2.         AnnotationAttributes attributes) {  
  3.     List<String> configurations = SpringFactoriesLoader.loadFactoryNames(  
  4.             getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());  
  5.     Assert.notEmpty(configurations,  
  6.             "No auto configuration classes found in META-INF/spring.factories. If you "  
  7.                     + "are using a custom packaging, make sure that file is correct.");  
  8.     return configurations;  
  9. }  
  10. protected Class<?> getSpringFactoriesLoaderFactoryClass() {  
  11.     return EnableAutoConfiguration.class;  
  12. }  

我们注意:
[java]  view plain  copy
  1. List<String> configurations = SpringFactoriesLoader.loadFactoryNames(  
  2.                 getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());  

这句,SpringFactoriesLoader是spring framework内部使用的通用的工厂加载机制,其可加载并实例化可能出现在classpath上的多个jar包中的META-INF/spring.factories文件中定义的指定类型的工厂,可视为一种类似于SPI的接口。
SpringBoot利用这种SPI接口实现了autoconfiguration机制:委托SpringFactoriesLoader来加载所有配置在META-INF/spring.factories中的org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的值,spring-boot-autoconfiguration jar包中的META-INF/spring.factories中的EnableAutoConfiguration配置摘录如下:

[plain]  view plain  copy
  1. # Auto Configure  
  2. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
  3. org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\  
  4. org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\  
  5. org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\  
  6. org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\  
  7. org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\  
  8. org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\  
  9. org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\  
  10. ..........................  

其中我们可以看到相当多非常熟悉的自动配置类,例如AopAutoConfiguration、CacheAutoConfiguration等等。其中的每一个自动配置类都会在一定条件(@Condition)下启动生效,并对相关的机制进行默认自动的配置。这便是SpringBoot自动配置机制的核心功能所在。

3.3 @ComponentScan

这是spring-context原来就存在的注释,需要在@Configuration标注的类上标注,用来指示扫描某些包及其子包上的组件。可通过配置属性basePackageClasses、basePackages或value来指出需要扫描哪些包(包括其子包),如果没有指定任何一个属性值,则默认扫描当前包及其子包。
例如,在前面例子中,如果SBConfiguration所在的包是springbootext,那么由于SBConfiguration打了@ComponentScan注释,那么在springbootext、springbootext.service、springbootext.config等等地方定义的@Configuration、@Component、@Service、@Controller等等组件都可以直接被加载,无需额外配置。而在anotherpackage中定义的组件,无法被直接加载。可以通过设置扫描路径来解决:

[java]  view plain  copy
  1. @EnableAutoConfiguration  
  2. @ComponentScan(basePackages={"springbootext""anotherpackage"})  
  3. public class SBConfiguration{  
  4. }  

当然也可以通过借助3.2节中介绍的在spring.factories中定义扩展机制定义EnableAutoConfiguration来实现加载。
启动过程中@ComponentScan起作用的时机是在springcontext refresh主流程的invokeBeanFactoryPostProcessor阶段,也就是BeanFactory创建并准备完毕后通过BeanFactoryPostProcessors来进一步对beanFactory进行处理的阶段。
在该阶段,ConfigurationClassPostProcessor中对于Configuration类的处理里包括了识别其打的@ComponentScan注释,并委托ComponentScanAnnotationParser根据该注释的属性值进行组件扫描。将扫描生成的beanDefinitions注册到beanFactory中供下一个阶段创建beans。

4 SpringApplication一站式程序启动解决方案

当我们执行SpringApplication.run(Object[] sources, String[] args)静态方法时,内部会执行
[java]  view plain  copy
  1. new SpringApplication(sources).run(args);  

因此,我们也可以将前面程序主类的启动过程修改为:
[java]  view plain  copy
  1. public class SBApplication {  
  2.     public static void main(String args[]) throws Exception{  
  3.         SpringApplication sa = new SpringApplication(SBConfiguration.class);  
  4.         sa.run(args);  
  5.     }  
  6. }  

如此一来,我们可以使用到SpringApplication提供的一系列实例方法对其进行配置。
从上面代码看,应用的启动过程分为两部分:首先创建一个SpringApplication对象;然后执行其对象方法run。构造函数中实际业务逻辑都放在了initialize方法中。下面我们分别分析这两部分都干了什么。

4.1 初始化initialize

顾名思义,initialize就是初始化函数,其代码如下:
[java]  view plain  copy
  1. private void initialize(Object[] sources) {  
  2.     if (sources != null && sources.length > 0) {  
  3.         this.sources.addAll(Arrays.asList(sources));  
  4.     }  
  5.     this.webEnvironment = deduceWebEnvironment();  
  6.     setInitializers((Collection) getSpringFactoriesInstances(  
  7.             ApplicationContextInitializer.class));  
  8.     setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));  
  9.     this.mainApplicationClass = deduceMainApplicationClass();  
  10. }  

首先,将初始化参数中的Object... sources保存在this.sources中,供后续run方法的prepareContext阶段使用。该参数代表了SpringBoot启动时指定的Configuration类(可多个)。然后通过判断当前是否含有:
[java]  view plain  copy
  1. private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",  
  2.         "org.springframework.web.context.ConfigurableWebApplicationContext" };  

中定义的两个类,来判断当前是否是一个web环境,后续会针对不同的环境创建不同的ApplicationContext。
接着通过两个函数getSpringFactoriesInstances(ApplicationContextInitializer.class)、getSpringFactoriesInstances(ApplicationListener.class)得到以SpringFactoriesLoader扩展方案注册的ApplicationContextInitializer和ApplicationListener类型的实例,并设置到当前SpringApplication的对象中。在这两个函数都调用了:

[java]  view plain  copy
  1. private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,  
  2.         Class<?>[] parameterTypes, Object... args) {  
  3.     ClassLoader classLoader = Thread.currentThread().getContextClassLoader();  
  4.     // Use names and ensure unique to protect against duplicates  
  5.     Set<String> names = new LinkedHashSet<String>(  
  6.             SpringFactoriesLoader.loadFactoryNames(type, classLoader));  
  7.     List<T> instances = createSpringFactoriesInstances(type, parameterTypes,  
  8.             classLoader, args, names);  
  9.     AnnotationAwareOrderComparator.sort(instances);  
  10.     return instances;  
  11. }  

来获取特定类型的实例,又看到了熟悉的SpringFactoriesLoader。查找配置在classpath及各个jar包中的META-INF/spring.factories中配置的ApplicationContextInitializer和ApplicationListener,并实例化。
最后一步得到含有main函数的主类,并保存在mainApplicationClass属性中。

4.2 实际启动过程run

SpringApplication将SpringBoot应用启动流程模板化,并在启动过程的不同时机定义了一系列不同类型的的扩展点,方便我们对其进行定制。下面对整个启动过程代码进行分析:


1. 通过SpringFactoriesLoader来获取定义在spring.factories中的SpringApplicationRunListener,SpringBoot框架默认只定义了一个EventPublishingRunListener,其中维护了一个SimpleApplicationEventMulticaster,并将前面initialize阶段中获得的ApplicationListeners注册进去。然后调用其start方法,给所有的SpringApplicationRunListener发送一个start事件,然后EventPublishingRunListener给注册在其中的所有ApplicationListener发送ApplicationStartedEvent。

[java]  view plain  copy
  1. SpringApplicationRunListeners listeners = getRunListeners(args);  
  2. listeners.starting();  
此处包含了两个扩展点,我们可以自定义SpringApplicationRunListener以扩展SpringBoot程序启动过程,也可以自定义ApplicationListener以扩展EventPublishingRunListener。我们后面可以看到在启动的不同阶段,会发送不同的事件给SpringApplicationRunListeners和ApplicationListeners。


2. 将参数包装为ApplicationArguments,DefaultApplicationArguments是用来维护命令行参数的,例如可以方便的将命令行参数中的options和non options区分开,以及获得某option的值等。

[java]  view plain  copy
  1. ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);  

3. 通过ApplicationArguments来准备应用环境Environment, Environment包含了两个层面的信息:属性(properties)和轮廓(profiles):
[java]  view plain  copy
  1. ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);  
prepareEnvironment方法的代码如下:
[java]  view plain  copy
  1. private ConfigurableEnvironment prepareEnvironment(  
  2.         SpringApplicationRunListeners listeners,  
  3.         ApplicationArguments applicationArguments) {  
  4.     // Create and configure the environment  
  5.     ConfigurableEnvironment environment = getOrCreateEnvironment();  
  6.     configureEnvironment(environment, applicationArguments.getSourceArgs());  
  7.     listeners.environmentPrepared(environment);  
  8.     if (!this.webEnvironment) {  
  9.         environment = new EnvironmentConverter(getClassLoader())  
  10.                 .convertToStandardEnvironmentIfNecessary(environment);  
  11.     }  
  12.     return environment;  
  13. }  
在getOrCreateEnvironment()方法中通过4.1初始化中判断的是否为web应用创建一个StandardServletEnvironment或StandardEnvironment。然后执行configureEnvironment函数:
[java]  view plain  copy
  1. protected void configureEnvironment(ConfigurableEnvironment environment,  
  2.         String[] args) {  
  3.     configurePropertySources(environment, args);  
  4.     configureProfiles(environment, args);  
  5. }  
其中分别配置属性源(propertySource)以及轮廓(profile),关于Environment中的属性来源分散在启动的若干个阶段,并且按照特定的优先级顺序,也就是说一个属性值可以在不同的地方配置,但是优先级高的值会覆盖优先级低的值。具体属性取值顺序我们将专门在下面详细介绍。
而轮廓(profile)可以认为是程序的运行环境,典型的环境比如有开发环境(Develop)、生产环境(Production)、测试环境(Test)等等。我们可以定义某个Bean在特定的环境中才生效,这样就可以通过指定profile来方便的切换运行环境。可通过SpringApplication.setAdditionalProfiles()来设置轮廓,environment内通过activeProfiles来维护生效的轮廓(可不止一个)。
在环境配置完毕后,执行所有SpringApplicationRunListeners的environmentPrepared函数,然后EventPublishingRunListener给所有注册其中的ApplicationListeners发送一个“环境准备好了”ApplicationEnvironmentPreparedEvent事件:

[java]  view plain  copy
  1. listeners.environmentPrepared(environment);  
此时各SpringApplicationRunListener或ApplicationListener已经可以得到environment了, 可以在此处对environment进行一些额外的处理。


4. 根据前面判断的是web应用还是普通应用决定创建什么类型的ApplicationContext:

[java]  view plain  copy
  1. context = createApplicationContext();  
createApplicationContext方法代码如下:
[java]  view plain  copy
  1. public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."  
  2.         + "annotation.AnnotationConfigApplicationContext";  
  3. public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework."  
  4.         + "boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext";  
  5. protected ConfigurableApplicationContext createApplicationContext() {  
  6.     Class<?> contextClass = this.applicationContextClass;  
  7.     if (contextClass == null) {  
  8.         try {  
  9.             contextClass = Class.forName(this.webEnvironment  
  10.                     ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);  
  11.         }  
  12.         catch (ClassNotFoundException ex) {  
  13.             throw new IllegalStateException(  
  14.                     "Unable create a default ApplicationContext, "  
  15.                             + "please specify an ApplicationContextClass",  
  16.                     ex);  
  17.         }  
  18.     }  
  19.     return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);  
  20. }  
关于ApplicationContext多说两句:ApplicationContext用于扩展BeanFactory中的功能,ApplicationContext拥有BeanFactory对于Bean的管理维护的所有功能,并且提供了更多的扩展功能,实际上ApplicationContext的实现在内部持有一个BeanFactory的实现来完成BeanFactory的工作。AbstractApplicationContext是ApplicationContext的第一个抽象实现类,其中使用模板方法模式定义了springcontext的核心扩展流程refresh,并提供几个抽象函数供具体子类去实现。其直接子类有AbstractRefreshableApplicationContext和GenericApplicationContext两种。
这两个子类的不同之处在于对内部的DefaultListableBeanFactory的管理:AbstractRefreshableApplicationContext允许多次调用其refreshBeanFactory()函数,每次调用时都会重新创建一个DefaultListableBeanFactory,并将已有的销毁;而GenericApplicationContext不允许刷新beanFactory,只能调用refreshBeanFactory()一次,当多次调用时会抛出异常。
无论AnnotationConfigApplicationContext还是AnnotationConfigEmbeddedWebApplicationContext,它们都是GenericApplicationContext的子类。因此其内部持有的BeanFactory是不可刷新的,并且从初始化开始就一直持有一个唯一的BeanFactory。相关细节会在spring context的介绍中深入讨论。


5. 借助SpringFactoriesLoader获得spring.factories中注册的FailureAnalyzers以供当运行过程中出现异常时进行分析:

[java]  view plain  copy
  1. analyzers = new FailureAnalyzers(context);  
其中又用到了SpringFactoriesLoader扩展方案:
[java]  view plain  copy
  1. List<String> analyzerNames = SpringFactoriesLoader  
  2.         .loadFactoryNames(FailureAnalyzer.class, classLoader);  

6. 接着就是SpringBoot启动过程中的最核心流程,对第4步创建的ApplicationContext进行准备:
[java]  view plain  copy
  1. prepareContext(context, environment, listeners, applicationArguments, printedBanner);  
此处就不展开了,在下一节专门对这个函数进行分析。


扫描二维码关注公众号,回复: 1669627 查看本文章

7. 调用ApplicationContext的refresh函数,开启spring context的核心流程,就是根据配置加载bean(spring beans核心功能)以及在各个时机开放的不同扩展机制(spring context):

[java]  view plain  copy
  1. refreshContext(context);  
这个过程在此不展开了,后续会写关于spring context的文章专门介绍。


8. 获取所有的ApplicationRunner和CommandLineRunner并执行:

[java]  view plain  copy
  1. afterRefresh(context, applicationArguments);  
此时由于context已经refresh完毕,因此bean都已经加载完毕了。所以这两个类型的runner都是直接从context中获取的:
[java]  view plain  copy
  1. protected void afterRefresh(ConfigurableApplicationContext context,  
  2.         ApplicationArguments args) {  
  3.     callRunners(context, args);  
  4. }  
  5. private void callRunners(ApplicationContext context, ApplicationArguments args) {  
  6.     List<Object> runners = new ArrayList<Object>();  
  7.     runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());  
  8.     runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());  
  9.     AnnotationAwareOrderComparator.sort(runners);  
  10.     for (Object runner : new LinkedHashSet<Object>(runners)) {  
  11.         if (runner instanceof ApplicationRunner) {  
  12.             callRunner((ApplicationRunner) runner, args);  
  13.         }  
  14.         if (runner instanceof CommandLineRunner) {  
  15.             callRunner((CommandLineRunner) runner, args);  
  16.         }  
  17.     }  
  18. }  
两者的执行时机是完全一样的,唯一的区别在于一个接受ApplicationArguments,一个接受String[]类型的原始命令行参数。而ApplicationArguments也只是对原始命令行参数的一个封装,因此本质上是一样的。
此处又定义了两个扩展机制,我们可以自定义ApplicationRunner或CommandLineRunner并将其配置为Bean,便可以在context refresh完毕后执行。


9. spring-context refresh过程完毕后执行所有SpringApplicationRunListeners的finished函数,然后EventPublishingRunListener给所有注册其中的ApplicationListeners发送一个“应用启动完毕”ApplicationReadyEvent事件:

[java]  view plain  copy
  1. listeners.finished(context, null);  


10. 当运行时出现异常时,向context发送退出码事件ExitCodeEvent,供其内部listener执行退出前的操作;并使用前面第5步获得的analyzers来打印可能的原因:

[java]  view plain  copy
  1. handleRunFailure(context, listeners, analyzers, ex);  
另外,就算运行异常,也会向SpringApplication中的listeners发送“应用启动完毕”的事件,代码如下:
[java]  view plain  copy
  1. private void handleRunFailure(ConfigurableApplicationContext context,  
  2.         SpringApplicationRunListeners listeners, FailureAnalyzers analyzers,  
  3.         Throwable exception) {  
  4.     try {  
  5.         try {  
  6.             handleExitCode(context, exception);  
  7.             listeners.finished(context, exception);  
  8.         }  
  9.         finally {  
  10.             reportFailure(analyzers, exception);  
  11.             if (context != null) {  
  12.                 context.close();  
  13.             }  
  14.         }  
  15.     }  
  16.     catch (Exception ex) {  
  17.         logger.warn("Unable to close ApplicationContext", ex);  
  18.     }  
  19.     ReflectionUtils.rethrowRuntimeException(exception);  
  20. }  
此时,EventPublishingRunListener发送给注册其中的ApplicationListeners的事件成了”应用启动异常“ApplicationFailedEvent。


至此,SpringApplication的run函数,也就是SpringBoot应用的启动过程就执行完毕了。可以看出,SpringBoot的启动过程是对Spring context启动过程的扩展,在其中定义了若干的扩展点并提供了不同的扩展机制。并提供了默认配置,我们可以什么都不配,也可以进行功能非常强大的配置和扩展。这也正是SpringBoot的优势所在。

4.3 核心过程prepareContext

顾名思义,该函数的功能就是对前面创建的ApplicationContext进行准备,其执行步骤如下:


1. 将environment设置到context中:

[java]  view plain  copy
  1. context.setEnvironment(environment);  
environment是我们在run过程的第3步创建的。


2. 对ApplicationContext应用相关的后处理,子类可以重写该方法来添加任意的后处理功能:

[java]  view plain  copy
  1. postProcessApplicationContext(context);  
该方法代码如下:
[java]  view plain  copy
  1. protected void postProcessApplicationContext(ConfigurableApplicationContext context) {  
  2.     if (this.beanNameGenerator != null) {  
  3.         context.getBeanFactory().registerSingleton(  
  4.             AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,  
  5.             this.beanNameGenerator);  
  6.     }  
  7.     if (this.resourceLoader != null) {  
  8.         if (context instanceof GenericApplicationContext) {  
  9.             ((GenericApplicationContext) context)  
  10.                 .setResourceLoader(this.resourceLoader);  
  11.         }  
  12.         if (context instanceof DefaultResourceLoader) {  
  13.             ((DefaultResourceLoader) context)  
  14.                 .setClassLoader(this.resourceLoader.getClassLoader());  
  15.         }  
  16.     }  
  17. }  
如果SpringApplication设置了beanNameGenerator,则将其注册为singleton类型的bean,并命名为:
[plain]  view plain  copy
  1. org.springframework.context.annotation.internalConfigurationBeanNameGenerator  
另外,若SpringApplication设置了resourceLoader,则设置进context中。


3. 对initialize阶段得到的通过spring.factories注册进来的所有ApplicationContextInitializer,逐个执行其initialize方法来修改context,并在执行之前对其进行校验:

[java]  view plain  copy
  1. protected void applyInitializers(ConfigurableApplicationContext context) {  
  2.     for (ApplicationContextInitializer initializer : getInitializers()) {  
  3.         Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(  
  4.             initializer.getClass(), ApplicationContextInitializer.class);  
  5.         Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");  
  6.         initializer.initialize(context);  
  7.     }  
  8. }  
此处定义了一个扩展点,可以自定义并通过spring.factories注册ApplicationContextInitializer,这些ApplicationContextInitializer可在ApplicationContext准备完毕后对其进行维护修改,例如可以改变其定义的activeProfiles以改变应用环境。


4. 执行所有SpringApplicationRunListeners的contextPrepared函数,注意EventPublishingRunListener并没有给所有注册其中的ApplicationListeners发送对应的事件:

[java]  view plain  copy
  1. listeners.contextPrepared(context);  
此时的listeners可以获得context作为参数,从而对context进行修改。


5. 将applicationArguments注册进context.getBeanFactory()中,名字为"SpringApplicationArguments"、若printBanner不为空,将printBanner注册到context.getBeanFactory()中,名字为"SpringBootBanner":

[java]  view plain  copy
  1. context.getBeanFactory().registerSingleton("springApplicationArguments",  
  2.     applicationArguments);  
  3. if (printedBanner != null) {  
  4.     context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);  
  5. }  

6. 得到所有的sources(可通过SpringApplication的run函数、构造函数和setSources函数指定,代表了一个或多个Configuration类),然后执行load(context, sources)函数:
[java]  view plain  copy
  1. Set<Object> sources = getSources();  
  2. Assert.notEmpty(sources, "Sources must not be empty");  
  3. load(context, sources.toArray(new Object[sources.size()]));  
load函数中会创建一个BeanDefinitionLoader并设置其beanNameGenerator, resourceLoader, environment等属性,然后委托其执行具体的load动作,代码如下:
[java]  view plain  copy
  1. protected void load(ApplicationContext context, Object[] sources) {  
  2.     if (logger.isDebugEnabled()) {  
  3.     logger.debug(  
  4.         "Loading source " + StringUtils.arrayToCommaDelimitedString(sources));  
  5.     }  
  6.     BeanDefinitionLoader loader = createBeanDefinitionLoader(  
  7.         getBeanDefinitionRegistry(context), sources);  
  8.     if (this.beanNameGenerator != null) {  
  9.         loader.setBeanNameGenerator(this.beanNameGenerator);  
  10.     }  
  11.     if (this.resourceLoader != null) {  
  12.         loader.setResourceLoader(this.resourceLoader);  
  13.     }  
  14.     if (this.environment != null) {  
  15.         loader.setEnvironment(this.environment);  
  16.     }  
  17.     loader.load();  
  18. }  
其中对于每一个source根据其类型不同执行不同的load逻辑:class, Resource, Package, CharSequence等。将解析出来的所有bean的BeanDefinition注册到BeanDefinitionRegistry中(注意,只是source本身,并不包括其内部定义的@Bean方法):
[java]  view plain  copy
  1. public int load() {  
  2.     int count = 0;  
  3.     for (Object source : this.sources) {  
  4.         count += load(source);  
  5.     }  
  6.     return count;  
  7. }  
  8. private int load(Object source) {  
  9.     Assert.notNull(source, "Source must not be null");  
  10.     if (source instanceof Class<?>) {  
  11.         return load((Class<?>) source);  
  12.     }  
  13.     if (source instanceof Resource) {  
  14.         return load((Resource) source);  
  15.     }  
  16.     if (source instanceof Package) {  
  17.         return load((Package) source);  
  18.     }  
  19.     if (source instanceof CharSequence) {  
  20.         return load((CharSequence) source);  
  21.     }  
  22.     throw new IllegalArgumentException("Invalid source type " + source.getClass());  
  23. }  
由于我们的source是class类,所以load某一个具体source的行为是委托给了AnnotatedBeanDefinitionReader的register方法:
[java]  view plain  copy
  1. public void register(Class<?>... annotatedClasses) {  
  2.     for (Class<?> annotatedClass : annotatedClasses) {  
  3.         registerBean(annotatedClass);  
  4.     }  
  5. }  

此处已是spring context的功能了,将通过注释定义的Configuration类的BeanDefinition注册到BeanDefinitionRegistry中。(此时尚不解析Configuration类内部定义的@Bean方法)


7. 执行所有SpringApplicationRunListeners的contextLoaded函数,然后EventPublishingRunListener给所有注册其中的ApplicationListeners发送一个“应用上下文准备完毕”ApplicationPreparedEvent事件,另外还将所有注册在自身的ApplicationListener注册到context之中:
[java]  view plain  copy
  1. listeners.contextLoaded(context);  
其中调用到EventPublishingRunListener的contextLoaded函数:
[java]  view plain  copy
  1. public void contextLoaded(ConfigurableApplicationContext context) {  
  2.     for (ApplicationListener<?> listener : this.application.getListeners()) {  
  3.         if (listener instanceof ApplicationContextAware) {  
  4.             ((ApplicationContextAware) listener).setApplicationContext(context);  
  5.         }  
  6.         context.addApplicationListener(listener);  
  7.     }  
  8.     this.initialMulticaster.multicastEvent(  
  9.         new ApplicationPreparedEvent(this.application, this.args, context));  
  10. }  

5 SpringBoot扩展机制

SpringApplication首先提供了一系列的setter方法来进行定制,另外从上面对于SpringBoot启动过程的解读可知,对于SpringBoot有如下几种扩展机制:

5.1 SpringApplicationRunListener

SpringApplicationRunListener是SpringBoot独有的概念,用以在SpringBoot应用启动过程的不同时机接收事件通知并执行相应操作,接口定义如下:
[java]  view plain  copy
  1. public interface SpringApplicationRunListener {  
  2.     void starting();  
  3.     void environmentPrepared(ConfigurableEnvironment environment);  
  4.     void contextPrepared(ConfigurableApplicationContext context);  
  5.     void contextLoaded(ConfigurableApplicationContext context);  
  6.     void finished(ConfigurableApplicationContext context, Throwable exception);  
  7. }  

SpringBoot框架内置定义了一个EventPublishingRunListener用以将定义在spring.factories中的ApplicationListener管理起来,并在SpringBoot应用启动的不同时机通知它们执行各自的业务逻辑。
我们可以自定义一个实现了EventPublishingRunListener接口的类,例如:

[java]  view plain  copy
  1. public class MySpringApplicationRunListener implements SpringApplicationRunListener {  
  2.     Logger logger = LoggerFactory.getLogger(MySpringApplicationRunListener.class);  
  3.     private final SpringApplication application;  
  4.     private final String[] args;  
  5.     public MySpringApplicationRunListener(SpringApplication application, String[] args){  
  6.         this.application = application;  
  7.         this.args = args;  
  8.     }  
  9.     @Override  
  10.     public void starting() {  
  11.         System.out.println("===============starting");  
  12.     }  
  13.   
  14.     @Override  
  15.     public void environmentPrepared(ConfigurableEnvironment environment) {  
  16.     environment.setActiveProfiles("Develop");  
  17.         logger.info("===============environmentPrepared");  
  18.     }  
  19.   
  20.     @Override  
  21.     public void contextPrepared(ConfigurableApplicationContext context) {  
  22.         logger.info("===============contextPrepared");  
  23.     }  
  24.   
  25.     @Override  
  26.     public void contextLoaded(ConfigurableApplicationContext context) {  
  27.         logger.info("===============contextLoaded");  
  28.     }  
  29.   
  30.     @Override  
  31.     public void finished(ConfigurableApplicationContext context,  
  32.         Throwable exception) {  
  33.         logger.info("===============finished");  
  34.     }  
  35. }  

注意:其必须拥有一个SpringApplication类型和String[]类型参数的构造函数:
[java]  view plain  copy
  1. public MySpringApplicationRunListener(SpringApplication application, String[] args)  

另外,SpringBoot启动过程中,LoggingApplicationListener作为一个注册到spring.factories中的ApplicationListener,其在接收到ApplicationEnvironmentPreparedEvent时,才会执行logging system的初始化,因此在starting方法中是没法使用logger打印日志的。


然后在META-INF/spring.factories中添加如下配置:

[plain]  view plain  copy
  1. org.springframework.boot.SpringApplicationRunListener=\  
  2.     springbootext.runlistener.MySpringApplicationRunListener  

此时启动应用,可得到如下的输出:
[plain]  view plain  copy
  1. ===============starting  
  2. 2017-12-31 02:28:39.647  INFO 16329 --- [           main] s.r.MySpringApplicationRunListener       : ===============environmentPrepared  
  3.   
  4.   
  5.   .   ____          _            __ _ _  
  6.  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \  
  7. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \  
  8.  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )  
  9.   '  |____| .__|_| |_|_| |_\__, | / / / /  
  10.  =========|_|==============|___/=/_/_/_/  
  11.  :: Spring Boot ::        (v1.5.8.RELEASE)  
  12.   
  13.   
  14. 2017-12-31 02:28:39.768  INFO 16329 --- [           main] s.r.MySpringApplicationRunListener       : ===============contextPrepared  
  15. 2017-12-31 02:28:39.772  INFO 16329 --- [           main] springbootext.SBApplication              : Starting SBApplication on wangd-ThinkPad-T450 with PID 16329 (/home/wangd/work/test/springbootext/target/classes started by wangd in /home/wangd/work/test/springbootext)  
  16. 2017-12-31 02:28:39.773  INFO 16329 --- [           main] springbootext.SBApplication              : The following profiles are active: Develop  
  17. 2017-12-31 02:28:39.824  INFO 16329 --- [           main] s.r.MySpringApplicationRunListener       : ===============contextLoaded  
  18. 2017-12-31 02:28:39.828  INFO 16329 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@58a90037: startup date [Sun Dec 31 02:28:39 CST 2017]; root of context hierarchy  
  19. 2017-12-31 02:28:41.654  INFO 16329 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 7668 (http)  
  20. 2017-12-31 02:28:41.672  INFO 16329 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]  
  21. 2017-12-31 02:28:41.673  INFO 16329 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23  
  22. 2017-12-31 02:28:41.802  INFO 16329 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext  
  23. 2017-12-31 02:28:41.802  INFO 16329 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1978 ms  
  24. 2017-12-31 02:28:41.980  INFO 16329 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]  
  25. 2017-12-31 02:28:41.985  INFO 16329 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]  
  26. 2017-12-31 02:28:41.986  INFO 16329 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]  
  27. 2017-12-31 02:28:41.987  INFO 16329 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]  
  28. 2017-12-31 02:28:41.987  INFO 16329 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]  
  29. 2017-12-31 02:28:42.390  INFO 16329 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@58a90037: startup date [Sun Dec 31 02:28:39 CST 2017]; root of context hierarchy  
  30. 2017-12-31 02:28:42.476  INFO 16329 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/index]}" onto public java.lang.String springbootext.controller.TestController.index()  
  31. 2017-12-31 02:28:42.482  INFO 16329 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)  
  32. 2017-12-31 02:28:42.483  INFO 16329 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)  
  33. 2017-12-31 02:28:42.526  INFO 16329 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]  
  34. 2017-12-31 02:28:42.526  INFO 16329 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]  
  35. 2017-12-31 02:28:42.581  INFO 16329 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]  
  36. 2017-12-31 02:28:42.786  INFO 16329 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup  
  37. 2017-12-31 02:28:42.852  INFO 16329 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 7668 (http)  
  38. 2017-12-31 02:28:42.858  INFO 16329 --- [           main] s.r.MySpringApplicationRunListener       : ===============finished  
  39. 2017-12-31 02:28:42.858  INFO 16329 --- [           main] springbootext.SBApplication              : Started SBApplication in 3.469 seconds (JVM running for 3.776)  
可以看到,在启动过程的不同时机,MySpringApplicationRunListener会执行不同的方法,并得到与当前时机息息相关的参数,因此可以在不同时机执行一些不同的操作。

5.2 ApplicationListener

如5.1所述,SpringBoot框架内置了一个EventPublishingRunListener用以将定义在spring.factories中的ApplicationListener管理起来,并在SpringBoot应用启动的不同时机给他们发送不同的事件。
EventPublishingRunListener内部维护了一个ApplicationEventMulticaster,并将SpringApplication初始化时获得的ApplicationListeners注册进去:

[java]  view plain  copy
  1. private final ApplicationEventMulticaster initialMulticaster;  
  2. public EventPublishingRunListener(SpringApplication application, String[] args) {  
  3.     this.application = application;  
  4.     this.args = args;  
  5.     this.initialMulticaster = new SimpleApplicationEventMulticaster();  
  6.     for (ApplicationListener<?> listener : application.getListeners()) {  
  7.         this.initialMulticaster.addApplicationListener(listener);  
  8.     }  
  9. }  

然后在SpringBoot执行的不同时机被调用不同方法时,将通知封装成不同类型的ApplicationEvent并广播给注册的ApplicationListener:
[java]  view plain  copy
  1. @Override  
  2. public void started() {  
  3.     this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(  
  4.         this.application, this.args));  
  5. }  
  6.   
  7. @Override  
  8. public void environmentPrepared(ConfigurableEnvironment environment) {  
  9.     this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(  
  10.         this.application, this.args, environment));  
  11. }  
  12.   
  13. @Override  
  14. public void contextPrepared(ConfigurableApplicationContext context) {  
  15.   
  16. }  
  17.   
  18. @Override  
  19. public void contextLoaded(ConfigurableApplicationContext context) {  
  20.     for (ApplicationListener<?> listener : this.application.getListeners()) {  
  21.         if (listener instanceof ApplicationContextAware) {  
  22.             ((ApplicationContextAware) listener).setApplicationContext(context);  
  23.         }  
  24.         context.addApplicationListener(listener);  
  25.     }  
  26.     this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(  
  27.         this.application, this.args, context));  
  28. }  
  29.   
  30. @Override  
  31. public void finished(ConfigurableApplicationContext context, Throwable exception) {  
  32.     // Listeners have been registered to the application context so we should  
  33.     // use it at this point  
  34.     context.publishEvent(getFinishedEvent(context, exception));  
  35. }  

注意:contextLoaded通知发出时,EventPublishingRunListener会将SpringApplication中的ApplicationListeners统统注册到context中。而在finished通知发出时,直接通知的是context中的所有ApplicationListeners。
我们也可以自定义一个实现了ApplicationListener接口的类,然后通过

[java]  view plain  copy
  1. springApplication.addListeners(new MyApplicationListener());  
或者在spring.factories中添加:
[java]  view plain  copy
  1. org.springframework.context.ApplicationListener=\  
  2.     springbootext.listener.MyApplicationListener  
来将其注册进EventPublishingRunListener中,参与SpringBoot启动过程生命周期。

5.3 ApplicationContextInitializer

ApplicationContextInitializer是Spring context原有的概念,其目的就是在ApplicationContext准备好之后且refresh之前执行,可以对ApplicationContext做进一步的维护。其执行时机是在SpringApplication run阶段的prepareContext时期调用applyInitializers函数来实现的,代码如下:
[java]  view plain  copy
  1. protected void applyInitializers(ConfigurableApplicationContext context) {  
  2.     for (ApplicationContextInitializer initializer : getInitializers()) {  
  3.         Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(  
  4.             initializer.getClass(), ApplicationContextInitializer.class);  
  5.         Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");  
  6.         initializer.initialize(context);  
  7.     }  
  8. }  
其中getInitializers()得到的就是在initialize阶段借助SpringFactoriesLoader工具来获取的所有initializers,详情请翻阅4.1小节。
我们也可以自定义一个实现了ApplicationContextInitializer接口的类,然后通过

[java]  view plain  copy
  1. springApplication.addInitializers(new MyInitializer());  
或者在spring.factories中添加:
[java]  view plain  copy
  1. org.springframework.context.ApplicationContextInitializer=\  
  2.     springbootext.initializer.MyInitializer  
来将其注册进SpringBoot的启动流程中。

5.4 CommandLineRunner和ApplicationRunner

这两个runner如此相似,就将他们放在一起说明了。CommandLineRunner和ApplicationRunner接口是在容器启动成功后的最后一步回调,可以认为是挂载了一些开机自启动程序。其执行时机在run的afterRefresh阶段:
[java]  view plain  copy
  1. protected void afterRefresh(ConfigurableApplicationContext context,  
  2.     ApplicationArguments args) {  
  3.     callRunners(context, args);  
  4. }  
  5. private void callRunners(ApplicationContext context, ApplicationArguments args) {  
  6.     List<Object> runners = new ArrayList<Object>();  
  7.     runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());  
  8.     runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());  
  9.     AnnotationAwareOrderComparator.sort(runners);  
  10.     for (Object runner : new LinkedHashSet<Object>(runners)) {  
  11.         if (runner instanceof ApplicationRunner) {  
  12.             callRunner((ApplicationRunner) runner, args);  
  13.         }  
  14.         if (runner instanceof CommandLineRunner) {  
  15.             callRunner((CommandLineRunner) runner, args);  
  16.         }  
  17.     }  
  18. }  
这些runner无法感知applicationContext或者是environment等,只能获得启动应用时候的命令行参数。
由于其执行时机是在applicationContext的refresh完成以后,因此其获取是通过applicationContext.getBeansOfType来得到的,也就是说所有注册在applicationContext(也就是其中的beanFactory)中的CommandLineRunner或ApplicationRunner类型的bean都会被执行。
所以我们可以通过在Configuration类中定义@Bean方法将自定义的Runner注册进来。
两者的执行时机是完全一样的,唯一的区别在于一个接受ApplicationArguments,一个接受String[]类型的原始命令行参数。而ApplicationArguments也只是对原始命令行参数的一个封装,因此本质上是一样的。

6 spring-boot-starter实现原理

SpringBoot中的starter是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要引入starter jar包,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置。
starter让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。
SpringBoot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。SpringBoot提供的starter包括了:

[plain]  view plain  copy
  1. spring-boot-starter  
  2. spring-boot-starter-logging  
  3. spring-boot-starter-web  
  4. spring-boot-starter-jdbc  
  5. spring-boot-starter-aop  
  6. spring-boot-starter-secutiry  
  7. ......  
等等,有数十个之多。由于这是一篇介绍实现原理而非介绍使用教程的文章,因此不会过多的介绍各个starter的具体功能或用法,想查看具体教程的同学请参考https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples
下面通过一个最常用的web应用场景使用的spring-boot-starter-web来介绍一下starter到底干了什么,以及是如何干的;然后自定义一个starter。

6.1 spring-boot-starter-web详解

打开spring-boot-starter-web-1.5.8.RELEASE.jar看一下,发现结构如下:

其中META-INF中并没有定义spring.factories文件,回顾前面介绍的@EnableAutoConfiguration注释的功能,其实这些SpringBoot官方提供的starter的配置类是在spring-boot-autoconfigure模块中注册的,而starter本身只通过pom.xml来定义其依赖的类库,因此其中唯一起作用的就是pom.xml,spring-boot-starter-web相应的pom.xml内容如下:
[html]  view plain  copy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">  
  3.     <modelVersion>4.0.0</modelVersion>  
  4.     <parent>  
  5.         <groupId>org.springframework.boot</groupId>  
  6.         <artifactId>spring-boot-starters</artifactId>  
  7.         <version>1.5.8.RELEASE</version>  
  8.     </parent>  
  9.     <artifactId>spring-boot-starter-web</artifactId>  
  10.     <name>Spring Boot Web Starter</name>  
  11.     <description>Starter for building web, including RESTful, applications using Spring  
  12.         MVC. Uses Tomcat as the default embedded container</description>  
  13.     <url>http://projects.spring.io/spring-boot/</url>  
  14.     <organization>  
  15.         <name>Pivotal Software, Inc.</name>  
  16.         <url>http://www.spring.io</url>  
  17.     </organization>  
  18.     <properties>  
  19.         <main.basedir>${basedir}/../..</main.basedir>  
  20.     </properties>  
  21.     <dependencies>  
  22.         <dependency>  
  23.             <groupId>org.springframework.boot</groupId>  
  24.             <artifactId>spring-boot-starter</artifactId>  
  25.         </dependency>  
  26.         <dependency>  
  27.             <groupId>org.springframework.boot</groupId>  
  28.             <artifactId>spring-boot-starter-tomcat</artifactId>  
  29.         </dependency>  
  30.         <dependency>  
  31.             <groupId>org.hibernate</groupId>  
  32.             <artifactId>hibernate-validator</artifactId>  
  33.         </dependency>  
  34.         <dependency>  
  35.             <groupId>com.fasterxml.jackson.core</groupId>  
  36.             <artifactId>jackson-databind</artifactId>  
  37.         </dependency>  
  38.         <dependency>  
  39.             <groupId>org.springframework</groupId>  
  40.             <artifactId>spring-web</artifactId>  
  41.         </dependency>  
  42.         <dependency>  
  43.             <groupId>org.springframework</groupId>  
  44.             <artifactId>spring-webmvc</artifactId>  
  45.         </dependency>  
  46.     </dependencies>  
  47. </project>  
里面定义了一个web应用需要依赖的类库,其中通过spring-boot-starters来管理具体的版本,这些版本经过了精心挑选配置,可以认为是经过了充分测试可避免冲突的。
从上可得,这些starters只是定义了各个场景需要依赖的jar包,而真正做自动化配置的代码的是在spring-boot-autoconfigure里面。SpringBoot会基于你的classpath中的jar包,试图猜测和配置您可能需要的bean。
我们通过在前面介绍的spring-boot-autoconfig包中定义的META-INF/spring.factories中可看到,与web相关的自动配置类有:

[plain]  view plain  copy
  1. org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration,\  
  2. org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration,\  
  3. org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,\  
  4. org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration,\  
  5. org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration,\  
  6. org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\  
  7. org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\  
  8. org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,\  
  9. org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\  
其中WebMvcAutoConfiguration就是对于web应用环境的自动配置主类,其注释信息如下:
[java]  view plain  copy
  1. @Configuration  
  2. @ConditionalOnWebApplication  
  3. @ConditionalOnClass({ Servlet.class, DispatcherServlet.class,  
  4.     WebMvcConfigurerAdapter.class })  
  5. @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)  
  6. @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)  
  7. @AutoConfigureAfter(DispatcherServletAutoConfiguration.class)  
  8. public class WebMvcAutoConfiguration{  
  9.     ...............  
  10. }  


内部注册了一系列必要的ViewResolver、Converter、Formatter、Filter等等。

其中@AutoConfigureAfter代表需要在DispatcherServletAutoConfiguration之后加载,因此,在此之前需要先加载DispatcherServletAutoConfiguration,其注释信息如下:
[java]  view plain  copy
  1. @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
  2. @Configuration  
  3. @ConditionalOnWebApplication  
  4. @ConditionalOnClass(DispatcherServlet.class)  
  5. @AutoConfigureAfter(EmbeddedServletContainerAutoConfiguration.class)  
  6. public class DispatcherServletAutoConfiguration {  
  7.     ......  
  8. }  

DispatcherServletAutoConfiguration内部通过@EnableConfigurationProperties/@ConfigurationProperties机制使用属性设置DispatcherServlet并注册进IOC容器,然后注册一个ServletRegistrationBean到IOC容器。DispatcherServlet是springframework中web模块的概念,是http请求分发路由的核心枢纽。ServletRegistrationBean是SpringBoot中用来在Servlet容器中注册servlet用的,其作用类似于ServletContext.addServlet(String, Servlet),但是用了更加友好的Spring bean配置注册方式。
在该类的注释中同样使用@AutoConfigureAfter来指示加载本类之前先加载EmbeddedServletContainerAutoConfiguration配置类,其代码如下:

[java]  view plain  copy
  1. @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
  2. @Configuration  
  3. @ConditionalOnWebApplication  
  4. @Import(BeanPostProcessorsRegistrar.class)  
  5. public class EmbeddedServletContainerAutoConfiguration {  
  6.     .........  
  7. }  

EmbeddedServletContainerAutoConfiguration首先判断是否web应用,若是才会继续。其作用是根据不同的条件(通过判断拥有哪些类来判断引入了哪个包)注册EmbeddedServletContainerCustomizer的具体实现(例如:TomcatEmbeddedServletContainerFactory和JettyEmbeddedServletContainerFactory等),然后通过Import机制为beanFactory注册embeddedServletContainerCustomizerBeanPostProcessor和errorPageRegistrarBeanPostProcessor这两个BeanDefinition。
这两个BeanPostProcessor会在EmbeddedServletContainerCustomizer类型的bean被实例化之后、初始化之前以后对其进行进一步的操作。
EmbeddedServletContainerCustomizerBeanPostProcessor会利用已注册在beanFactory中的EmbeddedServletContainerCustomizers来对ConfigurableEmbeddedServletContainer进行定制:

[java]  view plain  copy
  1. @Override  
  2. public Object postProcessBeforeInitialization(Object bean, String beanName)  
  3.     throws BeansException {  
  4.     if (bean instanceof ConfigurableEmbeddedServletContainer) {  
  5.         postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);  
  6.     }  
  7.     return bean;  
  8. }  
  9. private void postProcessBeforeInitialization(  
  10.     ConfigurableEmbeddedServletContainer bean) {  
  11.     for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {  
  12.         customizer.customize(bean);  
  13.     }  
  14. }  

例如其中有一个名为ServerProperties的customizer,其本身通过标注@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)来将自身属性设置为以server.开头配置的属性,然后其customize函数代码如下:
[java]  view plain  copy
  1. public void customize(ConfigurableEmbeddedServletContainer container) {  
  2.     if (getPort() != null) {  
  3.         container.setPort(getPort());  
  4.     }  
  5.     if (getAddress() != null) {  
  6.         container.setAddress(getAddress());  
  7.     }  
  8.     if (getContextPath() != null) {  
  9.         container.setContextPath(getContextPath());  
  10.     }  
  11.     if (getDisplayName() != null) {  
  12.         container.setDisplayName(getDisplayName());  
  13.     }  
  14.     if (getSession().getTimeout() != null) {  
  15.         container.setSessionTimeout(getSession().getTimeout());  
  16.     }  
  17.     container.setPersistSession(getSession().isPersistent());  
  18.     container.setSessionStoreDir(getSession().getStoreDir());  
  19.     if (getSsl() != null) {  
  20.         container.setSsl(getSsl());  
  21.     }  
  22.     if (getJspServlet() != null) {  
  23.         container.setJspServlet(getJspServlet());  
  24.     }  
  25.     if (getCompression() != null) {  
  26.         container.setCompression(getCompression());  
  27.     }  
  28.     container.setServerHeader(getServerHeader());  
  29.     if (container instanceof TomcatEmbeddedServletContainerFactory) {  
  30.         getTomcat().customizeTomcat(this,  
  31.             (TomcatEmbeddedServletContainerFactory) container);  
  32.     }  
  33.     if (container instanceof JettyEmbeddedServletContainerFactory) {  
  34.         getJetty().customizeJetty(this,  
  35.             (JettyEmbeddedServletContainerFactory) container);  
  36.     }  
  37.   
  38.     if (container instanceof UndertowEmbeddedServletContainerFactory) {  
  39.         getUndertow().customizeUndertow(this,  
  40.             (UndertowEmbeddedServletContainerFactory) container);  
  41.     }  
  42.     container.addInitializers(new SessionConfiguringInitializer(this.session));  
  43.     container.addInitializers(new InitParameterConfiguringServletContextInitializer(  
  44.         getContextParameters()));  
  45. }  
也就是说,该customizer负责将配置的属性设置进container中。

errorPageRegistrarBeanPostProcessor会利用已注册在beanFactory中的ErrorPageRegistrar来对ConfigurableEmbeddedServletContainer进行定制:
[java]  view plain  copy
  1. private void postProcessBeforeInitialization(ErrorPageRegistry registry) {  
  2.     for (ErrorPageRegistrar registrar : getRegistrars()) {  
  3.         registrar.registerErrorPages(registry);  
  4.     }  
  5. }  
默认注册了一个ErrorPageCustomizer,使用属性中的servletPath与error.path一起构建一个ErrorPage并注册到ConfigurableEmbeddedServletContainer中:
[java]  view plain  copy
  1. public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {  
  2.     ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()  
  3.         + this.properties.getError().getPath());  
  4.     errorPageRegistry.addErrorPages(errorPage);  
  5. }  

除此之外还有:
[plain]  view plain  copy
  1. org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,\  
  2. org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration,\  
  3. org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration,\  
  4. org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\  
  5. org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\  
  6. org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,\  
等等,它们都在一定条件下起作用,并各自实现特定的配置。
当我们引入spring-boot-starter-web时,上面的启动条件会满足,因此这些配置类都会被加载生效。于是对于一个web应用,默认就配置完成了。

6.2 自定义starter

SpringBoot官方提供的starter提供了两层有价值的信息:所需依赖的jar包相互之间使用不产生冲突的版本;在spring-boot-autoconfiguration库中提供了对其进行默认配置的配置类并利用Spring框架的扩展协议实现注册。
因此,当我们想要自定义一个starter,以实现一旦在pom.xml中引入该starter就能自动工作的效果时,我们需要确保两件事:
  • 仔细确定依赖的各个库的jar包的版本,保证它们之间不会产生冲突,或无法合作。
  • 提供对所需机制的Configuration类,以实现将需要的功能注入IOC容器。并通过SpringBoot的扩展机制确保引入自定义starter的jar包时Configuration类被加载。
具体需要完成以下几步:
1. 编写pom.xml在其中定义我们的场景需要依赖的jar包,并仔细选择其版本
2. 项目中编写@Configuration配置类以将必要的功能注册进IOC容器
3. 在META-INF/spring.factories中添加

[plain]  view plain  copy
  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
  2. mypackage1.MyConfiguration1,\  
  3. mypackage2.MyConfiguration2,\  
  4. .........  
来应用Spring的内部扩展机制注册Configuration类。
4. 打包成jar文件以供使用。
如此一来,每当该starter jar包被引入时,SpringBoot的@EnableAutoConfiguration注释便会借助SpringFactoriesLoader将我们配置的Configuration类加载,并对其进行解析,我们的starter就可以工作起来了。

7 自动配置机制实现原理

7.1 SpringFactoriesLoader

SpringFactoriesLoader是SpringFramework内部使用的通用的工厂加载机制,其可加载并实例化可能出现在classpath上的多个jar包中的META-INF/spring.factories文件中定义的指定类型的工厂。
factories文件必须使用Properties格式,也就是多行key=value组成的格式。其中key是指定工厂的抽象类或接口的全限定名,value是用逗号隔开的key指定工厂的具体实现类的全限定名。例如:

[plain]  view plain  copy
  1. example.MyService=example.MyServiceImpl1,example.MyServiceImpl2  
其中example.MyService是接口的全限定名,而MyServiceImpl1和MyServiceImpl2是其实现类。
这样配置完后,我们就可以使用SpringFactoriesLoader.loadFactories(Myservice.class, null)来获得MyServiceImpl1和MyServiceImpl2的实例。后续如何处理MyServiceImpl1和MyServiceImpl1,就看各个扩展机制的实际业务逻辑了。

7.2 EnableAutoConfiguration

EnableAutoConfiguration是SpringBoot利用SpringFactoriesLoader扩展机制来实现的一种自动配置机制。
EnableAutoConfiguration注释包含两层功能:第一是作为注释,通过Import机制实现标注在类上以表示开启自动配置机制。第二是作为SpringBoot的autoconfiguration模块利用SpringFactoriesLoader扩展的“工厂类”。
在autoconfiguration模块的META-INF/spring.factories中定义了:

[plain]  view plain  copy
  1. # Auto Configure  
  2. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
  3. org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\  
  4. org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\  
  5. org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\  
  6. org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\  
  7. org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\  
  8. org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\  
  9. org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\  
  10. .......  
这些配置,在启动时通过启动类上标注的@EnableAutoConfiguration注释中Import的AutoConfigurationImportSelector,来委托SpringFactoriesLoader加载进IOC容器,并在满足各自配置的@Condition条件时生效。

8 属性配置说明

8.1 SpringBoot属性配置顺序

Spring Boot支持多种外部属性源配置方式,并使用非常特别的属性源优先级顺序:
1. 命令行参数
2. 来自java:comp/env的JNDI属性
3. Java系统属性(System.getProperties())
4. 操作系统环境变量
5. RandomValuePropertySource配置的random.*属性值
6. jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件
7. jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件
8. jar包外部的application.properties或application.yml(不带sprin.profile)配置文件
9. jar包内部的application.properties或application.yml(不带spring.profile)配置文件
10. @Configuration注解类上的@PropertySource
11. 通过SpringApplication.setDefaultProperties指定的默认属性
具体使用说明,请参照文档https://docs.spring.io/spring-boot/docs/1.2.3.RELEASE/reference/html/boot-features-external-config.html

8.2 属性值配置/获取方式

SpringBoot为我们提供的获得属性值的方式有以下几种,其中有Springframework本身就提供了的,也有SpringBoot自己扩展出来的方式:


1. environment.getProperty(String key)

任何我们可以获得Environment对象的地方,都可以直接使用这个函数来获得key所定义的属性值。key是类似home.me.name或server.port等等这样的字符串,例如:
[java]  view plain  copy
  1. environment.getProperty("home.me.name");  
  2. environment.getProperty("server.port");  


2. environment.resolvePlaceholders(String placeholder)

同上,能获得environment对象的地方,都可以使用该函数。所不同的是,其参数为placeholder,也就是${home.me.name}或${server.port}并且支持嵌套表达,例如:
[java]  view plain  copy
  1. environment.resolverPlaceholders("${home.me.name}");  
  2. environment.resolverPlaceholders("${home.me.${test.name}}");  


3. @Value

当我们在一个Bean中的某属性上定义@Value("home.me.name")时,IOC容器在创建这个Bean的时候就会将对应的属性值注入(Inject)进去,例如:
[java]  view plain  copy
  1. @Component  
  2. public class SBConfiguration {  
  3. @Value("${home.me.name:hello}")  
  4. <span style="white-space:pre;"> </span>String name;  
  5. <span style="white-space:pre;"> </span>......  
  6. }  


4. @ConfigurationProperties("home.me")

当我们在一个配置类上标注@ConfigurationProperties时,IOC容器会在创建这个配置类时自动为其注入其字段对应的属性(若存在的话)。这是SpringBoot新引入的获取机制。
[java]  view plain  copy
  1. @Configuration  
  2. @ConfigurationProperties("home.me")  
  3. public class SBConfiguration {  
  4.     String name;  
  5.     ......  
  6. }  

所有这些获取方式都最终都会将实际的属性获取行为委托给spring-core模块中的PropertySourcesPropertyResolver:
[java]  view plain  copy
  1. protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {  
  2.     if (this.propertySources != null) {  
  3.         for (PropertySource<?> propertySource : this.propertySources) {  
  4.             if (logger.isTraceEnabled()) {  
  5.                 logger.trace("Searching for key '" + key + "' in PropertySource '" +  
  6.                     propertySource.getName() + "'");  
  7.             }  
  8.             Object value = propertySource.getProperty(key);  
  9.             if (value != null) {  
  10.                 if (resolveNestedPlaceholders && value instanceof String) {  
  11.                     value = resolveNestedPlaceholders((String) value);  
  12.                 }  
  13.                 logKeyFound(key, propertySource, value);  
  14.                 return convertValueIfNecessary(value, targetValueType);  
  15.             }  
  16.         }  
  17.     }  
  18.     if (logger.isDebugEnabled()) {  
  19.         logger.debug("Could not find key '" + key + "' in any property source");  
  20.     }  
  21.     return null;  
  22. }  

其中的this.propertySources是从environment中获取过来的。上述代码的意思就是遍历environment中预定义的propertySources,查找属性key对应的值,如有需要递归处理嵌套属性,一旦得到非空的属性值,就返回。因此对于多个propertySources来源而言,排在前面的优先级就高过排在后面的优先级。

8.3 属性源注册顺序

从8.2属性获取及绑定过程可知,属性源优先级顺序其实就取决于属性源在environment中注册完毕后的最终顺序,SpringBoot在启动阶段控制这这个属性源的加载及排序,我们跟踪一下启动过程:


1. 由于我们配置的是web应用,因此初始化的environment类型是StandardServletEnvironment。在StandardServletEnvironment初始化的时候,会执行:
[java]  view plain  copy
  1. protected void customizePropertySources(MutablePropertySources propertySources) {  
  2.     propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));  
  3.     propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));  
  4.     if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {  
  5.         propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));  
  6.     }  
  7.     super.customizePropertySources(propertySources);  
  8. }  
StandardServletEnvironment的父类是StandardEnvironment,其customizePropertySources方法代码如下:
[java]  view plain  copy
  1. protected void customizePropertySources(MutablePropertySources propertySources) {  
  2.     propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));  
  3.     propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,  
  4.         getSystemEnvironment()));  
  5. }  

此时propertySources的顺序为:

[plain]  view plain  copy
  1. servletConfigInitParams  
  2. servletContextInitParams  
  3. jndiProperties  
  4. systemProperties  
  5. systemEnvironment  

2. 然后在configEnvironment阶段:
[java]  view plain  copy
  1. protected void configureEnvironment(ConfigurableEnvironment environment,  
  2.     String[] args) {  
  3.     configurePropertySources(environment, args);  
  4.     configureProfiles(environment, args);  
  5. }  
其中configurePropertySources用于配置propertySources,为environment增加defaultProperties和commandLineArgs:
[java]  view plain  copy
  1. protected void configurePropertySources(ConfigurableEnvironment environment,  
  2.     String[] args) {  
  3.     MutablePropertySources sources = environment.getPropertySources();  
  4.     if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {  
  5.         sources.addLast(  
  6.             new MapPropertySource("defaultProperties"this.defaultProperties));  
  7.     }  
  8.     if (this.addCommandLineProperties && args.length > 0) {  
  9.         String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;  
  10.         if (sources.contains(name)) {  
  11.             PropertySource<?> source = sources.get(name);  
  12.             CompositePropertySource composite = new CompositePropertySource(name);  
  13.             composite.addPropertySource(new SimpleCommandLinePropertySource(  
  14.                 name + "-" + args.hashCode(), args));  
  15.             composite.addPropertySource(source);  
  16.             sources.replace(name, composite);  
  17.         }  
  18.         else {  
  19.             sources.addFirst(new SimpleCommandLinePropertySource(args));  
  20.         }  
  21.     }  
  22. }  
区别是defaultProperties加到末尾,而commandLineArgs添加到第一个。此时propertySources的顺序如下:

[plain]  view plain  copy
  1. commandLineArgs  
  2. servletConfigInitParams  
  3. servletContextInitParams  
  4. jndiProperties  
  5. systemProperties  
  6. systemEnvironment  
  7. defaultProperties  

3. 在发出environmentPrepared事件后,ConfigFileApplicationListener执行回调函数:
[java]  view plain  copy
  1. public void postProcessEnvironment(ConfigurableEnvironment environment,  
  2.     SpringApplication application) {  
  3.     addPropertySources(environment, application.getResourceLoader());  
  4.     configureIgnoreBeanInfo(environment);  
  5.     bindToSpringApplication(environment, application);  
  6. }  
其中addPropertySources方法首先将RandomValuePropertySource(名为random)注册进environment:
[java]  view plain  copy
  1. protected void addPropertySources(ConfigurableEnvironment environment,  
  2.     ResourceLoader resourceLoader) {  
  3.     RandomValuePropertySource.addToEnvironment(environment);  
  4.     new Loader(environment, resourceLoader).load();  
  5. }  
RandomValuePropertySource加载的位置如下:
[java]  view plain  copy
  1. public static void addToEnvironment(ConfigurableEnvironment environment) {  
  2.     environment.getPropertySources().addAfter(  
  3.         StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,  
  4.         new RandomValuePropertySource(RANDOM_PROPERTY_SOURCE_NAME));  
  5.     logger.trace("RandomValuePropertySource add to Environment");  
  6. }  
可看到是加在systemEnvironment之后。
然后委托Loader来搜索searchLocations路径上的可用的配置文件。默认搜索顺序是file:./config/, file:./, classpath:/config/, classpath:./。我们可以通过设置"spring.config.location"来修改默认搜索路径。
结合environment当前设置的profile,在上述路径中查询特定文件名(默认为"application",可通过设置"spring.config.name"来指定)和特定后缀(properties, xml, yml, yaml)的文件。将所有符合条件的文件封装为不同类型的PropertySource(例如PropertiesPropertySource)。最终将这些PropertySources封装成一个ConfigurationPropertySources(名为:applicationConfigurationProperties)然后添加在如下位置:

[java]  view plain  copy
  1. private void addConfigurationProperties(  
  2.     ConfigurationPropertySources configurationSources) {  
  3.     MutablePropertySources existingSources = this.environment  
  4.         .getPropertySources();  
  5.     if (existingSources.contains(DEFAULT_PROPERTIES)) {  
  6.         existingSources.addBefore(DEFAULT_PROPERTIES, configurationSources);  
  7.     }  
  8.     else {  
  9.         existingSources.addLast(configurationSources);  
  10.     }  
  11. }  
如果存在defaultProperties,则将该applicationConfigurationProperties添加到defaultProperties之前;否则添加到existingSources的最末尾。如此一来,propertySources的顺序如下:

[plain]  view plain  copy
  1. commandLineArgs  
  2. servletConfigInitParams  
  3. servletContextInitParams  
  4. jndiProperties  
  5. systemProperties  
  6. systemEnvironment  
  7. random  
  8. applicationConfigurationProperties  
  9. defaultProperties  

4. 随后,在context.refresh阶段的invokeBeanFactoryPostProcessors(beanFactory)子阶段,会执行到ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry函数。其中委托ConfigurationClassParser来解析已经注册到beanDefinitionFactory中的@Configuration类,其中调用到doProcessConfigurationClass方法来获得配置在@Configuration类上的propertySource:
[java]  view plain  copy
  1. for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(  
  2.     sourceClass.getMetadata(), PropertySources.class,  
  3.     org.springframework.context.annotation.PropertySource.class)) {  
  4.     if (this.environment instanceof ConfigurableEnvironment) {  
  5.         processPropertySource(propertySource);  
  6.     }  
  7.     else {  
  8.         logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +  
  9.             "]. Reason: Environment must implement ConfigurableEnvironment");  
  10.     }  
  11. }  
针对每一个打了PropertySource注释的配置类,执行processPropertySource方法,代码如下:
[java]  view plain  copy
  1. private void processPropertySource(AnnotationAttributes propertySource) throws IOException {  
  2.     String name = propertySource.getString("name");  
  3.     if (!StringUtils.hasLength(name)) {  
  4.         name = null;  
  5.     }  
  6.     String encoding = propertySource.getString("encoding");  
  7.     if (!StringUtils.hasLength(encoding)) {  
  8.         encoding = null;  
  9.     }  
  10.     String[] locations = propertySource.getStringArray("value");  
  11.     Assert.isTrue(locations.length > 0"At least one @PropertySource(value) location is required");  
  12.     boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");  
  13.   
  14.     Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");  
  15.     PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?  
  16.         DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));  
  17.   
  18.     for (String location : locations) {  
  19.         try {  
  20.             String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);  
  21.             Resource resource = this.resourceLoader.getResource(resolvedLocation);  
  22.             addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));  
  23.         }  
  24.         catch (IllegalArgumentException ex) {  
  25.             // Placeholders not resolvable  
  26.             if (ignoreResourceNotFound) {  
  27.                 if (logger.isInfoEnabled()) {  
  28.                     logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());  
  29.                 }  
  30.             }  
  31.             else {  
  32.                 throw ex;  
  33.             }  
  34.         }  
  35.         catch (IOException ex) {  
  36.             // Resource not found when trying to open it  
  37.             if (ignoreResourceNotFound &&  
  38.                 (ex instanceof FileNotFoundException || ex instanceof UnknownHostException)) {  
  39.                 if (logger.isInfoEnabled()) {  
  40.                     logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());  
  41.                 }  
  42.             }  
  43.             else {  
  44.                 throw ex;  
  45.             }  
  46.         }  
  47.     }  
  48. }  

其功能是获得PropertySource注释的value属性定义的逗号分割的多个locations,为每一个都创建ResourcePropertySource并对其执行addPropertySource方法:
[java]  view plain  copy
  1. private void addPropertySource(PropertySource<?> propertySource) {  
  2.     String name = propertySource.getName();  
  3.     MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();  
  4.     if (propertySources.contains(name) && this.propertySourceNames.contains(name)) {  
  5.         // We've already added a version, we need to extend it  
  6.         PropertySource<?> existing = propertySources.get(name);  
  7.         PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?  
  8.             ((ResourcePropertySource) propertySource).withResourceName() : propertySource);  
  9.         if (existing instanceof CompositePropertySource) {  
  10.             ((CompositePropertySource) existing).addFirstPropertySource(newSource);  
  11.         }  
  12.         else {  
  13.             if (existing instanceof ResourcePropertySource) {  
  14.                 existing = ((ResourcePropertySource) existing).withResourceName();  
  15.             }  
  16.             CompositePropertySource composite = new CompositePropertySource(name);  
  17.             composite.addPropertySource(newSource);  
  18.             composite.addPropertySource(existing);  
  19.             propertySources.replace(name, composite);  
  20.         }  
  21.     }  
  22.     else {  
  23.         if (this.propertySourceNames.isEmpty()) {  
  24.             propertySources.addLast(propertySource);  
  25.         }  
  26.         else {  
  27.             String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);  
  28.             propertySources.addBefore(firstProcessed, propertySource);  
  29.         }  
  30.     }  
  31.     this.propertySourceNames.add(name);  
  32. }  

addPropertySource函数主要用来确定ResourcePropertySource的添加次序:
  • 如果propertySources已经添加了相同名称的propertySource并且本实例维护的propertySourceNames包含本名称,则说明已经注册过一个相同名称的用@PropertySource标注的属性源,因此需要对已有的属性源进行扩展。将新增加的propertySource插入进去。
  • 否则,如果本实例维护的propertySourceNames为空,说明在此之前并没有注册任何通过@PropertySource标注的属性源,则将该属性源添加到propertySources的最后,也就是优先级最低的位置。
  • 若本实例维护的propertySourceNames不为空,则说明在此之前已经注册过通过@PropertySource标注的属性源,则将本属性源添加到上一次添加的属性源之前。
添加完以后顺序如下:

[plain]  view plain  copy
  1. commandLineArgs  
  2. servletConfigInitParams  
  3. servletContextInitParams  
  4. jndiProperties  
  5. systemProperties  
  6. systemEnvironment  
  7. random  
  8. applicationConfigurationProperties  
  9. defaultProperties  
  10. ResourcePropertySource(若干个)  


5. 跟着会由PropertySourceOrderingPostProcessor执行postProcessBeanFactory:

[java]  view plain  copy
  1. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)  
  2.     throws BeansException {  
  3.     reorderSources(this.context.getEnvironment());  
  4. }  
其中对context.getEnvironment()中的propertySources进行重新排序:
[java]  view plain  copy
  1. private void reorderSources(ConfigurableEnvironment environment) {  
  2.     ConfigurationPropertySources  
  3.         .finishAndRelocate(environment.getPropertySources());  
  4.     PropertySource<?> defaultProperties = environment.getPropertySources()  
  5.         .remove(DEFAULT_PROPERTIES);  
  6.     if (defaultProperties != null) {  
  7.         environment.getPropertySources().addLast(defaultProperties);  
  8.     }  
  9. }  
finishAndRelocate函数中将名为"applicationConfigurationProperties"的ConfigurationPropertySources在原位置展开:原来将路径上的多个配置文件生成的propertySource集成成一个ConfigurationPropertySources,现在将其展开成不同的PropertySource。
然后将"defaultProperties"删除,如果"defaultProperties"不为空,那么就将其添加到队列的最后,也就是优先级最低的地方。重新排序后最终propertySources的顺序如下:

[plain]  view plain  copy
  1. commandLineArgs  
  2. servletConfigInitParams  
  3. servletContextInitParams  
  4. jndiProperties  
  5. systemProperties  
  6. systemEnvironment  
  7. random  
  8. PropertiesPropertySource(若干)  
  9. ResourcePropertySource(若干)  
  10. defaultProperties  


经过上述的整个过程以后,environment中参数源列表(propertySources)的排序与前面文档中提到的顺序就完全一致了。




小结:SpringBoot使得开发Spring应用不再那么繁琐和冗长,由于其快速开发、约定大于配置的自动配置模块、可执行jar包部署等等特点,随着微服务的流行变得越来越重要。本文详细介绍了SpringBoot框架的内部实现原理,通过源码分析了解了其自动配置、扩展机制、参数获取顺序等等核心机制。在实际使用SpringBoot框架开发时,对于各种功能知其然更应知其所以然,才能做到游刃有余。

猜你喜欢

转载自blog.csdn.net/lldouble/article/details/80703797