SpringBoot3 基础教程之九核心原理

大纲

依赖管理机制

为什么导入 spring-boot-starter-web 后,所有相关的依赖都导入进来了?

  • 开发什么场景,导入什么场景启动器
  • Maven 依赖的传递原则。A -> B -> C,那么 A 就拥有 B 和 C
  • 导入场景启动器后,会自动把这个场景的所有核心依赖全部导入进来

为什么版本号都不用写?

  • 每个 SpringBoot 项目都有一个父项目 spring-boot-starter-parent
  • parent 的父项目是 spring-boot-dependencies
  • 父项目是版本仲裁中心,会将所有常见 Jar 包的依赖版本都声明好了,比如 mysql-connector-j

如何自定义依赖的版本号?

  • 第一种方式:直接在当前 Maven 配置文件的 <properties></properties> 标签中声明父项目用的版本属性的 Key
  • 第二种方式:直接在导入依赖的时候声明版本
  • 上述两种方式都是利用 Maven 的就近原则特性

如何导入第三方的 Jar 包

  • 对于 spring-boot-starter-parent 没有管理的 Jar 包依赖,直接在 Maven 的配置文件中自行声明就可以

自动配置原理

SpringBoot 应用关注的三大核心:场景配置组件

自动配置流程

初步理解

  • 自动配置 SpringMVC、Tomcat 等
    • 导入场景,容器中就会自动配置好这个场景的核心组件
    • 以前:DispatcherServlet、ViewResolver、CharacterEncodingFilter ….
    • 现在:自动配置好的这些组件
    • 验证:容器中有了什么组件,就具有什么功能
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// JDK 10 的新特性,局部变量类型的自动推断
var ioc = SpringApplication.run(MainApplication.class, args);
// 获取容器中所有组件的名称
String[] names = ioc.getBeanDefinitionNames();
// 遍历容器中的所有组件,SpringBoot 把以前需要手动配置的核心组件现在都给自动配置好
for (String name : names) {
System.out.println(name);
}
}
}
  • 默认的包扫描规则

    • @SpringBootApplication 注解标注的类就是主程序类
    • SpringBoot 只会扫描主程序类所在的包及其下面的子包,即自动的 component-scan 功能
    • 自定义包的扫描路径
      • 第一种方式:@SpringBootApplication(scanBasePackages = "com.clay")
      • 第二种方式:@ComponentScan("com.clay") 直接指定扫描的包路径
  • 配置默认值

    • 配置文件的所有配置项是和某个类的对象值进行一一绑定的
    • 绑定了配置文件中每一项值的类,称为配置属性类
    • 比如:
      • ServerProperties 绑定了所有 Tomcat 服务器有关的配置
      • MultipartProperties 绑定了所有文件上传相关的配置
      • ….. 参照 官方文档 或者参照绑定的配置属性类
  • 按需加载自动配置

    • 导入场景启动器 spring-boot-starter-web
    • 场景启动器除了会导入相关功能的依赖,还会导入 spring-boot-starter,它是所有 starterstarter,是基础核心 starter
    • spring-boot-starter 导入了一个包 spring-boot-autoconfigure,里面都是各种场景的 AutoConfiguration 自动配置类
    • 虽然全场景的自动配置都在 spring-boot-autoconfigure 这个包中,但并不是全部都默认开启的,导入哪个场景启动器才会开启哪个场景的自动配置

总结

导入场景启动器会触发 spring-boot-autoconfigure 这个包的自动配置生效,Spring 的 IOC 容器中就会具有相关场景的功能。

进阶理解

