SpringBoot原理篇之自动配置工作流程
本文最后更新于:13 天前
bean的定义由前期xml配置逐步演化成注解配置,本质是一样的,都是通过反射机制加载类名后创建对象,对象就是spring管控的bean。
@Import注解可以指定加载某一个类作为spring管控的bean,如果被加载的类中还具有@Bean相关的定义,会被一同加载。
spring开放出了若干种可编程控制的bean的初始化方式,通过分支语句由固定的加载bean转成了可以选择bean是否加载或者选择加载哪一种bean。
bean的加载方式
关于bean的加载方式,spring提供了各种各样的形式。因为spring管理bean整体上来说就是由spring维护对象的生命周期,所以bean的加载可以从大的方面划分成2种形式:已知类并交给spring管理、已知类名并交给spring管理。有什么区别?一个给.class,一个给类名字符串。内部其实都一样,都是通过spring的BeanDefinition对象初始化spring的bean。需要注意到是,在初始化时需要调用所说明的bean中的无参构造方法,如果没有会初始化失败。
方式一:配置文件+<bean/>
标签
最初级的bean的加载方式其实可以直击spring管控bean的核心思想,就是提供类名,然后spring就可以管理了。所以第一种方式就是给出bean的类名,内部是由反射机制加载成class。
xml方式声明自己开发的bean
1 |
|
1 |
|
xml方式声明第三方开发的bean
1 |
|
1 |
|
方式二:配置文件扫描+注解定义bean
由于方式一需要将spring管控的bean全部写在xml文件中,对于程序员来说非常不友好,所以就有了第二种方式。哪一个类要受到spring管控加载成bean,就在这个类的上面加一个注解,还可以顺带起一个bean的名字(id)。这里可以使用的注解有@Component
以及三个衍生注解@Service
、@Controller
、@Repository
。
1 |
|
1 |
|
由于我们无法在第三方提供的技术源代码中去添加上述4个注解,因此当你需要加载第三方开发的bean的时候可以使用@Bean定义在一个方法上方,将当前方法的返回值就可以交给spring管控,bean的id为该方法名。记得这个方法一定要定义在@Component修饰的类中。
1 |
|
上面提供的仅仅是bean的声明,spring并没有感知到这些东西,还必须设置spring去检查这些类。可以通过下列xml配置设置spring去检查哪些包,发现定了对应注解,就将对应的类纳入spring管控范围,声明成bean。
1 |
|
方式二声明bean的方式是目前企业中较为常见的bean的声明方式,但是也有缺点。方式一中,通过一个配置文件,你可以查阅当前spring环境中定义了多少个或者说多少种bean,但是方式二没有任何一个地方可以查阅整体信息,只有当程序运行起来才能感知到加载了多少个bean。
方式三:注解方式声明配置类
@ComponentScan声明包扫描
方式二已经完美的简化了bean的声明,再也不用写很多的配置信息了。仔细观察xml配置文件,会发现这个文件中只剩了扫描包这句话,于是就有人提出使用java类替换掉这种固定格式的配置。严格意义上讲这不能算全新的方式,但是由于此种开发形式是企业级开发中的主流形式,所以单独独立出来做成一种方式。
定义一个类并使用@ComponentScan替代原始xml配置中的包扫描这个动作,其实功能基本相同。
1 |
|
使用FactroyBean接口声明bean
spring提供了一个接口FactoryBean,也可以用于声明bean,只不过实现了FactoryBean接口的类造出来的对象不是当前类的对象,而是FactoryBean接口泛型指定类型的对象。如下列,造出来的bean并不是DogFactoryBean,而是Dog。有什么用呢?可以在对象初始化前做一些事情,实现对bean加载到容器之前的批处理操作。下例中的注释位置就是让你自己去扩展要做的其他事情的。
1 |
|
有人说,注释中的代码写入Dog的构造方法不就行了吗?干嘛这么费劲转一圈,还写个类,还要实现接口,多麻烦啊。还真不一样,你可以理解为Dog是一个抽象后剥离的特别干净的模型,但是实际使用的时候必须进行一系列的初始化动作。只不过根据情况不同,初始化动作不同而已。如果写入Dog,或许初始化动作A当前并不能满足你的需要,这个时候你就要做一个DogB的方案了。然后,就没有然后了,你就要做两个Dog类。当时使用FactoryBean接口就可以完美解决这个问题。
通常实现了FactoryBean接口的类使用@Bean的形式进行加载,当然你也可以使用@Component去声明DogFactoryBean,只要被扫描加载到即可,但是这种格式加载总觉得怪怪的,指向性不是很明确。
1 |
|
注解格式开发导入XML格式配置的bean
早期开发的系统大部分都是采用xml的形式配置bean,如果需要基于之前的系统进行二次开发,新开发的用注解格式,之前开发的是xml格式,这个时候两种要同时使用。spring提供了一个注解可以解决这个问题,**@ImportResource,在配置类上直接写上要被融合的xml配置文件名即可加载配置类并加载配置文件**,是一种兼容性解决方案(系统迁移)。
1 |
|
proxyBeanMethods属性
前面的例子中用到了@Configuration这个注解,当我们使用AnnotationConfigApplicationContext加载配置类的时候,配置类可以不添加这个注解。但这个注解有一个更加强大的功能,为@Configuration注解设置proxyBeanMethods属性值为true即可,可以保障该配置类中使用方法创建的bean的唯一性,proxyBeanMethods=true可以保障调用此配置类中方法得到的对象是从容器中获取的而不是重新创建的。由于此属性默认值为true,所以很少看见明确书写的,除非想放弃此功能。
1 |
|
通过容器调用上面的cat方法时,得到的是同一个对象,是从容器中获取的而不是重新创建的。需要注意的是必须使用spring容器对象调用此方法才有保持bean唯一性的特性。此特性在很多底层源码中有应用。
方式四:使用@Import注解注入bean
使用扫描的方式加载bean是企业级开发中常见的bean的加载方式,但是由于扫描的时候不仅可以加载到你要的东西,还有可能加载到各种各样的乱七八糟的东西,比如你扫描了cc.gaojie.service包,后来因为业务需要,又扫描了cc.gaojie.dao包,此时cc.gaojie包下面只有service和dao这两个包,所以直接扫描cc.gaojie。但后来又加入了一个外部依赖包,里面也有cc.gaojie包。
所以我们需要一种更精准制导的加载方式,使用@Import注解就可以解决这个问题。它可以加载所有的一切,只需要在注解的参数中写上加载的类对应的.class即可。可以指定加载,@Import注解拥有其重要的应用场景。使用@Import注解导入要注入的bean所对应的字节码,并且被导入的bean无需使用注解声明为bean,可以有效的降低源代码与Spring技术的耦合度,在spring技术底层及诸多框架的整合中大量使用。
使用@Import注解注入bean
可在被导入的bean上使用@Component、@Service、@Controller、@Repository注解指定bean的id。
使用@Import注解注入配置类
除了加载bean,还可以使用@Import注解加载配置类。
1 |
|
1 |
|
方式五:编程形式注册bean
上面四种加载bean的方式都是在容器启动阶段完成bean的加载,下面这种方式就比较特殊,可以在容器初始化完成后手动加载bean。通过这种方式可以实现编程式控制bean的加载。
1 |
|
1 |
|
方式六:导入ImportSelector接口的实现类
在方式五种,bean的加载可以在容器初始化后添加if语句进行编程化的控制。那是否可以在容器初始化过程中进行控制呢?答案是可以的。实现ImportSelector接口的类可以设置加载的bean的全路径类名,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。于是现在又多了一种控制bean加载的方式,或者说是选择bean的方式。导入实现了ImportSelector接口的类,实现对导入源的编程式处理。(谁导入了ImportSelector接口的实现类,就能对谁进行编程式管理)
1 |
|
1 |
|
在SpringConfig6.class上使用@Import注解导入了ImportSelector接口的实现类MyImportSelector.class,因此SpringConfig6.class为导入源,所以对SpringConfig6.class进行编程式处理。从运行结果也可以看到:导入的ImportSelector接口的实现类不会被注入为bean。
方式七:导入ImportBeanDefinitionRegistrar接口的实现类
方式六提供了给定类全路径类名控制bean加载的形式,但bean的加载不是一个简简单单的对象,spring中定义了一个叫做BeanDefinition的东西,它才是控制bean初始化加载的核心。BeanDefinition接口中给出了若干种方法,可以控制bean的相关属性。比如创建的对象是单例还是非单例,在BeanDefinition中定义了scope属性就可以控制。所以方式六没有开放出足够的对bean的控制操作。通过 ImportBeanDefinitionRegistrar 接口的实现类定义bean,可以对bean的初始化进行更加细粒度的控制。导入ImportBeanDefinitionRegistrar接口的实现类,通过BeanDefinition的注册器注册实名bean,实现对容器中bean的裁定,例如对现有bean的覆盖,进而达成不修改源代码的情况下更换实现的效果。
1 |
|
1 |
|
从运行结果也可以看出:导入的ImportBeanDefinitionRegistrar接口的实现类不会被注册为bean。可以得出结论:实现了ImportSelector接口或者ImportBeanDefinitionRegistrar接口的类不会被解析成一个Bean注册到容器中。
方式八:导入BeanDefinitionRegistryPostProcessor接口的实现类
上述七种方式都是在容器初始化过程中进行bean的加载或者声明,但是这里有一个问题:这么多种方式,它们之间如果有冲突怎么办?谁能有最终裁定权?当某种类型的bean被接二连三的使用各种方式加载后,在我们对所有加载方式的加载顺序没有完全理解清晰之前,还真不知道最后谁说了算。
spring提供了BeanDefinitionRegistryPostProcessorBeanDefinition接口,即Processor处理器,全称bean定义后处理器,在所有bean注册加载完后,它是最后一个运行的。通过BeanDefinitionRegistryPostProcessorBeanDefinition接口实现类注册的bean最后一个加载,如果有冲突时会覆盖前面的bean。
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
通过运行结果也可以看出:实现了BeanDefinitionRegistryPostProcessor接口的类也会被解析成一个Bean注册到容器中。
bean的加载控制
企业级开发中不可能在spring容器中进行bean的饱和式加载的。什么是饱和式加载,就是不管用不用,全部加载。比如jdk中有两万个类,那就加载两万个bean,显然是不合理的,因为你压根就不会使用其中大部分的bean。那合理的加载方式是什么?肯定是必要性加载,就是用什么加载什么。要用什么技术,就加载对应的bean。所以在spring容器中,通过判定是否加载了某个类来控制某些bean的加载是一种常见操作。其实思想很简单,先判断一个类的全路径名是否能够成功加载,加载成功说明有这个类,那就干某项具体的工作,否则就干别的工作。
前面学习bean的加载时,提出了有关加载控制的方式,其中手工注册bean,ImportSelector接口,ImportBeanDefinitionRegistrar接口,BeanDefinitionRegistryPostProcessor接口都可以控制bean的加载。springboot还定义了若干种控制bean加载的条件设置注解,由spring固定加载bean变成了可以根据情况选择性的加载bean。
1 |
|
通过上述的分析,可以看到此类操作将成为企业级开发中的常见操作,于是springboot将把这些常用操作给我们做了一次封装。
@ConditionalOnClass注解实现了当虚拟机中加载了cc.gaojie.bean.Wolf类时加载对应的bean。
1 |
|
@ConditionalOnMissingClass注解控制虚拟机中没有加载指定的类才加载对应的bean。
1 |
|
除了判定是否加载类,还可以对当前容器类型做判定。**@ConditionalOnWebApplication注解控制当前容器环境是web环境才去加载对应的bean。**
1 |
|
@ConditionalOnNotWebApplication注解控制当前容器环境不是web环境才去加载对应的bean。
1 |
|
还可以判定是否加载了指定名称的bean。比如当前容器中已经提供了jdbcTemplate对应的bean,就没有必要再加载一个全新的jdbcTemplate的bean了。**@ConditionalOnBean(name=””) 注解控制当前容器中没有某个bean时才去加载对应的bean。**
1 |
|
1 |
|
以下案例就是判定当前是否加载了mysql的驱动类,如果加载了,给提供一个Druid的数据源对象。
1 |
|
多条件时还可以做并且的逻辑关系,写2个就是2个条件都成立,写多个就是多个条件都成立。
1 |
|
springboot的bean加载控制注解还有很多,这里就不一一列举了,最常用的判定条件就是根据类是否加载来进行控制。
bean的依赖属性配置管理
bean在运行的时候,实现对应的业务逻辑时有可能需要开发者提供一些设置值,也就是属性。如果使用构造方法将参数固定,灵活性不足,这个时候就可以使用前期学习的bean的属性配置相关的知识进行灵活的配置。
通过yml配置文件,设置bean运行需要使用的配置信息:
1
2
3
4
5
6
7cartoon:
cat:
name: "图多盖洛"
age: 5
mouse:
name: "泰菲"
age: 1将业务功能bean运行需要的资源抽取成独立的属性类,加载配置属性,读取对应前缀相关的属性值:
1
2
3
4
5
6@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}使用@EnableConfigurationProperties注解设定使用属性类时加载bean:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25@EnableConfigurationProperties(CartoonProperties.class)
@Data
public class CartoonCatAndMouse {
private Cat cat;
private Mouse mouse;
private CartoonProperties cartoonProperties;
public CartoonCatAndMouse(CartoonProperties cartoonProperties) {
this.cartoonProperties = cartoonProperties;
cat = new Cat();
cat.setName(cartoonProperties.getCat() != null && StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName() : "tom");
cat.setAge(cartoonProperties.getCat() != null && cartoonProperties.getCat().getAge() != null ? cartoonProperties.getCat().getAge() : 3);
mouse = new Mouse();
mouse.setName(cartoonProperties.getMouse() != null && StringUtils.hasText(cartoonProperties.getMouse().getName()) ? cartoonProperties.getMouse().getName() : "jerry");
mouse.setAge(cartoonProperties.getMouse() != null && cartoonProperties.getMouse().getAge() != null ? cartoonProperties.getMouse().getAge() : 4);
}
public void play() {
System.out.println(cat.getAge() + "岁的" + cat.getName() + "和" + mouse.getAge() + "岁的" + mouse.getName() + "打起来了");
}
}建议在业务类上使用@EnableConfigurationProperties声明bean,这样在不使用这个类的时候,也不会无故加载专用的属性配置类CartoonProperties,减少spring管控的资源数量,解耦强制加载bean。
主类时运行使用@Import导入,解耦强制加载bean:
1
2
3
4
5
6
7
8
9
10@SpringBootApplication
@Import(CartoonCatAndMouse.class)
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(App.class);
CartoonCatAndMouse cartoon = context.getBean(CartoonCatAndMouse.class);
cartoon.play();
}
}最终的效果是运行时属性值按yaml属性文件中的值灵活加载,有则加载属性文件中的值,没有则加载默认值。
总结
- bean的运行如果需要外部设置值,建议将设置值封装成专用的属性类
- 设置属性类加载指定前缀的配置信息
- 在需要使用属性类的位置通过注解@EnableConfigurationProperties加载bean,而不要直接在属性配置类上定义bean,减少资源加载的数量,因需加载而不要饱和式加载。
自动配置原理(工作流程)
自动配置简单说就是springboot根据我们开发者的行为猜测你要做什么事情,然后把你要用的bean都给你准备好。springboot怎么做到的呢?就是看你导入了什么类,就知道你想干什么了。然后把你有可能要用的bean(注意是有可能)都给你加载好,你直接使用就行了,springboot把所需要的一切工作都做完了。
自动配置的意义就是加速开发效率,将开发者使用某种技术时需要使用的bean根据情况提前加载好,实现自动配置的效果。当然,开发者有可能需要提供必要的参数,比如你要用mysql技术,导入了mysql的坐标,springboot就知道了你要做数据库操作,一系列的数据库操作相关的bean都给你提前声明好,但是你要告诉springboot你到底用哪一个数据库,比如P地址、端口,不告诉spirngboot,springboot就无法帮你把自动配置相关的工作做完。
这种思想其实就是在日常的开发过程中根据开发者的习惯慢慢抽取得到了。整体过程分为2个阶段:
阶段一:准备阶段
springboot的开发人员先大量收集Spring开发者的编程习惯,整理开发过程每一个程序经常使用的技术列表,形成一个技术集A;
收集常用技术(技术集A)的使用参数,不管你用什么常用设置,我用什么常用设置,统统收集起来整理一下,得到开发过程中每一个技术的常用设置,形成每一个技术对应的设置集B;
阶段二:加载阶段
springboot初始化Spring容器基础环境,读取用户的配置信息,加载用户自定义的bean和导入的其他坐标,形成初始化环境;
springboot将技术集A包含的所有技术在SpringBoot启动时默认全部加载,这时肯定加载的东西有一些是无效的,没有用的;
springboot会对技术集A中每一个技术约定出启动这个技术对应的条件,并设置成按条件加载,由于开发者导入了一些bean和其他坐标,也就是与初始化环境,这个时候就可以根据这个初始化环境与springboot的技术集A进行比对了,哪个匹配上加载哪个;
因为有些技术不做配置就无法工作,所以springboot开始对设置集B下手了。它统计出各个国家各个行业的开发者使用某个技术时最常用的设置是什么,然后把这些设置作为默认值直接设置好,并告诉开发者当前设置我已经给你搞了一套,你要用可以直接用,这样可以减少开发者配置参数的工作量;
但是默认配置不一定能解决问题,于是springboot开放修改设置集B的接口,可以由开发者根据需要决定是否覆盖默认配置。
以上这些仅仅是一个思想,落地到代码实现阶段就要好好思考一下怎么实现了。假定我们想自己实现自动配置的功能,都要做哪些工作呢?
- 首先指定一个技术X,我们打算让技术X具备自动配置的功能,这个技术X可以是任意功能,这个技术隶属于上面描述的技术集A
1 |
|
- 然后找出技术X使用过程中的常用配置Y,这个配置隶属于上面表述的设置集B
1 |
|
- 将常用配置Y设计出对应的yml配置书写格式,然后定义一个属性类封装对应的配置属性,这个过程其实就是上一节咱们做的bean的依赖属性管理,一模一样
1 |
|
- 最后做一个配置类,当这个类加载的时候就可以初始化对应的功能bean,并且可以加载到对应的配置
1 |
|
- 当然,你也可以为当前自动配置类设置上激活条件,例如使用@CondtionOn* * * * 为其设置加载条件
1 |
|
做到这里都已经做完了,但是遇到了一个全新的问题,如何让springboot启动的时候去加载这个类呢?如果不加载的话,我们做的条件判定,做的属性加载这些全部都失效了。springboot为我们开放了一个配置入口,在配置目录中创建META-INF目录,并创建spring.factories文件,在其中添加设置,说明哪些类要启动自动配置就可以了。
1 |
|
其实这个文件就做了一件事,通过这种配置的方式加载了指定的类。转了一圈,就是个普通的bean的加载,和最初使用xml格式加载bean几乎没有区别,格式变了而已。那自动配置的核心究竟是什么呢?自动配置其实是一个小的生态,可以按照如下思想理解:
- 自动配置从根本上来说就是一个bean的加载
- 通过bean加载条件的控制给开发者一种感觉,自动配置是自适应的,可以根据情况自己判定,但实际上就是最普通的分支语句的应用,这是蒙蔽我们双眼的第一层面纱
- 使用bean的时候,如果不设置属性,就有默认值,如果不想用默认值,就可以自己设置,也就是可以修改部分或者全部参数,感觉这个过程好屌,也是一种自适应的形式,其实还是需要使用分支语句来做判断的,这是蒙蔽我们双眼的第二层面纱
- springboot技术提前将大量开发者有可能使用的技术提前做好了,条件也写好了,用的时候你导入了一个坐标,对应技术就可以使用了,其实就是提前帮我们把spring.factories文件写好了,这是蒙蔽我们双眼的第三层面纱
你在不知道自动配置这个知识的情况下,经过上面这一二三,你当然觉得自动配置是一种特别牛的技术,但是一窥究竟后发现,也就那么回事。而且现在springboot程序启动时,在后台偷偷的做了这么多次检测,这么多种情况判定,不用问了,效率一定是非常低的,毕竟它要检测100余种技术是否在你程序中使用。
以上内容是自动配置的工作流程。
总结
- springboot启动时先加载spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置项,将其中配置的所有的类都加载成bean
- 在加载bean的时候,bean对应的类定义上都设置有加载条件,因此有可能加载成功,也可能条件检测失败不加载bean
- 对于可以正常加载成bean的类,通常会通过@EnableConfigurationProperties注解初始化对应的配置属性类并加载对应的配置
- 配置属性类上通常会通过@ConfigurationProperties加载指定前缀的配置,当然这些配置通常都有默认值。如果没有默认值,就强制你必须配置后使用了
变更自动配置
知道了自动配置的执行过程,下面就可以根据这个自动配置的流程做一些高级定制了。例如系统默认会加载100多种自动配置的技术,如果我们先手工干预此工程,禁用自动配置是否可行呢?答案一定是可以的。方式还挺多:
方式一:通过yaml配置设置排除指定的自动配置类
1 |
|
方式二:通过注解参数排除自动配置类
1 |
|
方式三:排除坐标(应用面较窄)
如果当前自动配置中包含有更多的自动配置功能,也就是一个套娃的效果。此时可以通过检测条件的控制来管理自动配置是否启动。例如web程序启动时会自动启动tomcat服务器,可以通过排除坐标的方式,让加载tomcat服务器的条件失效。不过需要提醒一点,你把tomcat排除掉,记得再加一种可以运行的服务器。
1 |
|
总结
- springboot的自动配置并不是必然运行的,可以通过配置的形式干预是否启用对应的自动配置功能
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!