三万字盘点Spring/Boot的那些常用扩展点
通过@Bean声明了,并且和属性分别指定到了类的方法和方法
1
2
3
4
@Bean( ="", ="")
() {
();
}
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
on {
([] args) {
=();
//将 注册到容器中
.(.class);
.();
// 关闭上下文,触发销毁操作
.close();
}
@Bean( ="", ="")
() {
();
}
@Bean
user() {
();
}
}
执行结果:
1
2
3
4
5
6
7
8
对象被创建了
Aware接口起作用,t被调用了,此时user=com....User@
@注解起作用,方法被调用了
接口起作用,方法被调用了
@Bean#()起作用,方法被调用了
@注解起作用,方法被调用了
接口起作用,方法被调用了
@Bean#()起作用,方法被调用了
分析结果
通过测试的结果可以看出,Bean在创建和销毁的过程当我们实现了某些接口或者加了某些注解,就会回调我们实现的接口或者执行的方法。
同时,在执行t的时候,能打印出User对象,说明User已经被注入了,说明注入发生在t之前。
这里画张图总结一下Bean创建和销毁过程中调用的顺序。
回调顺序
红色部分发生在Bean的创建过程,灰色部分发生在Bean销毁的过程中,在容器关闭的时候,就会销毁Bean。
这里说一下图中的Aware接口指的是什么。其余的其实没什么好说的,就是按照这种方式配置,会调用对应的方法而已。
Aware接口是指以Aware结尾的一些提供的接口,当你的Bean实现了这些接口的话,在创建过程中会回调对应的set方法,并传入响应的对象。
这里列举几个Aware接口以及它们的作用
接口作用
are
注入
注入isher事件发布器
注入
注入Bean的名称
有了这些回调,比如说我的Bean想拿到,不仅可以通过@注入,还可以通过实现are接口拿到。
通过上面的例子我们知道了比如说@注解、@注解、@注解的作用,但是它们是如何在不同的阶段实现的呢?接着往下看。
,中文名 Bean的后置处理器,在Bean创建的过程中起作用。
是Bean在创建过程中一个非常重要的扩展点,因为每个Bean在创建的各个阶段,都会回调及其子接口的方法,传入正在创建的Bean对象,这样如果想对Bean创建过程中某个阶段进行自定义扩展,那么就可以自定义来完成。
说得简单点,就是在Bean创建过程中留的口子,通过这个口子可以对正在创建的Bean进行扩展。只不过Bean创建的阶段比较多,然后接口以及他的子接口、就提供了很多方法,可以使得在不同的阶段都可以拿到正在创建的Bean进行扩展。
来个Demo
现在需要实现一个这样的需求,如果Bean的类型是User,那么就设置这个对象的属性为 ”三友的java日记“。
那么就可以这么写:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
@
( bean, ) {
if() {
//如果当前的Bean的类型是 User ,就把这个对象 的属性赋值为 三友的java日记
((User) bean).("三友的java日记");
}
;
}
}
测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
on {
([] args) {
=();
//将 r 和 User 注册到容器中
.(r.class);
.(User.class);
.();
User user = .(User.class);
.out.("获取到的Bean为"+ user +",属性值为:"+ user.());
}
}
测试结果:
1
获取到的Bean为com....User@,属性值为:三友的java日记
从结果可以看出,每个生成的Bean在执行到某个阶段的时候,都会回调r,然后r就会判断当前创建的Bean的类型,如果是User类型,那么就会将的属性设置为 ”三友的java日记“。
内置的
这里我列举了常见的一些的实现以及它们的作用
作用
处理@、@Value注解
处理@、@、@注解
处理一些注解或者是AOP切面的动态代理
处理Aware接口注入的
处理@Async注解
处理@注解
通过列举的这些的实现可以看出, Bean的很多注解的处理都是依靠及其子类的实现来完成的,这也回答了上一小节的疑问,处理@、@、@注解是如何起作用的,其实就是通过,在Bean的不同阶段来调用对应的方法起作用的。
在Dubbo中的使用
在Dubbo中可以通过@(@)来引用生产者提供的接口,这个注解的处理也是依靠,也就是 的扩展来实现的。
1
2
3
4
5
or
, ssor {
// 忽略
}
当Bean在创建的某一阶段,走到了这个类,就会根据反射找出这个类有没有@(@)注解,有的话就构建一个动态搭理注入就可以了。
在 Bean的扩展中扮演着重要的角色,是 Bean生命周期中很重要的一部分。正是因为有了,你就可以在Bean创建过程中的任意一个阶段扩展自己想要的东西。 ssor
通过上面一节我们知道 是对Bean的处理,那么ssor很容易就猜到是对,也就是容器的处理。
举个例子,如果我们想禁止循环依赖,那么就可以这么写。
1
2
3
4
5
6
7
8
9
{
@
( ) {
// 禁止循环依赖
(() ).(false);
}
}
后面只需要将注入到容器中就会生效。
ssor是可以对容器做处理的,方法的入参就是的容器,通过这个接口,就对容器进行为所欲为的操作。 SPI机制
SPI全称为 ( ),是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离, 让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。
JDK有内置的SPI机制的实现,Dubbo也有自己的SPI机制的实现,但是这里我们都不讲。。
但是,我之前写过相关的文章,文章的前半部分有对比三者的区别,有需要的小伙伴可以关注微信公众号 三友的java日记 回复 dubbo spi 即可获得。
这里我们着重讲一下的SPI机制的实现r。
r
的SPI机制规定,配置文件必须在路径下的META-INF文件夹内,文件名必须为.,文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名。但是键和值可以没有任何关系,当然想有也可以有。
show me the code
这里我自定义一个类,ation作为键,值就是User
1
2
{
}
spring.factories文件
1
com....spi.ation=com....User
然后放在META-INF底下
测试:
1
2
3
4
5
6
7
8
on {
([] args) {
List = r.(ation.class, ation.class.());
.(.out::);
}
}
结果:
1
com....User
可以看出,通过r的确可以从.文件中拿到ation键对应的值。
到这你可能说会,这SPI机制也没啥用啊。的确,我这个例子比较简单,拿到就是遍历,但是在中,如果在加载类的话使用SPI机制,那我们就可以扩展,接着往下看。
启动扩展点
项目在启动的过程中有很多扩展点,这里就来盘点一下几个常见的扩展点。
1、自动装配
说到的扩展点,第一时间肯定想到的就是自动装配机制,面试贼喜欢问,但是其实就是一个很简单的东西。当项目启动的时候,会去从所有的.文件中读取@ion键对应的值,拿到配置类,然后根据一些条件判断,决定哪些配置可以使用,哪些不能使用。
.文件?键值?不错,自动装配说白了就是SPI机制的一种运用场景。
@ion注解:
1
2
3
4
@(.class)
@ {
//忽略
}
我擦,这个注解也是使用@注解,而且配置类还实现了接口,跟前面也都对上了。在中,@ion是通过@n来使用的。
在中还有这样一段代码
所以,这段代码也明显地可以看出,自动装配也是基于SPI机制实现的。
那么我想实现自动装配怎么办呢?很简单,只需两步。
第一步,写个配置类:
1
2
3
4
5
6
7
8
9
@
{
@Bean
n () {
Bean();
}
}
这里我为了跟前面的知识有关联,配置了一个。
第二步,往.文件配置一下
1
2
org..boot..ion=\
com.....n
到这就已经实现了自动装配的扩展。
接下来进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
@n
on {
([] args) {
= .run(.class);
User user = .(User.class);
.out.("获取到的Bean为"+ user);
}
}
运行结果:
1
2
调用 的 方法生成 Bean:com....User@
获取到的Bean为com....User@
从运行结果可以看出,自动装配起了作用,并且虽然往容器中注入的Bean的class类型为,但是最终会调用的的实现获取到User对象。
自动装配机制是的一个很重要的扩展点,很多框架在整合的时候,也都通过自动装配来的,实现项目启动,框架就自动启动的,这里我举个整合。
整合的.文件
2、 ,这是干啥的呢?
我们都知道,在环境下,外部化的配置文件支持和yaml两种格式。但是,现在不想使用和yaml格式的文件,想使用json格式的配置文件,怎么办?
当然是基于该小节讲的来实现的。
1
2
3
4
5
6
7
8
9
{
//可以支持哪种文件格式的解析
[] ();
// 解析配置文件,读出内容,封装成一个结合返回回去
List
> load( name, );
}
对于的实现,两个实现
:可以解析或者xml结尾的配置文件
ader:解析以yml或者yaml结尾的配置文件
所以可以看出,要想实现json格式的支持,只需要自己实现可以用来解析json格式的配置文件的就可以了。
动手来一个。
实现可以读取json格式的配置文件
实现这个功能,只需要两步就可以了。
第一步:自定义一个
ader,实现接口
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
26
27
28
29
30
31
32
oader {
@
[] () {
//这个方法表明这个类支持解析以json结尾的配置文件
[]{"json"};
}
@
> load( name, ) {
= .();
= .((int) .());
//将文件内容读到 中
.read();
//将读出来的字节转换成字符串
=(.array());
// 将字符串转换成
= JSON.();
Map map =(.size());
//将 json 的键值对读出来,放入到 map 中
for( key : .()) {
map.put(key, .(key));
}
.(("", map));
}
}
第二步:配置
ader 已经有了,那么怎么用呢?当然是SPI机制了,对于配置文件的处理,就是依靠SPI机制,这也是能扩展的重要原因。
SPI机制加载实现.文件配置
会先通过SPI机制加载所有,然后遍历每个,判断当前遍历的,通过获取到当前能够支持哪些配置文件格式的解析,让后跟当前需要解析的文件格式进行匹配,如果能匹配上,那么就会使用当前遍历的来解析配置文件。
其实就属于策略接口,配置文件的解析就是策略模式的运用。
所以,只需要按照这种格式,在.文件中配置一下就行了。
org.springframework.boot.env.PropertySourceLoader=\
com.sanyou.spring.extension.springbootextension.propertysourceloader.JsonPropertySourceLoader
到此,其实就扩展完了,接下来就来测试一下。
测试
先创建一个.json的配置文件
.json配置文件
改造User
1
2
3
4
5
{
// 注入配置文件的属性
@Value("${.:}")
;
}
启动项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@n
on {
([] args) {
= .run(.class);
User user = .(User.class);
.out.("获取到的Bean为"+ user +",属性值为:"+ user.());
}
@Bean
user() {
();
}
}
运行结果:
获取到的Bean为com.sanyou.spring.extension.User@481ba2cf,属性username值为:三友的java日记
成功将json配置文件的属性注入到User对象中。
至此,就支持了以json为结尾的配置文件格式。
Nacos对于的实现
如果你的项目正在用Nacos作为配置中心,那么刚刚好,Nacos已经实现json配置文件格式的解析。
Nacos对于的实现
Nacos不仅实现了json格式的解析,也实现了关于xml格式的配置文件的解析,并且优先级会比默认的xml格式文件解析的优先级高。至于Nacos为啥需要实现?其实很简单,因为Nacos作为配置中心,不仅支持和yaml格式的文件,还支持json格式的配置文件,那么客户端拿到这些配置就需要解析,已经支持了和yaml格式的文件的解析,那么Nacos只需要实现不支持的就可以了。
3、
也是启动过程的一个扩展点。
在启动过程,会回调这个类的实现方法,传入。
那怎么用呢?
依然是SPI。
SPI加载
然后遍历所有的实现,依次调用
调用
这里就不演示了,实现接口,按照如下这种配置就行了
但是这里需要注意的是,此时传入的并没有调用过方法,也就是里面是没有Bean对象的,一般这个接口是用来配置,而不是用来获取Bean的。
4、ssor
ssor在启动过程中,也会调用,也是通过SPI机制来加载扩展的。
ssor
ssor是用来处理ent的,也就是一些配置信息,所有的配置都是存在这个对象的。
说这个类的主要原因,主要不是说扩展,而是他的一个实现类很关键。
这个类的作用就是用来处理外部化配置文件的,也就是这个类是用来处理配置文件的,通过前面提到的解析配置文件,放到ent里面。
5、和
和都是在成功启动之后会调用,可以拿到启动时的参数。
那怎么扩展呢?
当然又是SPI了。
这两个其实不是通过SPI机制来扩展,而是直接从容器中获取的,这又是为啥呢?
因为调用和时,已经启动成功了,容器都准备好了,需要什么Bean直接从容器中查找多方便。
而前面说的几个需要SPI机制的扩展点,是因为在启动的时候,容器还没有启动好,也就是无法从容器获取到这些扩展的对象,为了兼顾扩展性,所以就通过SPI机制来实现获取到实现类。
刷新上下文和调用加载和调用
所以要想扩展这个点,只需要实现接口,添加到容器就可以了。
Event 事件
Event 事件可以说是一种观察者模式的实现,主要是用来解耦合的。当发生了某件事,只要发布一个事件,对这个事件的监听者(观察者)就可以对事件进行响应或者处理。
举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于事件的模型来实现,只需要打119、救人监听火灾的发生就行了,当发生了火灾,通知这些打119、救人去触发相应的逻辑操作。
什么是 Event 事件
那么是什么是 Event 事件,就是实现了这种事件模型,你只需要基于提供的API进行扩展,就可以完成事件的发布订阅
提供的事件api:
事件的父类,所有具体的事件都得继承这个类,构造方法的参数是这个事件携带的参数,监听器就可以通过这个参数来进行一些业务操作。
事件监听的接口,泛型是子类需要监听的事件类型,子类需要实现,参数就是事件类型,方法的实现就代表了对事件的处理,当事件发生时,会回调方法的实现,传入发布的事件。
isher
isher
事件发布器,通过方法就可以发布一个事件,然后就可以触发监听这个事件的监听器的回调。
实现了isher接口,所以通过就可以发布事件。
那怎么才能拿到呢?
前面Bean生命周期那节说过,可以通过are接口拿到,甚至你可以通过实现直接获取到isher,其实获取到的isher也就是,因为是实现了isher。
话不多说,上代码
就以上面的火灾为例
第一步:创建一个火灾事件类
火灾事件类继承
1
2
3
4
5
6
7
8
// 火灾事件
ent {
( ) {
super();
}
}
第二步:创建火灾事件的监听器
打119的火灾事件的监听器:
1
2
3
4
5
6
7
8
ener {
@
( event) {
.out.("打119");
}
}
救人的火灾事件的监听器:
1
2
3
4
5
6
7
8
{
@
( event) {
.out.("救人");
}
}
事件和对应的监听都有了,接下来进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
on {
([] args) {
=();
//将 事件监听器 注册到容器中
.(ener.class);
.(.class);
.();
// 发布着火的事件,触发监听
.(("着火了"));
}
}
将两个事件注册到容器中,然后发布事件
运行结果:
1
2
打119
救人
控制台打印出了结果,触发了监听。
如果现在需要对火灾进行救火,那么只需要去监听,实现救火的逻辑,注入到容器中,就可以了,其余的代码根本不用动。
内置的事件
内置的事件很多,这里我罗列几个
事件类型触发时机
t
在调用 接口中的()方法时触发
在调用的start()方法时触发
在调用的stop()方法时触发
当被关闭时触发该事件,也就是调用close()方法触发
在容器启动的过程中,会发布这些事件,如果你需要这容器启动的某个时刻进行什么操作,只需要监听对应的事件即可。
事件的传播
事件的传播是什么意思呢?
我们都知道,在中有子父容器的概念,而事件的传播就是指当通过子容器发布一个事件之后,不仅可以触发在这个子容器的事件监听器,还可以触发在父容器的这个事件的监听器。
上代码
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
{
([] args) {
// 创建一个父容器
text =();
//将 打119监听器 注册到父容器中
text.(ener.class);
text.();
// 创建一个子容器
ext =();
//将 救人监听器 注册到子容器中
ext.(.class);
ext.();
// 设置一下父容器
ext.(text);
// 通过子容器发布着火的事件,触发监听
ext.(("着火了"));
}
}
创建了两个容器,父容器注册了打119的监听器,子容器注册了救人的监听器,然后将子父容器通过关联起来,最后通过子容器,发布了着火的事件。
运行结果:
1
2
救人
打119
从打印的日志,的确可以看出,虽然是子容器发布了着火的事件,但是父容器的监听器也成功监听了着火事件。
源码验证
事件传播源码
从这段源码可以看出,如果父容器不为空,就会通过父容器再发布一次事件。
传播特性的一个坑
前面说过,在容器启动的过程,会发布很多事件,如果你需要有相应的扩展,可以监听这些事件。但是,在环境下,你的这些发布的事件的监听器可能会执行很多次。为什么会执行很多次呢?其实就是跟传播特性有关。
在的环境下,为了使像和这些不同的服务的配置相互隔离,会创建很多的子容器,而这些子容器都有一个公共的父容器,那就是项目启动时创建的容器,事件的监听器都在这个容器中。而这些为了配置隔离创建的子容器,在容器启动的过程中,也会发布诸如t等这样的事件,如果你监听了这些事件,那么由于传播特性的关系,你的这个事件的监听器就会触发多次。
如何解决这个坑呢?
你可以进行判断这些监听器有没有执行过,比如加一个判断的标志;或者是监听类似的事件,比如ent事件,这种事件是在启动中发布的事件,而子容器不是,所以不会多次发这种事件,也就会只执行一次。
事件的运用举例 1、在中的使用
又来以举例了。。的n监听了,然后判断如果是t就进行相应的处理,这个类还实现了接口。。
1
2
3
4
5
6
7
8
9
10
11
12
n, , {
@
( event) {
if( && ) {
// fail-fast -> check all are
this..().mes();
}
}
}
说实话,这监听代码写的不太好,监听了,那么所有的事件都会回调这个类的方法,但是方法实现又是当是t类型才会往下走,那为什么不直接监听t呢?
可以给个差评。
膨胀了膨胀了。。
2、在的运用
在的中,当项目启动的时候,会自动往注册中心进行注册,那么是如何实现的呢?当然也是基于事件来的。当web服务器启动完成之后,就发布事件。
然后不同的注册中心的实现都只需要监听这个事件,就知道web服务器已经创建好了,那么就可以往注册中心注册服务实例了。如果你的服务没往注册中心,看看是不是web环境,因为只有web环境才会发这个事件。
提供了一个抽象类 ,实现了对Event(的父类)事件的监听
一般不同的注册中心都会去继承这个类,监听项目启动,实现往注册中心服务端进行注册。
Nacos对于的继承
Event事件在内部中运用很多,是解耦合的利器。在实际项目中,你既可以监听/Boot内置的一些事件,进行相应的扩展,也可以基于这套模型在业务中自定义事件和相应的监听器,减少业务代码的耦合。 命名空间
最后来讲一个可能没有留意,但是很神奇的扩展点--命名空间。起初我知道这个扩展点的时候,我都惊呆了,这玩意也能扩展?真的不得不佩服设计的可扩展性。
回忆一下啥是命名空间?
先看一段配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
""