一、前言
当我们开始使用Spring Boot时,我们会发现只需引入一个web相关的starter依赖,并进行一些简单配置,有的时候甚至可以不用配置(
约定大于配置
),应用就可以跑起来!这与Spring MVC相比,后者需要我们进行大量的手动配置
和解决依赖冲突
,显得繁琐复杂。Spring Boot是如何解决这一点,让我们能够更加专注于业务逻辑的开发呢?这得益于Spring Boot的核心特性——自动装配
。
学习参考视频:SpringBoot3
参考资料:SpringBoot3自动装配原理
二、自动装配是什么
Spring Boot的自动装配实际上是从META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中获取到对应的需要进行自动装配的类,并生成相应的Bean对象,然后将它们交给Spring容器进行管理
三、源码解析
这里我从项目的开始一步步带领大家深入分析自动装配的原理
1.导入starter场景
我们这里以
spring-boot-starter-web
为例
当导入starter-web
场景启动器后,我们就导入了和Web相关场景的所有
依赖:starter-tomcat
、starter-json
、starter-tomcat
、spring-webmvc
、spring-boot-starter
其中spring-boot-starter
是核心场景启动器,是所有starter
的starter
,基础核心starter
(每个场景启动器都会引入一个核心场景启动器)
在核心场景启动器spring-boot-starter
中又引入了spring-boot-autoconfigure
包,这个包里面囊括了SpringBoot官方全场景的所有配置
只要spring-boot-autoconfigure
这个包下的所有类都能生效,那么相当于SpringBoot官方写好的整合功能就生效了,但是SpringBoot默认却扫描不到 spring-boot-autoconfigure
下写好的配置类。(SpringBoot默认只扫描主程序所在的包及其子包)
思考:
官方写好的配置类默认却扫描不到,那该怎么让它生效呢?
跟随我的脚步,进行深入分析主程序:@SpringBootApplication,相信你会找到答案!
2.深入剖析启动类
SpringBoot的启动类是一切的开始,我可以发现在main方法上有个注解@SpringBootApplication
@SpringBootApplication
是一个复合注解,由以下三个注解组合而成,@SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
@SpringBootConfiguration
:标记当前类为SpringBoot配置类@EnableAutoConfiguration
:开启自动配置的核心注解@ComponentScan
:组件扫描
注意:
上图我在三个注解前标注的序号,这是主程序运行起来后的执行顺序
下面我将逐一分析这个3个注解
@SpringBootConfiguration
@SpringBootConfiguration
就是@Configuration
的一个衍生注解
主要作用:标记该类为SpringBoot的配置类,并交给SpringIOC容器管理
@EnableAutoConfiguration
@EnableAutoConfiguration
,顾名思义就是:开启自动配置,也是一个复合注解,由@AutoConfigurationPackage
、@Import(AutoConfigurationImportSelector.class)
组合而成
@AutoConfigurationPackage
@AutoConfigurationPackage
主要作用:扫描主程序包,加载自己的组件
@Import
:导入第三方的组件,可以导入三种类型的类
- 导入
@Configuration
注解的配置类:容器中就会自动注册这个组件,ID默认为全类名- 导入
ImportSelector
实现类:通过实现ImportSelector
类,实现selectImports
方法,返回需要导入组件的全类名数组- 导入
ImportBeanDefinitionRegistrar
实现类:通过实现中ImportBeanDefinitionRegistrar
类,实现registerBeanDefinitions 方法手动注册Bean到容器中
详细内容可见@Import注解 -【Spring底层原理】Spring中Import注解源码解析
在这个注解利用 @Import(AutoConfigurationPackages.Registrar.class)
把主程序所在包及其子包的所有组件导入进来
利用
@Import
,在springboot中为给容器导入一个组件,而导入的组件由 AutoConfigurationPackages.class的内部类Registrar.class 执行逻辑来决定是如何导入的
进入到AutoConfigurationPackages类中找到内部类Registrar,并在图中位置打上断点
ImportBeanDefinitionRegistrar
接口:
- 这个接口是 Spring 框架提供的,用于在配置类中动态注册 Bean 定义
- 实现了这个接口的类
可以通过 @Import 注解导入到 Spring 容器中
接下来我们利用小工具,来看看new PackageImports(metadata).getPackageNames()
的值
运行后我们可以看到new PackageImports(metadata).getPackageNames()
的值为:com.weyang
当前启动类所在的包名,
即把主程序所在的包的所有组件导入进来。
结论:@AutoConfigurationPackage
就是将主配置类(@SpringBootApplication
标记的类)所在包及其子包中的所有组件都扫描注册到Spring容器中
@Import(AutoConfigurationImportSelector.class)
AutoConfigurationImportSelector
开启自动配置类的导包的选择器,确定哪些自动配置类应该被加载到 Spring 容器中。具体来说,它实现了 DeferredImportSelector
接口。
DeferredImportSelector
和ImportSelector
的区别
详细介绍:Spring中DeferredImportSelector源码解析
补充说明
由于AutoConfigurationImportSelector
没有直接实现ImportSelector接口,而是实现的DeferredImportSelector
接口,SpringBoot对于DeferredImportSelector接口的实现类有另外一组处理流程,因此
在自动装配的流程中并不会去调用selectImports()
方法
getImportGroup方法:根据这个方法得到具体的 Group 实现类
我们先在AutoConfigurationImportSelector中找到getImportGroup
方法,通过这个方法可以知道DeferredImportSelector实现类对应的Group类AutoConfigurationGroup
类
调用Group实现类AutoConfigurationGroup
中的,process
方法,并调用AutoConfigurationImportSelector
的getAutoConfigurationEntry
方法,获取所有的自动配置类
getAutoConfigurationEntry方法:获取所有的自动配置类
根据导入的 @Configuration 类
的 AnnotationMetadata
返回一个 AutoConfigurationImportSelector.AutoConfigurationEntry 对象。这个对象包含了应该被导入的自动配置类列表以及被排除的配置类列表。
在 getCandidateConfigurations(annotationMetadata, attributes)
这里打断点查看,可以看到configurations数组长度为181,并且文件后缀名都为 ***AutoConfiguration
进入getCandidateConfigurations(annotationMetadata, attributes)
看看这个181个配置类从哪里来的
通过ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
方法,从类路径中加载所有标记了 @AutoConfiguration
注解的类,并返回这些类的全限定名列表
,同时通过 Assert.notEmpty 方法确保列表不为空,否则抛出异常并提示检查配置文件
Assert.notEmpty(configurations,
"No auto configuration classes found in "
+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
意思是:在“META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
中没有找到自动配置类。如果您使用自定义包装,请确保该文件是正确的。” ------ 在2.7.0
版本之前存放的地址是META-INF/spring.factories
我们点击进入load
方法,就可以看到配置类存放地址是如何来的了!
我们可以看到扫描SPI文件地址:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
然后调用findUrlsInClasspath
方法在类路径中查找指定路径的资源,遍历找到的资源 URL,调用 readCandidateConfigurations
方法读取每个资源文件中的候选类名,并添加到 importCandidates 列表中。
注意
:这里会加载所有jar包
下的classpath路径下META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,这样的文件不止一个
现在我们进行回到AutoConfigurationImportSelector
的getAutoConfigurationEntry
方法
经过removeDuplicates
方法去除重复的配置类,getExclusions
方法去除手动排除的类,getConfigurationClassFilter
方法根据条件判断,过滤掉不符合条件的配置类后,在我的这个项目中实际就剩下自动配置类数量为70个。
SPI机制:
●Java中的SPI(Service Provider Interface)是一种软件设计模式
,用于在应用程序中动态地发现和加载组件。SPI的思想
是,定义一个接口或抽象类,然后通过在classpath中定义实现该接口的类来实现对组件的动态发现和加载。
● SPI的主要目的是解决在应用程序中使用可插拔组件的问题。例如,一个应用程序可能需要使用不同的日志框架或数据库> > 连接池,但是这些组件的选择可能取决于运行时的条件。通过使用SPI,应用程序可以在运行时发现并加载适当的组件,而> > 无需在代码中硬编码这些组件的实现类。
● 在Java中,SPI的实现方式是通过在META-INF/services目录下创建一个以服务接口全限定名为名字的文件,文件中包含实> 现该服务接口的类的全限定名。当应用程序启动时,Java的SPI机制会自动扫描classpath中的这些文件,并根据文件中指定> 的类名来加载实现类。
● 通过使用SPI,应用程序可以实现更灵活、可扩展的架构,同时也可以避免硬编码依赖关系和增加代码的可维护性。
根据刚刚找到的地址找到配置文件,我们可以看到在这个位置存的内容,就是SpringBoot启动默认加载
的152
个自动配置类xxxAutoConfiguration
,因为演示的项目中还引入了其他的依赖,例如druid、mybatisplus等,所以刚刚显示的是181。
虽然SpringBoot默认导入了152个自动配置类, 并不是这152个自动配置类都能生效,每一个自动配置类,都有条件注解@ConditionalOnxxx
,只有条件成立,才能生效
常用的@ConditionalOnXxx注解:
@ConditionalOn
Class : classpath中存在该类时起效
@ConditionalOn
MissingClass : classpath中不存在该类时起效
@ConditionalOn
Bean : DI容器中存在该类型Bean时起效
@ConditionalOn
MissingBean : DI容器中不存在该类型Bean时起效
@ConditionalOn
SingleCandidate : DI容器中该类型Bean只有一个或@Primary的只有一个时起效
@ConditionalOn
Expression : SpEL表达式结果为true时
@ConditionalOn
Property : 参数设置或者值一致时起效
@ConditionalOn
Resource : 指定的文件存在时起效
@ConditionalOn
Jndi : 指定的JNDI存在时起效
@ConditionalOn
Java : 指定的Java版本存在时起效
@ConditionalOn
WebApplication : Web应用环境下起效
@ConditionalOn
NotWebApplication : 非Web应用环境下起效
@ConditionalOnXxx
其实是spring底层注解,意思就是根据不同的条件,来进行自己不同的条件判断,如果满足指定的条件,那么配置类里边的配置才会生效。
在152个自动配置类的全类名中,我们找到Spring MVC 框架中的核心组件DispatcherServlet的自动配置类DispatcherServletAutoConfiguration
在 DispatcherServletAutoConfiguration 这个类上添加了一个@AutoConfiguration
和@ConditionalOnClass(DispatcherServlet.class)
注解
其中@AutoConfiguration
注解就是对@Configuration的一个封装,为了让其更加见名知意,说明这个类就是为了完成自动配置的。
@AutoConfiguration
注解是 Spring Boot 提供的一个重要注解,用于标记一个配置类,使其成为自动配置的一部分。它的主要作用和特点如下:
- 自动配置:
标记的类会被 Spring Boot 的自动配置机制识别。
Spring Boot 会根据应用的依赖和环境自动应用这些配置,减少手动配置的工作量。- 条件化配置:
通常与 @Conditional 注解一起使用。
使得配置类只有在满足特定条件时才会被加载和应用,提高了配置的灵活性和适应性。- 见名知意:
@AutoConfiguration 注解使得配置类的用途更加明确和直观,表明该类是为了完成自动配置> 的。
开发人员可以一目了然地知道某个类的作用,提高代码的可读性和可维护性。- 集成第三方库:
通过 @AutoConfiguration 注解,可以轻松地集成第三方库,而不需要手动编写大量的配置代码。
另外,还一个注解@ConditionalOnClass(DispatcherServlet.class)
,意思是如果我们的环境中有DispatcherServlet,才会注入 DispatcherServletAutoConfiguration 的bean对象,否则不会生效
在这个自动配置类中,我们还可以找到一个@EnableConfigurationProperties(xxxProperties.class)
@EnableConfigurationProperties
VS
@ConfigurationProperties
- 在@ConfigurationProperties的使用,把配置类的属性与yml配置文件绑定起来的时候,
还需要加上@Component注解
才能绑定并注入IOC容器中,若不加上@Component,则会无效。- @EnableConfigurationProperties,则是快速实现属性配置绑定,只要加上该注解,即可完成属性绑定,
无需加@Component
,更多用于导入第三方
写好的组件进行属性绑定。
因为SpringBoot默认只扫描自己主程序所在的包。如果导入第三方包,即使组件上标注了 @Component、@ConfigurationProperties 注解,也没用。因为组件都扫描不进来,此时使用这个注解就可以快速进行属性绑定并把组件注册进容器
结论:
SpringBoot通过@Conditional
注解,实现自动配置类的按需生效
。并通过@EnableConfigurationProperties注解实现组件的核心参数与配置文件进行绑定,从而实现只需要该配置文件的值,对应核心组件的底层参数都会被修改的效果。
@ComponentScan
组件扫描:排除一些组件(排除前面已经扫描进来的配置类、和自动配置类)
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
四、总结
核心流程总结:
- 导入starter,就会导入autoconfigure包。
- autoconfigure 包里面 有一个文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,里面指定的所有启动要加载的自动配置类
- @EnableAutoConfiguration 会自动的把上面文件里面写的所有自动配置类都导入进来。xxxAutoConfiguration 是有条件注解进行按需加载
- xxxAutoConfiguration给容器中导入一堆组件,组件都是从 xxxProperties中提取属性值
- xxxProperties又是和配置文件进行了绑定