思考以下问题

  • 1、SpringBoot 是怎么实现导一个 starter,写一些简单配置,开发者无需关心整合,应用就能跑起来的?
  • 2、为什么 Tomcat 的端口号可以配置在 application.properties 中,并且 Tomcat 能启动成功?
  • 3、导入场景启动器后,哪些自动配置能生效?

  • 导入 Web 开发场景 spring-boot-starter-web

    • 1、场景启动器导入了相关场景的所有依赖,如 spring-boot-starter-jsonspring-boot-starter-tomcatspring-webmvc
    • 2、每个场景启动器都引入了一个 spring-boot-starter,即核心场景启动器。
    • 3、核心场景启动器引入了 spring-boot-autoconfigure 包。
    • 4、spring-boot-autoconfigure 里面囊括了所有场景的自动配置。
    • 5、只要 spring-boot-autoconfigure 这个包下的所有类都能生效,那么相当于 SpringBoot 官方写好的整合功能就生效了。
    • 6、SpringBoot 默认是扫描不到 spring-boot-autoconfigure 下写好的所有配置类(这些配置类给我们做了整合操作),默认只扫描主程序类所在的包及其下面的子包。
  • 主程序注解 @SpringBootApplication

    • 1、@SpringBootApplication 由三个注解组成,分别是 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan
    • 2、SpringBoot 默认只能扫描自己主程序所在的包及其下面的子包,扫描不到 spring-boot-autoconfigure 包中官方写好的配置类。
    • 3、@EnableAutoConfiguration 是 SpringBoot 开启自动配置的核心。
      • 1、是由 @Import(AutoConfigurationImportSelector.class) 提供核心功能,批量往容器中导入组件。
      • 2、SpringBoot 启动时会默认加载 142 个配置类,这 142 个配置类是由 spring-boot-autoconfigure 包下的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件指定。
      • 3、项目启动的时候利用 @Import 批量导入组件的机制,将 spring-boot-autoconfigure 包下的 142 个 xxxxAutoConfiguration 类导入进来(自动配置类)。
      • 4、虽然导入了 142 个自动配置类,但并不是这 142 个自动配置类都能生效;每一个自动配置类,都有条件注解 @ConditionalOnxxx,只有条件成立才能生效。
  • xxxxAutoConfiguration 自动配置类

    • 1、往容器中使用 @Bean 注册一堆组件
    • 2、每个自动配置类都可能有这个注解 @EnableConfigurationProperties(ServerProperties.class),用来把配置文件中配的指定前缀的属性值封装到 xxxProperties 属性类中。
    • 3、以 Tomcat 为例,把服务器的所有配置都是以 server 开头的配置都封装到了属性类中。
    • 4、往容器中放的所有组件的一些核心参数,都来自于 xxxProperties 属性类,它都是和配置文件绑定的。
    • 5、只需要更改配置文件的值,核心组件的底层参数就能修改。
    • 6、将精力都用于写业务,全程无需关心各种框架的整合(底层这些整合都写好了,而且也按需生效了)

自动配置流程的总结

  • 1、导入 spring-boot-starter-xxxx,会导入 spring-boot-starter,也就会导入 spring-boot-autoconfigure 包。
  • 2、spring-boot-autoconfigure 包里面有一个文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,里面指定了应用启动时所有要加载的自动配置类。
  • 3、@EnableAutoConfiguration 会自动地将上面文件里写的所有自动配置类都导入进来,同时 xxxAutoConfiguration 是有声明条件注解的,目的是按需加载。
  • 4、xxxAutoConfiguration 往容器中导入一堆组件,这些组件都是从 xxxProperties 中获取属性值。
  • 5、xxxProperties 又是和配置文件进行了绑定。

最终效果:导入 starter,修改配置文件,就能修改框架的底层行为。

深入理解

@SpringBootApplication 注解包含了三个核心注解,分别是 @ComponentScan@EnableAutoConfiguration@SpringBootConfiguration,它们的作用如下:

  • @ComponentScan
    • 组件扫描,排除一些组件(哪些不需要的)
    • 排除前面已经扫描进来的配置类和自动配置类
  • @EnableAutoConfiguration:开启自动配置功能
    • @AutoConfigurationPackage:扫描主程序所在的包以及其子包,加载自己的组件
      • 利用 @Import(AutoConfigurationPackages.Registrar.class),将主程序所在的包以及其子包的所有组件导入进来
    • @Import(AutoConfigurationImportSelector.class) 加载所有自动配置类,会加载 starter 导入的组件
      • 扫描 SPI 文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中的所有自动配置类
  • @SpringBootConfiguration:本质就是 @Configuration 注解,标注在组件类、配置类上面,Spring IOC 容器启动时就会创建加载这些类对象

SpringBoot 完整的生命周期启动加载流程如下:

SPI 机制介绍

  • Java 中的 SPI(Service Provider Interface)是一种软件设计模式,用于在应用程序中动态地发现和加载组件。SPI 的思想是,定义一个接口或抽象类,然后通过在 classpath 中定义实现该接口的类来实现对组件的动态发现和加载。
  • SPI 的主要目的是解决在应用程序中使用可插拔组件的问题。例如,一个应用程序可能需要使用不同的日志框架或数据库连接池,但是这些组件的选择可能取决于运行时的条件。通过使用 SPI,应用程序可以在运行时发现并加载适当的组件,而无需在代码中硬编码这些组件的实现类。
  • 在 Java 中,SPI 的实现方式是通过在 META-INF/services 目录下创建一个以服务接口全限定名为名字的文件,文件中包含实现该服务接口的类的全限定名。当应用程序启动时,Java 的 SPI 机制会自动扫描 classpath 中的这些文件,并根据文件中指定的类名来加载实现类。
  • 通过使用 SPI,应用程序可以实现更灵活、可扩展的架构,同时也可以避免硬编码依赖关系和增加代码的可维护性。

