springboot中接口实例化_疫情爆发在家闲出屁的我,梳理一下SpringBoot知识点

在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架。或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用。因而Spring Boot应用本质上就是一个基于Spring框架的应用,它是Spring对“约定优先于配置”理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于Spring生态圈的应用。
那Spring Boot有何魔法?自动配置、起步依赖、Actuator、命令行界面(CLI) 是Spring Boot最重要的4大核心特性,其中CLI是Spring Boot的可选特性,虽然它功能强大,但也引入了一套不太常规的开发模型,因而这个系列的文章仅关注其它3种特性。如文章标题,本文是这个系列的第一部分,将为你打开Spring Boot的大门,重点为你剖析其启动流程以及自动配置实现原理。要掌握这部分核心内容,理解一些Spring框架的基础知识,将会让你事半功倍。
一、抛砖引玉:探索Spring IoC容器
如果有看过 SpringApplication.run()方法的源码,Spring Boot冗长无比的启动流程一定会让你抓狂,透过现象看本质,SpringApplication只是将一个典型的Spring应用的启动流程进行了扩展,因此,透彻理解Spring容器是打开Spring Boot大门的一把钥匙。
1.1、Spring IoC容器
可以把Spring IoC容器比作一间餐馆,当你来到餐馆,通常会直接招呼服务员:点菜!至于菜的原料是什么?如何用原料把菜做出来?可能你根本就不关心。IoC容器也是一样,你只需要告诉它需要某个bean,它就把对应的实例(instance)扔给你,至于这个bean是否依赖其他组件,怎样完成它的初始化,根本就不需要你关心。
作为餐馆,想要做出菜肴,得知道菜的原料和菜谱,同样地,IoC容器想要管理各个业务对象以及它们之间的依赖关系,需要通过某种途径来记录和管理这些信息。BeanDefinition对象就承担了这个责任:容器中的每一个bean都会有一个对应的BeanDefinition实例,该实例负责保存bean对象的所有必要信息,包括bean对象的class类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的bean实例。
原材料已经准备好(把BeanDefinition看着原料),开始做菜吧,等等,你还需要一份菜谱, BeanDefinitionRegistry和 BeanFactory就是这份菜谱,BeanDefinitionRegistry抽象出bean的注册逻辑,而BeanFactory则抽象出了bean的管理逻辑,而各个BeanFactory的实现类就具体承担了bean的注册以及管理工作。它们之间的关系就如下图:

