首页 >> 大全

三万字盘点Spring/Boot的那些常用扩展点

2023-07-10 大全 52 作者:考证青年

通过@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

""

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了