SpringBoot 的自动配置使用了 SPI 机制,但实现方式不是在 META-INF/services 目录下创建一个以服务接口全限定名为名字的文件,而是使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件进行替代。

功能开关介绍

  • 自动配置:自动批量导入,全部都配置好,什么都不用管
    • 项目一启动,SPI 文件中指定的所有组件都会被加载
  • @EnableXxxx:手动导入,手动控制哪些功能的开启
    • 开启 xxx 功能
    • 都是利用 @Import 注解把此功能要用的组件导入进去

嵌入式容器

Servlet 容器指的是管理、运行 Servlet 组件(ServletFilterListener)的环境,一般指 Web 服务器。

自动配置原理

  • SpringBoot 默认使用嵌入的 Tomcat 作为 Servlet 容器
  • 嵌入式容器的自动配置类是 ServletWebServerFactoryAutoConfigurationEmbeddedWebServerFactoryCustomizerAutoConfiguration
  • ServletWebServerFactoryAutoConfiguration 自动配置了嵌入式容器场景
  • 绑定了 ServerProperties 配置类,所有和服务器相关的配置都使用 server 作为开始前缀
  • ServletWebServerFactoryAutoConfiguration 默认导入了嵌入式的三大服务器,包括 TomcatJettyUndertow
    • 导入 TomcatJettyUndertow 时都有条件注解,系统中有对应的类才会生效(也就是导了包)
    • 在默认情况下,Tomcat 的配置会生效,SpringBoot 往容器中放了 TomcatServletWebServerFactory 组件
    • 往容器中放一个 Web 服务器工厂 ServletWebServerFactory 后,可以创建 Web 服务器
    • Web 服务器工厂都有一个功能,可以调用 getWebServer() 获取 Web 服务器
    • TomcatServletWebServerFactory 创建了 Tomcat Web 服务器
  • ServletWebServerApplicationContext IOC 容器在启动的时候,会调用 ServletWebServerFactory 创建 Web 服务器
  • Spring 容器刷新(启动)的时候,会预留一个时机,调用 onRefresh() 刷新子容器
  • refresh() 容器刷新,十二大步的刷新子容器会调用 onRefresh()
1
2
3
4
5
6
7
8
9
10
11
12
@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

}
1
2
3
4
5
6
7
8
9
10
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

总结

  • Web 场景的 Spring 容器启动,在调用 onRefresh() 的时候,会调用创建 Web 服务器的方法。
  • Web 服务器的创建是通过 WebServerFactory 实现的,容器中又会根据条件注解,启动相关的服务器配置,默认 EmbeddedTomcat 会往容器中放一个 TomcatServletWebServerFactory 组件,导致项目启动后,自动创建出 Tomcat 服务器。

内容协商

底层原理分析

1、@ResponseBody 的底层由 HttpMessageConverter 处理数据,即标注了 @ResponseBody 的返回值,将会由支持它的 HttpMessageConverter 将数据返回给浏览器。

  • 如果 Controller 方法的返回值标注了 @ResponseBody 注解

    • 请求进来先来到 DispatcherServletdoDispatch() 进行处理
    • 找到一个 HandlerAdapter 适配器,利用适配器执行目标方法
    • RequestMappingHandlerAdapter 会执行,调用 invokeHandlerMethod() 来执行目标方法
    • 在目标方法执行之前,需要准备好两样东西
      • HandlerMethodArgumentResolver:参数解析器,确定目标方法的每个参数值
      • HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值该怎么处理
    • RequestMappingHandlerAdapter 里面的 invokeAndHandle() 真正执行目标方法
    • 目标方法执行完成,会返回返回值的对象
    • 去找一个合适的返回值处理器 HandlerMethodReturnValueHandler
    • 最终找到 RequestResponseBodyMethodProcessor,它能处理标注了 @ResponseBody 注解的方法
    • RequestResponseBodyMethodProcessor 调用 writeWithMessageConverters(),利用 MessageConverter 把返回值输出给浏览器
  • HttpMessageConverter 会先进行内容协商

    • 遍历所有的 MessageConverter,看哪个支持这种内容类型的数据
    • 默认的 MessageConverter这些
    • 最终因为需要返回 JSON 数据,所以通过 MappingJackson2HttpMessageConverter 输出 JSON 数据
    • Jackson 利用 ObjectMapper 把返回值对象写出去

