Spring Boot 完整学习指南

在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。Spring Boot 应用本质上就是一个基于 Spring 框架的应用,它是 Spring 对“约定优先于配置”理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于 Spring 生态圈的应用。

那 Spring Boot 有何魔法?自动配置、起步依赖、Actuator、命令行界面(CLI) 是Spring Boot 最重要的 4 大核心特性,本文将为你打开 Spring Boot 的大门,重点为你剖析其启动流程以及自动配置实现原理。

一、抛砖引玉:探索 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 底层是如何工作的:

Spring IoC 容器的整个工作流程大致可以分为两个阶段:

①、容器启动阶段

容器启动时,会通过某种途径加载 ConfigurationMetaData。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,来看一个简单的例子吧,过往,所有的 bean 都定义在 XML 配置文件中,下面的代码将模拟 BeanFactory 如何从配置文件中加载 bean 的定义以及依赖关系:

②、Bean 的实例化阶段

经过第一阶段,所有bean定义都通过BeanDefinition的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。

在实际场景下,我们更多的使用另外一种类型的容器: 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 定义中的属性值就已经被替换成我们配置的值。

跟BeanFactoryPostProcessor 类似,它会处理容器内所有符合条件并且已经实例化后的对象。简单的对比,BeanFactoryPostProcessor 处理 bean的定义,而BeanPostProcessor 则处理 bean 完成实例化后的对象。BeanPostProcessor 定义了两个接口:

为了理解这两个方法执行的时机,简单的了解下 bean 的整个生命周期:

Bean的实例化过程(来自:Spring揭秘)

postProcessBeforeInitialization()方法与 postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。再来看一个更常见的例子,在 Spring 中经常能够看到各种各样的 Aware 接口,其作用就是在对象实例化完成以后将 Aware 接口定义中规定的依赖注入到当前实例中。

比如最常见的 ApplicationContextAware 接口,实现了这个接口的类都可以获取到一个 ApplicationContext 对象。当容器中每个对象的实例化过程走到 BeanPostProcessor 前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor,然后就会调用其 postProcessBeforeInitialization()方法,检查并设置 Aware 相关依赖。看看代码吧,是不是很简单:

理解这部分内容,足以让您轻松理解 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 的配置形式是这样的:

如果两个 bean 之间有依赖关系的话,在 XML 配置中应该是这样:

而在 JavaConfig 中则是这样:

你可能注意到这个示例中,有两个 bean 都依赖于 dependencyService,也就是说当初始化 bookService 时会调用 dependencyService(),在初始化 otherService 时也会调用 dependencyService()。

2.2、@ComponentScan

@ComponentScan 注解对应 XML 配置形式中的 <context:component-scan>元素,表示启用组件扫描,Spring 会自动扫描所有通过注解配置的 bean,然后将其注册到 IOC 容器中。我们可以通过 basePackages 等属性来指定 @ComponentScan 自动扫描的范围,如果不指定,默认从声明 @ComponentScan 所在类的 package 进行扫描。正因为如此,SpringBoot 的启动类都默认在 src/main/java 下。

2.3、@Import

@Import 注解用于导入配置类,举个简单的例子:

现在有另外一个配置类,比如: MoonUserConfiguration,这个配置类中有一个bean 依赖于 MoonBookConfiguration 中的 bookService,如何将这两个 bean 组合在一起?借助 @Import 即可:

需要注意的是,在 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 类时才生效:

当你用 Java 来声明 bean 的时候,可以使用这个自定义条件类:

这个例子中只有当 JdbcTemplateCondition 类的条件成立时才会创建 MyService 这个 bean。也就是说 MyService 这 bean 的创建条件是 classpath 里面包含 JdbcTemplate,否则这个 bean 的声明就会被忽略掉。

SpringBoot 定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了 SpringBoot 的自动配置的基础。 SpringBoot 运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了 SpringBoot 提供的部分条件化注解:

2.5、@ConfigurationProperties 与@EnableConfigurationProperties

当某些属性的值需要配置的时候,我们一般会在 application.properties 文件中新建配置项,然后在 bean 中使用 @Value 注解来获取配置的值,比如下面配置数据源的代码。

使用 @Value 注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot 提供了更优雅的实现方式,那就是 @ConfigurationProperties 注解。我们可以通过下面的方式来改写上面的代码:

@ConfigurationProperties 对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件:

可以定义如下配置类来接收这些属性

@EnableConfigurationProperties 注解表示对 @ConfigurationProperties 的内嵌支持,默认会将对应 Properties Class 作为 bean 注入的 IOC 容器中,即在相应的 Properties 类上不用加 @Component 注解。

