在上一篇,我们了解SpringApplication初始化的过程中提到,在初始化的时候,获取了三种类型的拓展点,这一篇,我们来看看BootstrapRegistryInitializer这个拓展点的具体应用。
1、BootstrapRegistryInitializer的作用
在初始化SpringApplication的时候,获取了BootstrapRegistryInitializer的实现(虽然我们挑选的demo中并没有对应的实现),那它的作用是啥呢?为什么要整这么个东西?带着这个疑问,我们先来看看BootstrapRegistryInitializer接口的注释
这个接口的注释出奇的简单,以至于一眼可能看不出它的具体作用。以下是简单的翻译
类注释表明,这个一个回调接口,可用于在使用之前初始化 {@link BootstrapRegistry}。
initialize方法注释表明该方法使用任何所需的注册初始化给定的 {@link BootstrapRegistry}。
显然,这个接口的主要作用就是用于初始化BootstrapRegistry,也就是最终是要通过操作BootstrapRegistry来达到相应的目的。那么BootstrapRegistry才是重点。所以我们继续看看BootstrapRegistry接口。因为BootstrapRegistry注释较多,所以我自己简单翻译下(tips:注释最好还是自己看一下,每个人的理解不同,翻译出来的可能也不同),其中一些比较核心的也均通过颜色标注
我们先看开头的注释:
BootstrapRegistry是一个简单的对象注册表,在启动期间以及{@link Environment}后处理阶段一直可用,直到{@link ApplicationContext}被准备好为止。
可用于注册可能创建成本较高的实例,或在{@link ApplicationContext}可用之前需要共享的实例。
该注册表使用{@link Class}作为键,这意味着只能存储给定类型的单个实例。
{@link #addCloseListener(ApplicationListener)} 方法可用于添加一个监听器,该监听器可以在 {@link BootstrapContext} 被关闭且 {@link ApplicationContext} 完全准备好时执行某些操作。例如,某个实例可以选择将自己注册为常规的 Spring bean,以便应用程序可以使用它。
这个接口定义了下面几个方法
其大致说明如下:
| 方法名 | 说明 |
|---|---|
| register | 将特定类型注册到注册表中。如果指定的类型已经被注册且尚未作为 {@link Scope#SINGLETON 单例} 获取,它将被替换。 |
| registerIfAbsent | 如果注册表中尚未存在该类型,则将特定类型注册到注册表中。 |
| isRegistered | 判断给定的类型是否已注册 |
| getRegisteredInstanceSupplier | 返回给定类型的任何现有 {@link InstanceSupplier} |
| addCloseListener | 添加一个 {@link ApplicationListener},该监听器将在 {@link BootstrapContext} 被关闭且 {@link ApplicationContext} 已准备好时,使用 {@link BootstrapContextClosedEvent} 被调用。 |
看完注释,我们大概能知道个一二:BootstrapRegistryInitializer用于往BootstrapRegistry注册一些创建成本比较高或在ApplicationContext准备好之前要用到的实例(这里如果后面阅读完整个启动流程,可能会更好理解,后面阅读的时候也可以想想为什么springboot要这么设计)。
也就是说:如果我们要自定义BootstrapRegistryInitializer,在实现initialize方法时,主要是通过调用BootstrapRegistry的方法,来往springboot中去注册实例。有了大概的思路后,我们就可以实现自己的BootstrapRegistryInitializer
2、自定义BootstrapRegistryInitializer
为了不跟原项目混淆,我们选择新建一个spring-demo模块,专门用于我们自己的demo编写。
2.1、新建模块
将新建模块的目录下删除感觉,然后用同样的方法再建一个子模块,demo-bootstrap-registry-initializer,
在把原来spring-boot-smoke-test-tomcat下的代码拷贝过来,最终形成下面这样的:
我们要做的,是把一个User对象通过BootstrapRegistryInitializer注册上去
2.2 新增user对象
对象非常简单,就两属性
public class User {
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
2.3 实现BootstrapRegistryInitializer
我们直接自定义MyBootstrapRegistryInitializer,实现BootstrapRegistryInitializer接口,实现也很简单:
initialize方法执行的时候,先往控制台打印一行文本,然后将实例化好的user实例通过registry注册上去。
public class MyBootstrapRegistryInitializer implements BootstrapRegistryInitializer {
@Override
public void initialize(BootstrapRegistry registry) {
System.out.println("MyBootstrapRegistryInitializer=============initialize");
User user = new User();
user.setAge(100);
user.setName("Wiggin");
registry.register(User.class, InstanceSupplier.of(user));
}
}
2.4 配置spring.factories
之前看代码的时候,我们已经了解过springboot是怎么去加载BootstrapRegistryInitializer的了,所以我们只要照葫芦画瓢,在项目中新建spring.factories即可
内容如下:
org.springframework.boot.BootstrapRegistryInitializer=org.springframework.boot.demo.initializer.MyBootstrapRegistryInitializer
2.5 运行项目
做完上面的工作之后,我们就可以运行项目,看看控制台有没有打印。
大家可以看到,运行完成控制台打印的第一行就是我们自定义BootstrapRegistryInitializer中实现的。
通过打印的顺序可以看到BootstrapRegistryInitializer调用的时机应该是挺早的。
3、 通过BootstrapRegistryInitializer注册的对象怎么使用?
刚刚我们自定义的程序确实实现了注册,但是一般我们往springboot中注册些什么东西都是为了后面使用,但是现在我们只完成了注册,注册完该怎么使用呢?有些同学可能会想当然的以为:都注册上去了,我直接通过getBean拿不就可以了吗?那我们来试试:
为了方便,我们直接在main方法中获取context,并从context中获取bean
public static void main(String[] args) {
// main方法入口,通过SpringApplication.run启动springboot,核心入参就是这个类的class,加上启动时传入的启动参数
ConfigurableApplicationContext context = SpringApplication.run(SampleTomcatApplication.class, args);
User user = context.getBean(User.class);
System.out.println("userName==============="+user.getName());
}
这会再去运行,大家就会惊奇的发现,居然报找不到bean:
No qualifying bean of type 'org.springframework.boot.demo.model.User' available
为什么我明明注册上去了,但是想拿出来用的时候就找不到呢?大家应该还记得文字开头,我们看注释的时候说过:
BootstrapRegistryInitializer是可用于注册可能创建成本较高的实例,或在{@link ApplicationContext}可用之前需要共享的实例。且在启动期间以及{@link Environment}后处理阶段一直可用,直到{@link ApplicationContext}被准备好为止。
其实问题就出在这,一般情况下,通过BootstrapRegistryInitializer注册上去的bean,是在启动期间到ApplicationContext准备好的时候可以用,也就是说:当ApplicationContext准备好之后,注册上去的bean就不能用了。
其实,注册上去的bean想用也可以,springboot在启动的过程中(后续深入了解启动过程会涉及到),或发布很多事件,你对哪些事件感兴趣,你就可以实现对应的监听器,一旦事件触发,就会告诉你。我们可以通过实现SpringApplicationRunListener来进行监听,SpringApplicationRunListener接口中有很多方法,对应启动过程中的多个阶段,这里我们只实现starting方法,在其中获取我们之前注册上去的bean
public class MyApplicationRunListener implements SpringApplicationRunListener {
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("====================启动中");
boolean registered = bootstrapContext.isRegistered(User.class);
if (registered) {
User user = bootstrapContext.get(User.class);
System.out.println("userName==============="+user.getName());
}
}
}
同样的,要将监听器加到配置文件spring.factories中
org.springframework.boot.SpringApplicationRunListener=org.springframework.boot.demo.listener.MyApplicationRunListener
运行项目,可以看到控制台输出了:
4、项目启动完后如果还要一开始注册的bean,怎么办?
通过上面的demo,我们验证了:默认情况下通过BootstrapRegistryInitializer注册上去的bean,是在启动期间到ApplicationContext准备好的时候可以用,也就是说:当ApplicationContext准备好之后,注册上去的bean就不能用了。但是,如果这个bean后续我应用起来之后,还要用,这咋整?总不能再创一个吧?
在看注释的时候,注释也告诉我们这种情况该如何处理:
{@link #addCloseListener(ApplicationListener)} 方法可用于添加一个监听器,该监听器可以在 {@link BootstrapContext} 被关闭且 {@link ApplicationContext} 完全准备好时执行某些操作。例如,某个实例可以选择将自己注册为常规的 Spring bean,以便应用程序可以使用它。
也是通过添加监听器,在监听到BootstrapContext被关闭的时候,我们拿到bean,再把它注册到ApplicationContext就可以了。下面我们来试试
4.1 实现ApplicationListener
实现也非常简单:从BootstrapContext获取我们一开始注册的bean,然后添加到ApplicationContext的BeanFactory中
public class MyBootstrapContextClosedEvent implements ApplicationListener<BootstrapContextClosedEvent> {
@Override
public void onApplicationEvent(BootstrapContextClosedEvent event) {
System.out.println("====================MyBootstrapContextClosedEvent");
User user = event.getBootstrapContext().get(User.class);
event.getApplicationContext().getBeanFactory()
.registerSingleton("user",user);
}
}
修改MyBootstrapRegistryInitializer,在注册之后添加监听器
public class MyBootstrapRegistryInitializer implements BootstrapRegistryInitializer {
@Override
public void initialize(BootstrapRegistry registry) {
System.out.println("MyBootstrapRegistryInitializer=============initialize");
User user = new User();
user.setAge(100);
user.setName("Wiggin");
registry.register(User.class, InstanceSupplier.of(user));
// 添加监听器
registry.addCloseListener(new MyBootstrapContextClosedEvent());
}
}
修改main方法,继续用回一开始获取bean失败的代码,看看这次是否能成功获取
public static void main(String[] args) {
// main方法入口,通过SpringApplication.run启动springboot,核心入参就是这个类的class,加上启动时传入的启动参数
ApplicationContext context = SpringApplication.run(SampleTomcatApplication.class, args);
User bean = context.getBean(User.class);
System.out.println(bean.getName());
}
运行项目,查看控制台:
可以看到打印完banner之后,触发了MyBootstrapContextClosedEvent(这里的触发顺序对我们后面理解源码很有帮助)
项目完全启动后,也可以拿到bean,并打印出其name属性
同样的,大家也可以自己实现另外两个扩展点,快动手试试吧。