2、WebMvcAutoConfiguration 提供了 6 种 默认的 HttpMessageConverters

  • EnableWebMvcConfiguration 通过 addDefaultHttpMessageConverters 添加了默认的 MessageConverter,如下:
    • ByteArrayHttpMessageConverter:支持字节数据读写
    • StringHttpMessageConverter:支持字符串读写
    • ResourceHttpMessageConverter:支持资源读写
    • ResourceRegionHttpMessageConverter:支持分区资源写出
    • AllEncompassingFormHttpMessageConverter:支持表单 XML/JSON 读写
    • MappingJackson2HttpMessageConverter:支持请求响应体 JSON 读写

提示

SpringBoot 提供默认的 MessageConverter 功能有限,仅用于 JSON 或者普通的返回数据。如果需要增加新的内容协商功能,必须添加新的 HttpMessageConverter

事件和监听器

生命周期监听

自定义监听器

SpringApplicationRunListener 负责监听应用的生命周期。

  • 自定义 SpringApplicationRunListener 来监听应用生命周期的步骤
    • 1、编写 SpringApplicationRunListener 接口的实现类
    • 2、在项目的 /META-INF/spring.factories 中配置 org.springframework.boot.SpringApplicationRunListener=自定义的Listener,还可以指定一个有参构造方法,接受两个参数(SpringApplication applicationString[] args
    • 3、值得一提的是,SpringBoot 在 spring-boot.jar 中配置了默认的 Listener,即 org.springframework.boot.SpringApplicationRunListener=org.springframework.boot.context.event.EventPublishingRunListener

生命周期流程

  • SpringBoot 应用的生命周期流程
    • 1、首先从 /META-INF/spring.factories 读取到 Listener
    • 2、引导:利用 BootstrapContext 引导整个项目启动
      • starting:应用开始,SpringApplication 的 run() 方法一调用,只要有了 BootstrapContext 就执行
      • environmentPrepared:环境准备好(把启动参数等绑定到环境变量中),但是 IOC 容器还没有创建,此步骤只会执行一次
    • 3、启动
      • contextPrepared:IOC 容器创建并准备好,但是 Sources(主配置类)还没加载,并关闭引导上下文,组件都没创建,此步骤只会执行一次
      • contextLoaded:IOC 容器加载。主配置类加载进去了,但是 IOC 容器还没刷新(所有 Bean 还没创建),此步骤只会执行一次
      • started:IOC 容器刷新了(所有 Bean 创建好了),但是 runner 没调用
      • ready:IOC 容器刷新了(所有 Bean 创建好了),所有 runner 调用完了
    • 4、运行
      • 以上步骤都正确执行,代表容器成功运行

提示

关于 SpringBoot 应用的生命周期流程,更详细的说明请阅读 SpringApplicationSpringApplicationRunListener 的底层源码。

事件触发时机

各种事件监听器

  • BootstrapRegistryInitializer:感知特定阶段(感知引导初始化)
    • META-INF/spring.factories
    • 创建引导上下文 BootstrapContext 的时候触发
    • application.addBootstrapRegistryInitializer()
    • 使用场景:进行密钥校对授权
  • ApplicationContextInitializer:感知特定阶段(感知 IOC 容器初始化)
    • META-INF/spring.factories
    • application.addInitializers()
  • ApplicationListener:感知全阶段,基于事件机制感知事件,一旦到了哪个阶段可以做别的事
    • @Bean@EventListener:事件驱动
    • SpringApplication.addListeners(…)SpringApplicationBuilder.listeners(…)
    • META-INF/spring.factories
  • SpringApplicationRunListener:感知全阶段生命周期,各种阶段都能自定义操作,功能更完善
    • META-INF/spring.factories
  • ApplicationRunner: 感知特定阶段(感知应用就绪 - Ready)。如果应用卡死,就不会就绪
    • @Bean
  • CommandLineRunner:感知特定阶段(感知应用就绪 - Ready)。如果应用卡死,就不会就绪
    • @Bean

最佳实战

  • 如果想要在项目启动前做事:BootstrapRegistryInitializerApplicationContextInitializer
  • 如果想要在项目启动完成后做事:ApplicationRunnerCommandLineRunner
  • 如果要干涉整个生命周期做事:SpringApplicationRunListener
  • 如果想要利用事件机制做事:ApplicationListener

完整事件触发流程

  • SpringBoot 9 大事件触发顺序与触发时机
    • ApplicationStartingEvent:应用启动但未做任何事情,除了注册 listenersinitializers
    • ApplicationEnvironmentPreparedEvent: Environment 准备好了,但 ApplicationContext 未创建
    • ApplicationContextInitializedEvent: ApplicationContext 准备好了,ApplicationContextInitializers 被调用,但是任何 Bean 未加载
    • ApplicationPreparedEvent:容器刷新之前,Bean 定义信息加载
    • ApplicationStartedEvent:容器刷新完成,runner 未被调用
    • AvailabilityChangeEventLivenessState.CORRECT 应用存活探针
    • ApplicationReadyEvent: 任何 runner 被调用
    • AvailabilityChangeEventReadinessState.ACCEPTING_TRAFFIC 应用就绪探针,可以接收请求了
    • ApplicationFailedEvent:应用启动出错

探针使用说明

  • 感知应用是否存活了:应用可能处于植物状态,虽然活着,但是不能处理请求。
  • 应用是否就绪了:应用可以处理外部请求,说明确实活的比较好。
  • 探针的使用场景:若应用将来部署到 Kubernetes 等平台,则会大量被使用到。

更详细的 SpringBoot 事件触发顺序,请看这里的图解。

SpringBoot 事件驱动开发

SpringBoot 事件驱动开发,依赖应用启动过程生命周期事件感知(9 大事件)、应用运行中事件感知(无数种)。

  • 事件发布:实现 ApplicationEventPublisherAware 接口,或者注入 ApplicationEventMulticaster
  • 事件监听:实现 ApplicationListener 接口,或者使用 @EventListener 注解

SpringBoot 事件驱动开发的使用案例可以阅读 这里 的教程。

Redis 整合

自动配置原理

  • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中导入了 RedisAutoConfigurationRedisReactiveAutoConfigurationRedisRepositoriesAutoConfiguration,所有属性都绑定在 RedisProperties
  • RedisReactiveAutoConfiguration 适用于响应式编程,RedisRepositoriesAutoConfiguration 适用于 JPA 操作
  • RedisAutoConfiguration 配置了以下组件
    • LettuceConnectionConfiguration: 往容器中注入了连接工厂 LettuceConnectionFactory 和操作 Redis 的客户端 DefaultClientResources
    • RedisTemplate<Object, Object>: 可以往 Redis 存储任意对象,默认会使用 JDK 的序列化机制
    • StringRedisTemplate: 可以往 Redis 存储字符串,如果需要存储对象,需要开发人员自己执行序列化操作,Key-Value 都是以字符串的形式进行操作

MyBatis 整合

自动配置原理

JDBC 场景的自动配置

  • JDBC 场景的自动配置
    • mybatis-spring-boot-starter 导入了 spring-boot-starter-jdbc,JDBC 用于操作数据库的场景
    • JDBC 场景的几个自动配置
      • org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
        • 支持数据源的自动配置
        • 所有和数据源相关的配置都绑定在 DataSourceProperties 配置类
        • 默认使用 HikariDataSource 数据源
      • org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
        • 往容器注册 JdbcTemplate,操作数据库
      • org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration
      • org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration
        • 基于 XA 二阶提交协议的分布式事务数据源
      • org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
        • 支持事务的自动配置
    • 拥有的底层能力:数据源、JdbcTemplate、事务处理

MyBatisAutoConfiguration 分析

  • MyBatisAutoConfiguration 配置了 MyBatis 的整合流程
    • mybatis-spring-boot-starter 导入 mybatis-spring-boot-autoconfigure(MyBatis 的自动配置包)
    • 默认加载两个自动配置类
      • org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration
      • org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
        • 必须在数据源配置好之后才配置
        • 往容器注册 SqlSessionFactory 组件,用于创建访问数据库的一次会话
        • 往容器注册 SqlSessionTemplate 组件,用于操作数据库
    • MyBatis 的所有配置绑定在 MybatisProperties
    • 每个 Mapper 接口的代理对象是怎么创建并放到容器中,详见 @MapperScan 的底层源码
      • 利用 @Import(MapperScannerRegistrar.class) 批量往容器中注册组件。解析指定的包路径下的每一个类,为每一个 Mapper 接口类创建代理对象,并注册到容器中

如何分析哪个场景导入以后,开启了哪些自动配置类?

  • spring.boot.autoconfigure 包里面找 classpath:/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中配置的所有值,就是要开启的自动配置类;但是每个类可能有条件注解,基于条件注解判断哪个自动配置类会生效。
  • 快速定位生效的自动配置,方法如下:
1
2
# 是否开启调试模式,可以详细打印开启了哪些自动配置,Positive(生效的自动配置),Negative(不生效的自动配置)
debug=true

参考资料