三、削铁如泥:SpringFactoriesLoader 详解

JVM 提供了 3 种类加载器: BootstrapClassLoader、 ExtClassLoader、 AppClassLoader 分别加载 Java 核心类库、扩展类库以及应用的类路径( CLASSPATH)下的类库。JVM通过双亲委派模型进行类的加载,我们也可以通过继承 java.lang.classloader 实现自己的类加载器。

何为双亲委派模型?当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的 BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。

采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证 Java 核心库的类型安全。查看 ClassLoader 的源码,对双亲委派模型会有更直观的认识:

但双亲委派模型并不能解决所有的类加载器问题,比如,Java 提供了很多服务提供者接口,允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些 SPI 的接口由核心类库提供,却由第三方实现,这样就存在一个问题:

SPI 的接口是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的;SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。

线程上下文类加载器( ContextClassLoader)正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。

线程上下文类加载器在很多 SPI 的实现中都会用到。但在 JDBC 中,你可能会看到一种更直接的实现方式,比如,JDBC 驱动管理 java.sql.Driver 中的 loadInitialDrivers()方法中,你可以直接看到 JDK 是如何加载驱动的:

其实讲解线程上下文类加载器,最主要是让大家在看到 Thread.currentThread().getClassLoader()和 Thread.currentThread().getContextClassLoader()时不会一脸懵逼,这两者除了在许多底层框架中取得的 ClassLoader 可能会有所不同外,其他大多数业务场景下都是一样的,大家只要知道它是为了解决什么问题而存在的即可。

类加载器除了加载 class 外,还有一个非常重要功能,就是加载资源,它可以从jar包中读取任何资源文件,比如, ClassLoader.getResources(Stringname)方法就是用于读取 jar 包中的资源文件,其代码如下:

是不是觉得有点眼熟,不错,它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到 BootstrapClassLoader,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的 jar 包,就如同加载 class 一样,最后会扫描所有的 jar 包,找到符合条件的资源文件。

类加载器的 findResources(name)方法会遍历其负责加载的所有 jar 包,找到 jar 包中名称为 name 的资源文件,这里的资源可以是任何文件,甚至是 .clas 文件,比如下面的示例,用于查找 Array.class 文件:

运行后可以得到如下结果:

根据资源文件的 URL,可以构造相应的文件来读取资源内容。

看到这里,你可能会感到挺奇怪的,你不是要详解 SpringFactoriesLoader 吗?上来讲了一堆 ClassLoader 是几个意思?看下它的源码你就知道了:

有了前面关于 ClassLoader 的知识,再来理解这段代码,是不是感觉豁然开朗:从 CLASSPATH 下的每个 Jar 包中搜寻所有 META-INF/spring.factories 配置文件,然后将解析 properties 文件,找到指定名称的配置后返回。

需要注意的是,其实这里不仅仅是会去 ClassPath 路径下查找,会扫描所有路径下的 Jar 包,只不过这个文件只会在 Classpath 下的 jar 包中。来简单看下 spring.factories 文件的内容吧:

执行 loadFactoryNames(EnableAutoConfiguration.class,classLoader)后,得到对应的一组 @Configuration 类,我们就可以通过反射实例化这些类然后注入到 IOC 容器中,最后容器里就有了一系列标注了 @Configuration 的 JavaConfig 形式的配置类。

这就是 Spring Boot 启动流程的上半部分,其核心就是在 Spring 容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener 以及各种 BeanFactoryPostProcessor 等。

四、另一件武器:Spring 容器的事件监听机制

过去,事件监听机制多用于图形界面编程,比如:点击按钮、在文本框输入内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件,而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。

Java提供了实现事件监听机制的两个基础类:自定义事件类型扩展自 java.util.EventObject、事件的监听器扩展自 java.util.EventListener。

五、启动引导:Spring Boot 应用启动的秘密

5.1 SpringApplication 初始化

SpringBoot 整个启动流程分为两个步骤:初始化一个 SpringApplication 对象、执行该对象的 run 方法。看下 SpringApplication 的初始化流程, SpringApplication 的构造方法中调用 initialize(Object[] sources)方法,其代码如下:

5.2 Spring Boot 启动流程

Spring Boot 应用的整个启动流程都封装在 SpringApplication.run 方法中,其整个流程真的是太长太长了,但本质上就是在 Spring 容器启动的基础上做了大量的扩展,按照这个思路来看看源码:

这就是 Spring Boot 的整个启动流程,其核心就是在 Spring 容器初始化并启动的基础上加入各种扩展点

原文发布于微信公众号 - GitChat精品课(CSDN_Tech)

原文发表时间:2018-12-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券