8、SpringBoot自动装配原理篇
1、自动装配的设计思想
其实我们前面Bean的内容,其实就涉及到了自动装配的各个部分
1.1、前期的调研工作
- 对于开发人员来说,每天都会使用到很多的技术,而对于spring的开发人员来说,他们想知道大部分人使用的究竟是一些什么样的技术,对市场做了调研,将使用到的技术做一个整合,那么就得到了最终的一个技术集,所有的技术都在其中,逐步更新,包含了企业级开发中的所有常用技术
- 进行进一步的调研,每个人使用这些技术还是有些区别的,这个时候,spring将这些技术当中的一些常用的参数做一个设置,得到一个设置集。该设置集针对的是技术集当中的每一个技术,例如:Redis数据库,最常用的是什么?端口号嘛--localhost:6379,全部列下来,这就是前期的调研工作。
- 把这些事情做好之后,spring为我们做了一件事,自动装配。
1.2、自动装配
- 你程序搭建的时候,现将springboot的基础环境搭建(spring-boot-starter)出来,加载用户自定义的Bean和导入的其他坐标,形成初始化的环境,这个环境就是我开发的时候所要用的这些东西
- 将技术集中的全部技术都定义出来,在Spring/SpringBoot启动的时候,默认全部加载
- 将技术集中具有使用条件的技术,通过约定的方式定义出来,设置为按条件加载,由开发者决定是否使用该技术(与初始化环境的对比)
- 那么问题来了,每一个技术的使用还需要大量的配置,相当麻烦,还记得上方的设置集嘛?
- 如果某一个技术,大量的人都是用了某一种配置,那么我设置集就作为你这个技术的默认配置,进行加载---约定大于配置
- 那么也肯定存在一种情况,你的默认配置和我搭不上,如何解决?
- springboot开放了设置集的配置覆盖接口,由开发者决定是否去覆盖,如果与我开发发生冲突,那我肯定会选择去覆盖掉他
我想要买辆车,那么开发商需不需要将车上的这些组件全部给我定义好?例如:发动机,变速箱,车载导航,弹压监测....,你要不要无所谓,但是这些东西我的全部定义好,万一其他人需要使用呢?
技术集可以根据我们的使用需求,进行调整,你可以不要这些东西,但是有些东西你肯定得要,比如,你开车不能没有发动机吧?
先给你定义好,你需要使用的全部技术,然后根据你的使用需求,你来决定是要,还是不要,把这些东西确定好以后,你还可以进行一些个性化的配置----设置集
同样都是配置,根据设置的不同,那么产生的结果就不一样;例如:这个坐椅我不要真皮的坐椅,我就要牛皮的,可以吗?当然可以
设置集的作用就是基于你技术集的配置之上,给我做配置,我只需要设置,那么他马上就可以根据我的配置要求去使用
2、原理1:整体拆分
就用之前讲过的猫和老鼠案例来讲解,其实也不用,只需要一个能跑起来的springboot项目即可
2.1、@SpringBootApplication
我们的程序是从这个启动类Application开始的,那么这个启动类,没有@SpringBootApplication注解肯定是不能运行的嘛,那这个注解就是我们的万物起源
含义
这个注解是一个组合注解,他是一个接口,这个接口上方包含了多个注解
最上方的四个注解不看,主要看下面三个,也就是说@SpringBootApplication实际上是这三个玩意儿的合体,这仨看明白了,自然就明白了
2.2、注解1:@SpringBootConfiguration
这个怎么用不知道,但看名字就晓得这是SpringBoot的配置注解,是给SpringBoo配置的哦,点进去看看
整体架构
@SpringBootConfiguration
@Configuration
@Component
@Index
2.3、注解2:@EnableAutoConfiguration
是否启用自动配置,这个注解在@SpringBootApplication中,那么默认就代表启动了自动装配,点进去看看
@AutoConfiurationPackage
使用了@AutoConfiurationPackage注解,这是什么,自动配置包?点进去
到头了,最终他引入了一个组件
@Import(AutoConfigurationImportSelector.class)
ImportSelector,这是我们之前加载Bean方式的一种,通过返回一个String的数组来注册我们的Bean组件
结构
@EnableAutoConfiguration
@AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class)
@Import(AutoConfigurationImportSelector.class)
2.4、注解3:@ComponentScan
包扫描嘛,我们把那个注解展开看看
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
进入这个注解看看
ComponentScans,扫描多个,应该就是集束整体扫描
excludeFilters
Filter:过滤器,这里指定了两个过滤规则
TypeExcludeFilter.class
按类型排除性的过滤器
AutoConfigurationExcludeFilter.class
满足什么要求以后,他会做一定的排除,就是不扫描了,那不就是扫描不扫描,哪些东西要,哪些东西不要么
结论
实际上就是当这个注解加载完毕后,我哪些类可以不要了
2.5、整体目录结构
@SpringBootApplication
// 项目配置
@SpringBootConfiguration
@Component
// 这个注解是加载springboot的启动速度的,根据启动的过程创建全新的文件,加载Bean的时候加载这个文件
@Index
// 自动装配
@EnableAutoConfiguration
@AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class)
@Import(AutoConfigurationImportSelector.class)
// 项目过滤器,就是加载Bean的方式,只不过加载的花哨一点,高级一点
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
那么我说句实话,很多东西都明白,那么是不是只需要搞清楚在@EnableAutoConfiguration中的@AutoConfigurationPackage的@Import(AutoConfigurationPackages.Registrar.class)
和与@AutoConfigurationPackage同级的@Import(AutoConfigurationImportSelector.class) 搞明白就可以了?
2.6、AutoConfigurationPackages.Registrar
Registrar是AutoConfigurationPackages的一个抽象类,内部有俩方法,我们看第一个
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
1、registerBeanDefinitions
继承自ImportBeanDefinitionRegistrar和 DeterminableImports,第一个参数熟悉嘛?这是不是我们之前说过的加载Bean的一种方式,通过BeanDefinition这个对象来创建Bean?
1.1、ImportBeanDefinitionRegistrar的回顾
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
this.registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
内部有俩个默认实现的方法,我们如果要使用它,那就必须要去覆盖掉这俩方法,使用谁覆盖掉谁,参数不多讲嘛,第一个是数据源,也就是你这个类,是被谁加载的(import),第二个参数是我们创建Bean的对象,第三个是创建Bean的名称
这里原码所使用的是覆盖掉第二种方法,也就是只要前面俩个参数
1.2、覆盖的registerBeanDefinitions方法实现(注册BeanDefinition对象)
那么这个静态类Registrar覆盖了这个方法之后,他干了个什么事情呢?
- 调用了一个register的方法,register--注册;
- 两个参数
- registry
- 这个对象会通过所给予的Bean注册器--BeanDefinition,到容器当中去创建Bean
- new PackageImports(数据源对象).getPackageNames().toArray(new String[0])
- 这个我们单独提一个小点来讲
- registry
1.3、断点调试(new PackageImports(数据源对象))
对registerBeanDefinitions方法体打断点,对第二个参数进行一个计算
-
new PackageImports()的计算
-
-
-
这里看不出来是什么结果
-
-
new PackageImports(metadata).getPackageNames()的计算
哦?这里就将我的包结构给读取出来了,或者说?我这个启动类,引导类--Application的所在路径被加载出来了
1.4、结论
这也是为什么平时老师经常会讲,自己新建的package,一定要包含在启动类的类路径,及其子包下,因为配置注解的时候,注解要生效,就会进行组件扫描,而组件扫描--@ComponentScan,他需要一个String类型的数组(其中一种),数组当中写的就是我们,需要被扫描的路径
2、内部调用的--register方法
重复一遍,这个方法需要两个参数,第一个参数是我们Bean的构造器,第二个参数是传递了一个我们启动类所在的包路径
2.1、if-----registry.containsBeanDefinition(BEAN)
-
这个程序启动之后,第一步会判定,我们这个注册器,是否包含一个,Bean定义叫做参数(Bean)
-
这个参数叫什么呢?
-
就是这个类--AutoConfigurationPackages所对应的字节码文件的全路径类名嘛
他需要看我们加载过这个东西没有,加载过,走if,没有加载过,走下方的路
2.2、else----
我们显然是没有加载过的,那么他会执行一个什么样的方法?
前期提要,这玩意儿是不是就是我们之前写过的
它定义了一个Bean,这个Bean,这个Bean的名称就是我们那个变量名,也就是Autoxxxx这个类的全路径名,第二个参数new 了一个BasePackagesBeanDefinition,并且将我们刚刚的参数传递了过来,什么参数?(com.waves)
1、BasePackagesBeanDefinition
我不晓得这个类是干什么几把用的,但我晓得他这一坨,就是一个Bean的构造器--BeanDefinition,点进去看看
简单说下这里干了个什么事情,你要扫描哪些包?我需要知道吧,我要不知道,我怎么给你扫描,所以程序用这种形式给我记录了一下,什么形式?--addBasePackages。
为什么是add,这里可以设置扫描不同的包,因此,这里可以选择添加进去,而不是通过设置的方式设置进去,因为还有许多的包都可以参与扫描的
3、总结
他这里就做了一件事情,设置当前启动类所在的包,作为扫描包,后续要针对我设置的这个包,进行一个扫描。
springboot启动之后,他需要知道加载你哪些包,中的,哪些Bean,这里就确定了这么一个东西
2.7、@Import(AutoConfigurationImportSelector.class)
no BB,直接点进去看
1、他所实现的接口
很多接口,大致分为3类
- 以Aware结尾的接口
- Ordered接口
- DeferredImportSelector接口
1.1、Aware
用发现;发觉解释比较好
我这里做一个测试案例
-
创建一个类去继承一个Aware结尾的接口
-
将其定义为组件
-
实现ApplicationContextAware接口
-
实现其定义的一个方法
-
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { }
-
无论实现的是XXXAware接口,这些接口都会让你实现一个方法,setxxxxAware()方法,并且需要一个对应的参数,这个参数是XXXXX,他既然需要,那我就给他一个这个对象嘛,但是后面抛的异常就不一定了
-
定义方法体所需要的形参
-
// 所需要的形参 private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }
-
-
方法设置完毕
我这里就做了一个set注入的操作,对吧,他有一个什么作用?
当我的任何一个spring环境当中的Bean实现了某个XXXX开头的Aware结尾的接口之后,我就可以在当前这个Bean所在的类里面,去使用这个XXXX对象
-
设置一个方法,这个方法既然可以拿到ApplicationContext上下文对象,那我就可以使用他的方法对吧
-
我这里就简单的打印一个IOC容器当中所有组件的名字
-
public void getBeanNames(){ String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames(); for (String bean : beanDefinitionNames) { System.out.println(bean); } }
-
-
启动主程序,开始调用
没毛病吧,用Aware接口是可以注入到我需要使用的那些资源的
现在明白了吧,如果你想使用环境(EnvironmentAware);Bean工厂(BeanFactoryAware);资源加载器(ResourceLoaderAware);Bean的类加载器(BeanClassLoaderAware);他们的对象该怎么办?
就去实现对应的接口就可以了,前提是你实现他们这些接口的类必须被Spring加载到了哈,不然我也不会写@Component的,你这个类得是一个需要被Spring所管理的对象,Bean,才可以
1.2、Ordered
Order;意为排序,点进去看看内部是什么样的
public interface Ordered {
int HIGHEST_PRECEDENCE = -2147483648;
int LOWEST_PRECEDENCE = 2147483647;
int getOrder();
}
内部一个最大值,一个最小值,其中有一个getOrder()的方法,获取排序?也就是加载顺序
你当前这个类,加载的时候,在Spring容器当中的加载顺序,为什么需要这个顺序?
1、为什么需要这个顺序?
我这里有50个bean,我现在要加入第51个Bean,而51个Bean的,加载的前提是,第37个Bean必须是存在的,不然我不会加载;那么现在出现一个情况,我51和37的位置互换了,那么51号Bean还能被加载吗?不能,因为37还没有加载,那么我的功能就不完全了,所以这个顺序很重要
我这里将---AutoConfigurationImportSelector,的类展开,我看他内部的方法中是否存在这个getOrder()
显然是存在的嘛,并且有个值,是多少我就不晓得了,反正这里它有个加载顺序
1.3、DeferredImportSelector
Deferred:推迟,延迟
前面这一坨不认识,后面的ImportSelector非常熟悉,这是一种加载Bean的方式,我们点进这个接口看看
很显然,这个接口继承了ImportSelector接口,说明他的功能会比继承的接口更加的广泛,也就是说,这是一个推迟的选择器
而我们的这个AutoConfigurationImportSelector这个类实现了这个接口,那么可以得出一个结论,这个类或者说是这个Bean,一定会比其他Bean加载的时间要晚
1、interface Group
Group--意为组,也就是说,推迟的类并非他一个,而是有许多的类,整合为一个组,一起推迟加载
我们主要观看Group中定义的方法
-
process
- 需要的参数为
-
那我们看下,这个接口是一个子接口,那么实现他父接口的实现类AutoConfigurationImportSelector,对其实现了什么内容
2、AutoConfigurationImportSelector--实现的process方法、
看不懂对吧,没关系
-
Assert---断言:
- 那么这一坨实际上是对某种情况进行一个判定
-
那么下方就是不符合这个情况所走的条件
-
-
这里的get方法就是我们要解读的方法
-
3、getAutoConfigurationEntry
3.1、if--首先判断你给定我的这个注解(annotationMetadata)源注解是否是一个不可用的?那不屁话吗,他的源注解是谁?是谁调用的它?启动类嘛~启动类的那个注解调用的?SpringBoot下的EnableAutoConfiguration下的AutoConfigurationImportSelector类实现的process方法
是否是一个不可用的?那显然不是啊,如果是的话就会返回一个空对象
3.2、获取源注解当中的属性--getAttributes(annotationMetadata);
什么属性?
源注解的俩属性
这俩注解干啥用的?
3.3、排除部分不需要的技术-----干啥用的这俩b玩意儿?
在我们的EnableAutoConfiguration这个注解点进去以后
下方俩属性,这俩属性就是读取的属性
如果我们在做装配的时候,SpringBoot不是给我提供了一个技术集吗,里面包含了大量企业级开发的常用技术,我如果我不想用哪些技术,我可以在这里面配置
- exclude--按照Class,类的形式匹配
- excludeName--按照字符串的名称匹配
3.4、所以这一步
他去把这俩东西exclude和excludeName拿出来,如果你不想要的东西,我先获取,然后把它拿掉
3.5、最核心的配置--getCandidateConfigurations
获得候选配置
返回值是一个List集合,内部包含的是String类型的字符串,传递了一个源注解,和得到的不需要被封装的技术的名称对象
3.6、什么是候选配置?
SpringBoot封装的技术集A中,有着大量的配置,那些技术是都需要加载的吗?不一定啊,但是在哪里摆着的一些是有可能需要被加载的,这个有可能的范围,需要实际情况去斟酌,不加载的和可能会加载的;是两码事,搞清楚
点进这个候选配置的方法中getCandidateConfigurations()--
- 主要分为三步
- 通过loadFactoryName的操作,获取到了我们可能会被加载的技术
- 然后通过一个断言匹配
- 最终返回一个configurations的对象
3.7、getCandidateConfigurations----loadFactoryNames
- ClassLoade--类加载器,这个不是我们需要看的
- 最终的返回值,调用的方法,是我们需要注意的
3.8、loadSpringFactories
逐个解析,因为代码量很大
从缓存当中获取我们,类加载器的各种信息,判断是否为空,如果不为空就返回,为空往下面看
这个getResources使我们需要看的
3.9、getResources
通过类加载器,的读取外部资源的方法(getResources)读取一个外部文件,这个外部文件点进去看看
"META-INF/spring.factories"
META-INF下的spring.factories文件,这个文件下配置了大量的spring工厂,这个文件在哪儿呢?
3.10spring.factories文件的位置和装载的内容
我这里导入的spring-boot的核心配置文件的版本是
在它的配置文件下是什么都找不到的,我们需要找autoconfiguration,自动配置下的META-INF,因为这玩意儿本身就是在@EnableAutoConfiguration的注解下
人家这里给你写明了,Auto Configure--自动装配,那说明我们之前的SpringBoot封装的技术集,就是这一大坨东西,从红框开始的这一大坨内容,就是自动装载的全部技术
这里urls就获取到的配置文件中全部技术的装载,然后使用一个while循环进行加载,最终将这个内容,装载到另外一个区域里,就是我们最里面套的那个东西
3.11、回到getAutoConfigurationEntry的方法中
getCandidateConfigurations这个方法最终给我们返回了一个加载着springboot的技术集中的内容,的list集合,这里面加载的项,就是我们刚刚看到的那一大坨配置项
这里面加载的项就是我们刚刚那些AutoConfiguration的项
3.12、总结
这个地方(getResources)他做了一件事情,就是把spring.factories的配置加载到了内存中,回头这些东西就会成为我们的技术集
2.8、自动装配的案例
我这里选择使用redis.RedisAutoConfiguration--redis的这项技术
我们来看看这个类
1、RedisAutoConfiguration
看看这些注解,熟悉嘛?
1.1Configuration---这个类是一个配置类,并且还没有启用代理对象
1.2、ConditionalOnClass--如果这个类,存在,RedisAutoConfiguration才会被加载到容器当中
-
那么我们看看这个类是否存在吧?点进去看看
-
-
就是找不到这个类,那么这个类在哪里呢?
-
spring-boot-starter-data-redis
-
那这个类就被加载到了嘛,所以我们这个RedisAutoConfiguration会被加载到容器当中嘛
-
我现在将这个依赖取消掉
-
我去重新打印一遍容器中加载的组件
-
释放注解,打印加载的组件
- 看是不是都加载上了
1.3、@EnableConfigurationProperties(RedisProperties.class)
这是什么,强行让一个类加载成Bean,关联注解,我们点金这个RedisProperties看看
读取了一个配置文件,那是不是跟我们读取yml配置文件当中的内容一样?还记得毛和老鼠吗
这个类中我读取了yml的配置文件
1.4、RedisTemplate
下方的stringRedisTemplate也是一样的道理
2、总结
我们通过读取这个配置文件的信息,将技术集当中的配置加载到了我们的环境当中,但是这些配置真的被加载到了Bean上吗?那显然是没有的,上方的案例就是例子
加载完毕之后,会进行一个条件检测,检测通过,那么就将配置加载到容器当中
将技术集中具有使用条件的技术约定出来,形成按条件加载,使用与否,就是对比我们开发者定义的初始化环境,也就是我们的pom.xml中导入的坐标,Grandle也是,只要能导包的都行
那么比对完毕之后,他才会去进行一个配置,配置在哪儿?就是别人定义好的配置类,这个配置类读取配置文件的信息完成值的注入
这个技术当中的参数,可能有默认值的,也可能没有默认值的,有没有,取决于用什么样的技术,别人给你实现的配置方法
当然,这是默认配置,如果这个配置,我们可以根据默认配置去加载它。
如果我们不想使用默认配置,那么就会根据开发者的自身需求去覆盖默认的配置,例如我在yml配置文件中通过配置port的属性去修改他的端口号,当然,这里应该写别的值容易区分
3、整体总结
我前面大概全部复述了整体的一个流程,建立在我听得懂的基础之上,
spring默认启动的时候,加载了无数技术的自动配置类,用来检测你当前的环境需不需要加载对应的技术---pom.xml
根据自动配置类的加载去确认这个类是否要激活,条件只要能成立,那么他就能激活
3.1、问题所在
你现在的自动配置类能不能少加载点东西呢,我用不到那么多的配置类,能不能提高点速度,别做那么多了,这个也是可以的
4、干预自动配置
4.1、添加自动配置
我们导入一个MP的坐标
<!-- MP -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
打开我们所加载的信息
MP也有自己的spring.factories配置文件,我们点开看看
Auto Configure,不用我多说了把,自动配置,我们掐掉上面的,上面的不是所需要的,上面的配置是环境的配置,我们掐掉第一个,留下自动配置加载的第一个配置,我这里是自己新建了一个spring.factories的配置文件哈,别人的是没办法改的
3.3、开始自动配置
怎么配置,我之前在启动类当中是加载了我的这个类的,什么类?猫和老鼠的类
我这个类除了没有限制条件,其他的东西基本和原码写的一模一样把
引导类中我引入了这个类,让其成为了一个组件,我也因此可以调用这其中的方法
我现在在配置文件当中将刚刚复制的那个MP的配置类的全路径名变为我自己的猫和老鼠的全路径名,那么这个时候我就完成自己的自动配置了,那么现在我要取消掉启动类当中的引入注解
我通过配置文件的方式实现了自动配置,我的猫和老鼠的方法被加载出来了
我没有通过注解去让他自动配置吧,并没有
4.2、排除自动配置
就是我加载的时候不想要这个默认加载的配置,让运行速度加快,或者有些时候技术冲突,那么我如何操作?
这是我IOC容器当中加载的组件,我现在不香要他,我该如何干预?排除
1、配置文件的方式
0匹配
2、注解的方式
我们的@EnableAutoConfiguration中有俩属性是用于排除不需要的配置
而@SpringBootApplication这个注解继承了这俩个属性
我们这里就通过路径名排除吧
@SpringBootApplication(excludeName="org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration")