---
title: SpringBoot 知识图谱
date: 2020-08-12 07:01:26
order: 21
categories:
- Java
- 框架
- Spring
- Spring综合
tags:
- Java
- 框架
- Spring
- SpringBoot
permalink: /pages/fecda6b3/
---
# SpringBoot 知识图谱
> 1. 预警:本文非常长,建议先 mark 后看,也许是最后一次写这么长的文章
> 2. 说明:前面有 4 个小节关于 Spring 的基础知识,分别是:IOC 容器、JavaConfig、事件监听、SpringFactoriesLoader 详解,它们占据了本文的大部分内容,虽然它们之间可能没有太多的联系,但这些知识对于理解 Spring Boot 的核心原理至关重要,如果你对 Spring 框架烂熟于心,完全可以跳过这 4 个小节。正是因为这个系列的文章是由这些看似不相关的知识点组成,因此取名知识清单。
在过去两三年的 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();
// 根据业务对象构造相应的BeanDefinition
AbstractBeanDefinition 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 容器的整个工作流程大致可以分为两个阶段:
①、容器启动阶段
容器启动时,会通过某种途径加载`Configuration MetaData`。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如:`BeanDefinitionReader`,BeanDefinitionReader 会对加载的`Configuration MetaData`进行解析和分析,并将分析后的信息组装为相应的 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 的整个生命周期:
`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 的配置形式是这样的:
```
@Configuration
public class MoonBookConfiguration {
// 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中
// 方法名默认成为该bean定义的id
@Bean
public BookService bookService() {
return new BookServiceImpl();
}
}
```
如果两个 bean 之间有依赖关系的话,在 XML 配置中应该是这样:
```
```
而在 JavaConfig 中则是这样:
```
@Configuration
public 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 配置形式中的``元素,表示启用组件扫描,Spring 会自动扫描所有通过注解配置的 bean,然后将其注册到 IOC 容器中。我们可以通过`basePackages`等属性来指定`@ComponentScan`自动扫描的范围,如果不指定,默认从声明`@ComponentScan`所在类的`package`进行扫描。正因为如此,SpringBoot 的启动类都默认在`src/main/java`下。
### 2.3、@Import
`@Import`注解用于导入配置类,举个简单的例子:
```
@Configuration
public 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)
@Service
public MyService service() {
......
}
```
这个例子中只有当`JdbcTemplateCondition`类的条件成立时才会创建 MyService 这个 bean。也就是说 MyService 这 bean 的创建条件是`classpath`里面包含`JdbcTemplate`,否则这个 bean 的声明就会被忽略掉。
`Spring Boot`定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了`Spring Boot`的自动配置的基础。`Spring Boot`运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了`Spring Boot`提供的部分条件化注解:
| 条件化注解 | 配置生效条件 |
| ------------------------------- | ------------------------------------------------------- |
| @ConditionalOnBean | 配置了某个特定 bean |
| @ConditionalOnMissingBean | 没有配置特定的 bean |
| @ConditionalOnClass | Classpath 里有指定的类 |
| @ConditionalOnMissingClass | Classpath 里没有指定的类 |
| @ConditionalOnExpression | 给定的 Spring Expression Language 表达式计算结果为 true |
| @ConditionalOnJava | Java 的版本匹配特定指或者一个范围值 |
| @ConditionalOnProperty | 指定的配置属性要有一个明确的值 |
| @ConditionalOnResource | Classpath 里有指定的资源 |
| @ConditionalOnWebApplication | 这是一个 Web 应用程序 |
| @ConditionalOnNotWebApplication | 这不是一个 Web 应用程序 |
### 2.5、@ConfigurationProperties 与@EnableConfigurationProperties
当某些属性的值需要配置的时候,我们一般会在`application.properties`文件中新建配置项,然后在 bean 中使用`@Value`注解来获取配置的值,比如下面配置数据源的代码。
```
// jdbc config
jdbc.mysql.url=jdbc:mysql://localhost:3306/sampledb
jdbc.mysql.username=root
jdbc.mysql.password=123456
......
// 配置数据源
@Configuration
public 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;
}
@Configuration
public 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`对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件:
```
#App
app.menus[0].title=Home
app.menus[0].name=Home
app.menus[0].path=/
app.menus[1].title=Login
app.menus[1].name=Login
app.menus[1].path=/login
app.compiler.timeout=5
app.compiler.output-folder=/temp/
app.error=/error/
```
可以定义如下配置类来接收这些属性
```
@Component
@ConfigurationProperties("app")
public class AppProperties {
public String error;
public List