BeanFactory、BeanDefinitionRegistry关系图(来自:Spring揭秘)
DefaultListableBeanFactory作为一个比较通用的BeanFactory实现,它同时也实现了BeanDefinitionRegistry接口,因此它就承担了Bean的注册管理工作。从图中也可以看出,BeanFactory接口中主要包含getBean、containBean、getType、getAliases等管理bean的方法,而BeanDefinitionRegistry接口则包含registerBeanDefinition、removeBeanDefinition、getBeanDefinition等注册管理BeanDefinition的方法。
下面通过一段简单的代码来模拟BeanFactory底层是如何工作的:
// 默认容器实现DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();// 根据业务对象构造相应的BeanDefinitionAbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true);// 将bean定义注册到容器中beanRegistry.registerBeanDefinition("beanName",definition);// 如果有多个bean,还可以指定各个bean之间的依赖关系// ........// 然后可以从容器中获取这个bean的实例// 注意:这里的beanRegistry其实实现了BeanFactory接口,所以可以强转,// 单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的BeanFactory container = (BeanFactory)beanRegistry;Business business = (Business)container.getBean("beanName");
这段代码仅为了说明BeanFactory底层的大致工作流程,实际情况会更加复杂,比如bean之间的依赖关系可能定义在外部配置文件(XML/Properties)中、也可能是注解方式。Spring IoC容器的整个工作流程大致可以分为两个阶段:
①、容器启动阶段
容器启动时,会通过某种途径加载 ConfigurationMetaData。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如:BeanDefinitionReader,BeanDefinitionReader会对加载的 ConfigurationMetaData进行解析和分析,并将分析后的信息组装为相应的BeanDefinition,最后把这些保存了bean定义的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于bean对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。
来看一个简单的例子吧,过往,所有的bean都定义在XML配置文件中,下面的代码将模拟BeanFactory如何从配置文件中加载bean的定义以及依赖关系:
// 通常为BeanDefinitionRegistry的实现类,这里以DeFaultListabeBeanFactory为例BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory();// XmlBeanDefinitionReader实现了BeanDefinitionReader接口,用于解析XML文件XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry);// 加载配置文件beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml");// 从容器中获取bean实例BeanFactory container = (BeanFactory)beanRegistry;Business business = (Business)container.getBean("beanName");
②、Bean的实例化阶段
经过第一阶段,所有bean定义都通过BeanDefinition的方式注册到BeanDefinitionRegistry中,当某个请求通过容器的getBean方法请求某个对象,或者因为依赖关系容器需要隐式的调用getBean时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。
BeanFactory只是Spring IoC容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器:ApplicationContext,它构建在BeanFactory之上,属于更高级的容器,除了具有BeanFactory的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的bean,在容器启动时全部完成初始化和依赖注入操作。
1.2、Spring容器扩展机制
IoC容器负责管理容器中所有bean的生命周期,而在bean生命周期的不同阶段,Spring提供了不同的扩展点来改变bean的命运。在容器的启动阶段, BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作,比如修改bean定义的某些属性或者增加其他信息等。
如果要自定义扩展类,通常需要实现 org.springframework.beans.factory.config.BeanFactoryPostProcessor接口,与此同时,因为容器中可能有多个BeanFactoryPostProcessor,可能还需要实现 org.springframework.core.Ordered接口,以保证BeanFactoryPostProcessor按照顺序执行。Spring提供了为数不多的BeanFactoryPostProcessor实现,我们以 PropertyPlaceholderConfigurer来说明其大致的工作流程。
在Spring项目的XML配置文件中,经常可以看到许多配置项的值使用占位符,而将占位符所代表的值单独配置到独立的properties文件,这样可以将散落在不同XML文件中的配置集中管理,而且也方便运维根据不同的环境进行配置不同的值。这个非常实用的功能就是由PropertyPlaceholderConfigurer负责实现的。
根据前文,当BeanFactory在第一阶段加载完所有配置信息时,BeanFactory中保存的对象的属性还是以占位符方式存在的,比如 ${jdbc.mysql.url}。当PropertyPlaceholderConfigurer作为BeanFactoryPostProcessor被应用时,它会使用properties配置文件中的值来替换相应的BeanDefinition中占位符所表示的属性值。当需要实例化bean时,bean定义中的属性值就已经被替换成我们配置的值。当然其实现比上面描述的要复杂一些,这里仅说明其大致工作原理,更详细的实现可以参考其源码。
与之相似的,还有 BeanPostProcessor,其存在于对象实例化阶段。跟BeanFactoryPostProcessor类似,它会处理容器内所有符合条件并且已经实例化后的对象。简单的对比,BeanFactoryPostProcessor处理bean的定义,而BeanPostProcessor则处理bean完成实例化后的对象。BeanPostProcessor定义了两个接口:
public interface BeanPostProcessor { // 前置处理 Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; // 后置处理 Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;}
为了理解这两个方法执行的时机,简单的了解下bean的整个生命周期:

Bean的实例化过程(来自:Spring揭秘)
postProcessBeforeInitialization()方法与 postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了bean对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。注解、AOP等功能的实现均大量使用了 BeanPostProcessor,比如有一个自定义注解,你完全可以实现BeanPostProcessor的接口,在其中判断bean对象的脑袋上是否有该注解,如果有,你可以对这个bean实例执行任何操作,想想是不是非常的简单?
再来看一个更常见的例子,在Spring中经常能够看到各种各样的Aware接口,其作用就是在对象实例化完成以后将Aware接口定义中规定的依赖注入到当前实例中。比如最常见的 ApplicationContextAware接口,实现了这个接口的类都可以获取到一个ApplicationContext对象。当容器中每个对象的实例化过程走到BeanPostProcessor前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor,然后就会调用其postProcessBeforeInitialization()方法,检查并设置Aware相关依赖。看看代码吧,是不是很简单:
// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法private void invokeAwareInterfaces(Object bean) { if (bean instanceof EnvironmentAware) { ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); } // ......}
最后总结一下,本小节内容和你一起回顾了Spring容器的部分核心内容,限于篇幅不能写更多,但理解这部分内容,足以让您轻松理解Spring Boot的启动原理,如果在后续的学习过程中遇到一些晦涩难懂的知识,再回过头来看看Spring的核心知识,也许有意想不到的效果。也许Spring Boot的中文资料很少,但Spring的中文资料和书籍有太多太多,总有东西能给你启发。
二、夯实基础:JavaConfig与常见Annotation
2.1、JavaConfig
我们知道 bean是Spring IOC中非常核心的概念,Spring容器负责bean的生命周期的管理。在最初,Spring使用XML配置文件的方式来描述bean的定义以及相互间的依赖关系,但随着Spring的发展,越来越多的人对这种方式表示不满,因为Spring项目的所有业务类均以bean的形式配置在XML文件中,造成了大量的XML文件,使项目变得复杂且难以管理。
后来,基于纯Java Annotation依赖注入框架 Guice出世,其性能明显优于采用XML方式的Spring,甚至有部分人认为, Guice可以完全取代Spring( Guice仅是一个轻量级IOC框架,取代Spring还差的挺远)。正是这样的危机感,促使Spring及社区推出并持续完善了 JavaConfig子项目,它基于Java代码和Annotation注解来描述bean之间的依赖绑定关系。比如,下面是使用XML配置方式来描述bean的定义:
而基于JavaConfig的配置形式是这样的:
@Configurationpublic class MoonBookConfiguration { // 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中 // 方法名默认成为该bean定义的id @Bean public BookService bookService() { return new BookServiceImpl(); }}
如果两个bean之间有依赖关系的话,在XML配置中应该是这样:
而在JavaConfig中则是这样:
@Configurationpublic class MoonBookConfiguration { // 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可 // 这里直接调用dependencyService() @Bean public BookService bookService() { return new BookServiceImpl(dependencyService()); } @Bean public OtherService otherService() { return new OtherServiceImpl(dependencyService()); } @Bean public DependencyService dependencyService() { return new DependencyServiceImpl(); }}
你可能注意到这个示例中,有两个bean都依赖于dependencyService,也就是说当初始化bookService时会调用 dependencyService(),在初始化otherService时也会调用 dependencyService(),那么问题来了?这时候IOC容器中是有一个dependencyService实例还是两个?这个问题留着大家思考吧,这里不再赘述。
2.2、@ComponentScan
@ComponentScan注解对应XML配置形式中的
2.3、@Import
@Import注解用于导入配置类,举个简单的例子:
@Configurationpublic class MoonBookConfiguration { @Bean public BookService bookService() { return new BookServiceImpl(); }}
现在有另外一个配置类,比如:MoonUserConfiguration,这个配置类中有一个bean依赖于 MoonBookConfiguration中的bookService,如何将这两个bean组合在一起?借助 @Import即可:
@Configuration// 可以同时导入多个配置类,比如:@Import({A.class,B.class})@Import(MoonBookConfiguration.class)public class MoonUserConfiguration { @Bean public UserService userService(BookService bookService) { return new BookServiceImpl(bookService); }}
需要注意的是,在4.2之前, @Import注解只支持导入配置类,但是在4.2之后,它支持导入普通类,并将这个类作为一个bean的定义注册到IOC容器中。
2.4、@Conditional
@Conditional注解表示在满足某种条件后才初始化一个bean或者启用某些配置。它一般用在由 @Component、 @Service、 @Configuration等注解标识的类上面,或者由 @Bean标记的方法上。如果一个 @Configuration类标记了 @Conditional,则该类中所有标识了 @Bean的方法和 @Import注解导入的相关类将遵从这些条件。
在Spring里可以很方便的编写你自己的条件类,所要做的就是实现 Condition接口,并覆盖它的 matches()方法。举个例子,下面的简单条件类表示只有在 Classpath里存在 JdbcTemplate类时才生效:
public class JdbcTemplateCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { try { conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate"); return true; } catch (ClassNotFoundException e) { e.printStackTrace(); } return false; }}
当你用Java来声明bean的时候,可以使用这个自定义条件类:
@Conditional(JdbcTemplateCondition.class)@Servicepublic MyService service() { ......}
这个例子中只有当 JdbcTemplateCondition类的条件成立时才会创建MyService这个bean。也就是说MyService这bean的创建条件是 classpath里面包含 JdbcTemplate,否则这个bean的声明就会被忽略掉。
SpringBoot定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了 SpringBoot的自动配置的基础。SpringBoot运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了 SpringBoot提供的部分条件化注解:
条件化注解配置生效条件@ConditionalOnBean配置了某个特定bean@ConditionalOnMissingBean没有配置特定的bean@ConditionalOnClassClasspath里有指定的类@ConditionalOnMissingClassClasspath里没有指定的类@ConditionalOnExpression给定的Spring Expression Language表达式计算结果为true@ConditionalOnJavaJava的版本匹配特定指或者一个范围值@ConditionalOnProperty指定的配置属性要有一个明确的值@ConditionalOnResourceClasspath里有指定的资源@ConditionalOnWebApplication这是一个Web应用程序@ConditionalOnNotWebApplication这不是一个Web应用程序
2.5、@ConfigurationProperties与@EnableConfigurationProperties
当某些属性的值需要配置的时候,我们一般会在 application.properties文件中新建配置项,然后在bean中使用 @Value注解来获取配置的值,比如下面配置数据源的代码。
// jdbc configjdbc.mysql.url=jdbc:mysql://localhost:3306/sampledbjdbc.mysql.username=rootjdbc.mysql.password=123456......// 配置数据源@Configurationpublic class HikariDataSourceConfiguration { @Value("jdbc.mysql.url") public String url; @Value("jdbc.mysql.username") public String user; @Value("jdbc.mysql.password") public String password; @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(url); hikariConfig.setUsername(user); hikariConfig.setPassword(password); // 省略部分代码 return new HikariDataSource(hikariConfig); }}
使用 @Value注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot提供了更优雅的实现方式,那就是 @ConfigurationProperties注解。我们可以通过下面的方式来改写上面的代码:
@Component// 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件@ConfigurationProperties("jdbc.mysql")// 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项pulic class JdbcConfig { public String url; public String username; public String password;}@Configurationpublic class HikariDataSourceConfiguration { @AutoWired public JdbcConfig config; @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(config.url); hikariConfig.setUsername(config.username); hikariConfig.setPassword(config.password); // 省略部分代码 return new HikariDataSource(hikariConfig); }}
@ConfigurationProperties对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件:
#Appapp.menus[0].title=Homeapp.menus[0].name=Homeapp.menus[0].path=/app.menus[1].title=Loginapp.menus[1].name=Loginapp.menus[1].path=/loginapp.compiler.timeout=5app.compiler.output-folder=/temp/app.error=/error/
可以定义如下配置类来接收这些属性
@Component@ConfigurationProperties("app")public class AppProperties { public String error; public List
- 在合适的时机发布事件。此例中的methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布MethodMonitorEvent事件,每个时间点发布的事件都会传给相应的监听器进行处理。在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。
- 事件监听器的管理。publisher类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。如果这里没有提供remove方法,那么注册的监听器示例将一直被MethodMonitorEventPublisher引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。
Spring容器内的事件监听机制
Spring的ApplicationContext容器内部中的所有事件类型均继承自 org.springframework.context.AppliationEvent,容器中的所有监听器都实现 org.springframework.context.ApplicationListener接口,并且以bean的形式注册在容器中。一旦在容器内发布ApplicationEvent及其子类型的事件,注册到容器的ApplicationListener就会对这些事件进行处理。
你应该已经猜到是怎么回事了。
ApplicationEvent继承自EventObject,Spring提供了一些默认的实现,比如:ContextClosedEvent表示容器在即将关闭时发布的事件类型, ContextRefreshedEvent表示容器在初始化或者刷新的时候发布的事件类型......
容器内部使用ApplicationListener作为事件监听器接口定义,它继承自EventListener。ApplicationContext容器在启动时,会自动识别并加载EventListener类型的bean,一旦容器内有事件发布,将通知这些注册到容器的EventListener。
ApplicationContext接口继承了ApplicationEventPublisher接口,该接口提供了 voidpublishEvent(ApplicationEventevent)方法定义,不难看出,ApplicationContext容器担当的就是事件发布者的角色。如果有兴趣可以查看 AbstractApplicationContext.publishEvent(ApplicationEventevent)方法的源码:ApplicationContext将事件的发布以及监听器的管理工作委托给 ApplicationEventMulticaster接口的实现类。在容器启动时,会检查容器内是否存在名为applicationEventMulticaster的ApplicationEventMulticaster对象实例。如果有就使用其提供的实现,没有就默认初始化一个SimpleApplicationEventMulticaster作为实现。
最后,如果我们业务需要在容器内部发布事件,只需要为其注入ApplicationEventPublisher依赖即可:实现ApplicationEventPublisherAware接口或者ApplicationContextAware接口(Aware接口相关内容请回顾上文)。
五、出神入化:揭秘自动配置原理
典型的Spring Boot应用的启动类一般均位于 src/main/java根路径下,比如 MoonApplication类:
@SpringBootApplicationpublic class MoonApplication { public static void main(String[] args) { SpringApplication.run(MoonApplication.class, args); }}
其中 @SpringBootApplication开启组件扫描和自动配置,而 SpringApplication.run则负责启动引导应用程序。@SpringBootApplication是一个复合 Annotation,它将三个有用的注解组合在一起:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication { // ......}
@SpringBootConfiguration就是 @Configuration,它是Spring框架的注解,标明该类是一个 JavaConfig配置类。而 @ComponentScan启用组件扫描,前文已经详细讲解过,这里着重关注 @EnableAutoConfiguration。
@EnableAutoConfiguration注解表示开启Spring Boot自动配置功能,Spring Boot会根据应用的依赖、自定义的bean、classpath下有没有某个类 等等因素来猜测你需要的bean,然后注册到IOC容器中。那 @EnableAutoConfiguration是如何推算出你的需求?首先看下它的定义:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(EnableAutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration { // ......}
你的关注点应该在 @Import(EnableAutoConfigurationImportSelector.class)上了,前文说过, @Import注解用于导入类,并将这个类作为一个bean的定义注册到容器中,这里它将把 EnableAutoConfigurationImportSelector作为bean注入到容器中,而这个类会将所有符合条件的@Configuration配置都加载到容器中,看看它的代码:
public String[] selectImports(AnnotationMetadata annotationMetadata) { // 省略了大部分代码,保留一句核心代码 // 注意:SpringBoot最近版本中,这句代码被封装在一个单独的方法中 // SpringFactoriesLoader相关知识请参考前文 List
这个类会扫描所有的jar包,将所有符合条件的@Configuration配置类注入的容器中,何为符合条件,看看 META-INF/spring.factories的文件内容:
// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories// 配置的key = EnableAutoConfiguration,与代码中一致org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.....
以 DataSourceAutoConfiguration为例,看看Spring Boot是如何自动配置的:
@Configuration@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })@EnableConfigurationProperties(DataSourceProperties.class)@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })public class DataSourceAutoConfiguration {}
分别说一说:
- @ConditionalOnClass({DataSource.class,EmbeddedDatabaseType.class}):当Classpath中存在DataSource或者EmbeddedDatabaseType类时才启用这个配置,否则这个配置将被忽略。
- @EnableConfigurationProperties(DataSourceProperties.class):将DataSource的默认配置类注入到IOC容器中,DataSourceproperties定义为:
// 提供对datasource配置信息的支持,所有的配置前缀为:spring.datasource@ConfigurationProperties(prefix = "spring.datasource")public class DataSourceProperties { private ClassLoader classLoader; private Environment environment; private String name = "testdb"; ......}
- @Import({Registrar.class,DataSourcePoolMetadataProvidersConfiguration.class}):导入其他额外的配置,就以 DataSourcePoolMetadataProvidersConfiguration为例吧。
@Configurationpublic class DataSourcePoolMetadataProvidersConfiguration { @Configuration @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class) static class TomcatDataSourcePoolMetadataProviderConfiguration { @Bean public DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() { ..... } } ......}
DataSourcePoolMetadataProvidersConfiguration是数据库连接池提供者的一个配置类,即Classpath中存在 org.apache.tomcat.jdbc.pool.DataSource.class,则使用tomcat-jdbc连接池,如果Classpath中存在 HikariDataSource.class则使用Hikari连接池。
这里仅描述了DataSourceAutoConfiguration的冰山一角,但足以说明Spring Boot如何利用条件话配置来实现自动配置的。回顾一下, @EnableAutoConfiguration中导入了EnableAutoConfigurationImportSelector类,而这个类的 selectImports()通过SpringFactoriesLoader得到了大量的配置类,而每一个配置类则根据条件化配置来做出决策,以实现自动配置。
整个流程很清晰,但漏了一个大问题:EnableAutoConfigurationImportSelector.selectImports()是何时执行的?其实这个方法会在容器启动过程中执行:AbstractApplicationContext.refresh(),更多的细节在下一小节中说明。
六、启动引导:Spring Boot应用启动的秘密
6.1 SpringApplication初始化
SpringBoot整个启动流程分为两个步骤:初始化一个SpringApplication对象、执行该对象的run方法。看下SpringApplication的初始化流程,SpringApplication的构造方法中调用initialize(Object[] sources)方法,其代码如下:
private void initialize(Object[] sources) { if (sources != null && sources.length > 0) { this.sources.addAll(Arrays.asList(sources)); } // 判断是否是Web项目 this.webEnvironment = deduceWebEnvironment(); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 找到入口类 this.mainApplicationClass = deduceMainApplicationClass();}
初始化流程中最重要的就是通过SpringFactoriesLoader找到 spring.factories文件中配置的ApplicationContextInitializer和 ApplicationListener两个接口的实现类名称,以便后期构造相应的实例。ApplicationContextInitializer的主要目的是在 ConfigurableApplicationContext做refresh之前,对ConfigurableApplicationContext实例做进一步的设置或处理。ConfigurableApplicationContext继承自ApplicationContext,其主要提供了对ApplicationContext进行设置的能力。
实现一个ApplicationContextInitializer非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个ApplicationContextInitializer,即便是Spring Boot框架,它默认也只是注册了两个实现,毕竟Spring的容器已经非常成熟和稳定,你没有必要来改变它。
而 ApplicationListener的目的就没什么好说的了,它是Spring框架对Java事件监听机制的一种框架实现,具体内容在前文Spring事件监听机制这个小节有详细讲解。这里主要说说,如果你想为Spring Boot应用添加监听器,该如何实现?
Spring Boot提供两种方式来添加自定义监听器:
- 通过 SpringApplication.addListeners(ApplicationListener>...listeners)或者 SpringApplication.setListeners(Collection>listeners)两个方法来添加一个或者多个自定义监听器
- 既然SpringApplication的初始化流程中已经从 spring.factories中获取到 ApplicationListener的实现类,那么我们直接在自己的jar包的 META-INF/spring.factories文件中新增配置即可:
org.springframework.context.ApplicationListener=cn.moondev.listeners.xxxxListener
关于SpringApplication的初始化,我们就说这么多。
6.2 Spring Boot启动流程
Spring Boot应用的整个启动流程都封装在SpringApplication.run方法中,其整个流程真的是太长太长了,但本质上就是在Spring容器启动的基础上做了大量的扩展,按照这个思路来看看源码:
public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; FailureAnalyzers analyzers = null; configureHeadlessProperty(); // ① SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { // ② ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments); // ③ Banner printedBanner = printBanner(environment); // ④ context = createApplicationContext(); // ⑤ analyzers = new FailureAnalyzers(context); // ⑥ prepareContext(context, environment, listeners, applicationArguments,printedBanner); // ⑦ refreshContext(context); // ⑧ afterRefresh(context, applicationArguments); // ⑨ listeners.finished(context, null); stopWatch.stop(); return context; } catch (Throwable ex) { handleRunFailure(context, listeners, analyzers, ex); throw new IllegalStateException(ex); } }
① 通过SpringFactoriesLoader查找并加载所有的 SpringApplicationRunListeners,通过调用starting()方法通知所有的SpringApplicationRunListeners:应用开始启动了。SpringApplicationRunListeners其本质上就是一个事件发布者,它在SpringBoot应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication加载了一系列ApplicationListener吗?这个启动流程中没有发现有发布事件的代码,其实都已经在SpringApplicationRunListeners这儿实现了。
简单的分析一下其实现流程,首先看下SpringApplicationRunListener的源码:
public interface SpringApplicationRunListener { // 运行run方法时立即调用此方法,可以用户非常早期的初始化工作 void starting(); // Environment准备好后,并且ApplicationContext创建之前调用 void environmentPrepared(ConfigurableEnvironment environment); // ApplicationContext创建好后立即调用 void contextPrepared(ConfigurableApplicationContext context); // ApplicationContext加载完成,在refresh之前调用 void contextLoaded(ConfigurableApplicationContext context); // 当run方法结束之前调用 void finished(ConfigurableApplicationContext context, Throwable exception);}
SpringApplicationRunListener只有一个实现类:EventPublishingRunListener。①处的代码只会获取到一个EventPublishingRunListener的实例,我们来看看starting()方法的内容:
public void starting() { // 发布一个ApplicationStartedEvent this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));}
顺着这个逻辑,你可以在②处的 prepareEnvironment()方法的源码中找到 listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二个方法,那不出你所料, environmentPrepared()又发布了另外一个事件 ApplicationEnvironmentPreparedEvent。接下来会发生什么,就不用我多说了吧。
② 创建并配置当前应用将要使用的 Environment,Environment用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当Environment准备好后,在整个应用的任何时候,都可以从Environment中获取资源。
总结起来,②处的两句代码,主要完成以下几件事:
- 判断Environment是否存在,不存在就创建(如果是web项目就创建 StandardServletEnvironment,否则创建 StandardEnvironment)
- 配置Environment:配置profile以及properties
- 调用SpringApplicationRunListener的 environmentPrepared()方法,通知事件监听者:应用的Environment已经准备好
③、SpringBoot应用在启动时会输出这样的东西:
. ____ _ __ _ _ / / ___'_ __ _ _(_)_ __ __ _ ( ( )___ | '_ | '_| | '_ / _` | / ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |___, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.6.RELEASE)
如果想把这个东西改成自己的涂鸦,你可以研究以下Banner的实现,这个任务就留给你们吧。
④、根据是否是web项目,来创建不同的ApplicationContext容器。
⑤、创建一系列 FailureAnalyzer,创建流程依然是通过SpringFactoriesLoader获取到所有实现FailureAnalyzer接口的class,然后在创建对应的实例。FailureAnalyzer用于分析故障并提供相关诊断信息。
⑥、初始化ApplicationContext,主要完成以下工作:
- 将准备好的Environment设置给ApplicationContext
- 遍历调用所有的ApplicationContextInitializer的 initialize()方法来对已经创建好的ApplicationContext进行进一步的处理
- 调用SpringApplicationRunListener的 contextPrepared()方法,通知所有的监听者:ApplicationContext已经准备完毕
- 将所有的bean加载到容器中
- 调用SpringApplicationRunListener的 contextLoaded()方法,通知所有的监听者:ApplicationContext已经装载完毕
⑦、调用ApplicationContext的 refresh()方法,完成IoC容器可用的最后一道工序。从名字上理解为刷新容器,那何为刷新?就是插手容器的启动,联系一下第一小节的内容。那如何刷新呢?且看下面代码:
// 摘自refresh()方法中一句代码invokeBeanFactoryPostProcessors(beanFactory);
看看这个方法的实现:
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); ......}
获取到所有的 BeanFactoryPostProcessor来对容器做一些额外的操作。BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作。这里的getBeanFactoryPostProcessors()方法可以获取到3个Processor:
ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessorSharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessorConfigFileApplicationListener$PropertySourceOrderingPostProcessor
不是有那么多BeanFactoryPostProcessor的实现类,为什么这儿只有这3个?因为在初始化流程获取到的各种ApplicationContextInitializer和ApplicationListener中,只有上文3个做了类似于如下操作:
public void initialize(ConfigurableApplicationContext context) { context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));}
然后你就可以进入到 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,这个方法除了会遍历上面的3个BeanFactoryPostProcessor处理外,还会获取类型为 BeanDefinitionRegistryPostProcessor的bean:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,对应的Class为 ConfigurationClassPostProcessor。ConfigurationClassPostProcessor用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理 @import注解的时候,就会调用<自动配置>这一小节中的 EnableAutoConfigurationImportSelector.selectImports()来完成自动配置功能。其他的这里不再多讲,如果你有兴趣,可以查阅参考资料6。
⑧、查找当前context中是否注册有CommandLineRunner和ApplicationRunner,如果有则遍历执行它们。
⑨、执行所有SpringApplicationRunListener的finished()方法。
这就是Spring Boot的整个启动流程,其核心就是在Spring容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener以及各种BeanFactoryPostProcessor等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。
整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring才是核心,理解清楚Spring容器的启动流程,那Spring Boot启动流程就不在话下了。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
