springboot中的第二个IOC容器BootstrapContext,超有用的

往期文章

一、前言

用过spring的小伙伴想必都知道IOC容器吧,spring将我们的单例对象实例化后保存到IOC容器,而且一说到IOC容器,大家第一反应都是ApplicationContext,读过源码的同学还知道IOC容器中的单例对象都是保存在一个Map集合singletonObjects中的,而且是以beanName为key,单例对象为value的Map集合。

但是目前大家用的肯定都是springboot了,如果面试官问你springboot中的IOC容器是什么?而你还像上面那样照本宣科按照spring的回答,那你就错了。

正解:在springboot刚启动时,IOC容器为BootstrapContext,直到ApplicationContext初始化完成时,IOC容器为ApplicationContext

是的,springboot有不仅一个ApplicationContextIOC容器,他还有另外一个IOC容器,即BootstrapContext

对标配置文件application.ymlbootstrap.yml,不难知道ApplicationContextBootstrapContext的关系:

  • ApplicationContext是常规的IOC容器
  • BootstrapContext是项目启动前的IOC容器

ApplicationContextIOC容器相同的是,BootstrapContext容器内部也是通过一个Map集合保存单例对象的。但不同的是,在这个Map集合中是以单例对象的类型为key,单例对象为value来保存的,也正是这个原因,所以一定是单例对象。

二、介绍

BootstrapContext是一个引导上下文。我们知道ApplicationContext是在项目启动过程中完成初始化、注册bean等一系列操作的,但BootstrapContext是在项目启动前就完成了。另外在BootstrapContext保存的不是真实的单例对象,而是该单例对象的Bean工厂(InstanceSupplier),当我们第一次获取单例对象时都是从bean工厂中获取的(InstanceSupplier)。下面是spring官方对他的解释:

一个简单的对象注册表,在启动和环境后处理期间可用,直到准备好ApplicationContext为止。 可用于注册创建成本较高的实例,或者在ApplicationContext可用之前需要共享的实例。 注册表使用Class作为键,这意味着只能存储给定类型的单个实例。 addCloseListener(ApplicationListener)方法可用于添加一个侦听器,该侦听器可以在BootstrapContext已关闭且ApplicationContext已完全准备就绪时执行操作。例如,实例可以选择将自己注册为常规Spring bean,以便应用程序可以使用它。

三、源码

1. BootstrapContext容器

BootstrapContext是一个IOC容器,我们看一下它的小白脸长啥样

public interface BootstrapContext {
    
    

	// 从容器中获取单例对象
	<T> T get(Class<T> type) throws IllegalStateException;

	// 从容器中获取单例对象,如果不存在,则返回other
	<T> T getOrElse(Class<T> type, T other);

	// 从容器中获取单例对象,如果不存在,则从other中获取,可以认为它是一个FactoryBean
	<T> T getOrElseSupply(Class<T> type, Supplier<T> other);

	// 从容器中获取单例对象,如果不存在,则从exceptionSupplier中获取一个异常抛出来
	<T, X extends Throwable> T getOrElseThrow(Class<T> type, Supplier<? extends X> exceptionSupplier) throws X;

	// 判断指定类型是否已注册到容器
	<T> boolean isRegistered(Class<T> type);

}

我们看到BootstrapContext容器只定义了获取对象的方法,那如何向容器中注册呢?这个任务交给BootstrapRegistry

2. BootstrapRegistry注册器

BootstrapRegistry是用来向BootstrapContext容器中注册单例对象的注册器,看一下它的小屁股长啥样

public interface BootstrapRegistry {
    
    

	// 向容器中注册实例,如果容器中已存在该类型对应的实例,会发生覆盖
	<T> void register(Class<T> type, InstanceSupplier<T> instanceSupplier);

	// 向容器中注册实例,如果容器中已存在该类型对应的实例,则取消注册,即不发生覆盖
	<T> void registerIfAbsent(Class<T> type, InstanceSupplier<T> instanceSupplier);

	// 判断指定类型是否已注册到容器
	<T> boolean isRegistered(Class<T> type);

	// 从容器中获取指定类型对应的InstanceSupplier实例
	<T> InstanceSupplier<T> getRegisteredInstanceSupplier(Class<T> type);

	// 添加BootstrapContextClosedEvent事件的监听器
	void addCloseListener(ApplicationListener<BootstrapContextClosedEvent> listener);

	// 可理解为FactoryBean
	@FunctionalInterface
	interface InstanceSupplier<T> {
    
    

		T get(BootstrapContext context);

		default Scope getScope() {
    
    
			return Scope.SINGLETON;
		}

		default InstanceSupplier<T> withScope(Scope scope) {
    
    
			Assert.notNull(scope, "Scope must not be null");
			InstanceSupplier<T> parent = this;
			return new InstanceSupplier<T>() {
    
    

				@Override
				public T get(BootstrapContext context) {
    
    
					return parent.get(context);
				}

				@Override
				public Scope getScope() {
    
    
					return scope;
				}
			};
		}

        // 将instance实例放在InstanceSupplier中
		static <T> InstanceSupplier<T> of(T instance) {
    
    
			return (registry) -> instance;
		}

		static <T> InstanceSupplier<T> from(Supplier<T> supplier) {
    
    
			return (registry) -> (supplier != null) ? supplier.get() : null;
		}
	}

	enum Scope {
    
    
		// 单例
		SINGLETON,
		// 原型
		PROTOTYPE
	}
}

看到这里,我们应该对这两个接口有个基本了解

  • BootstrapContext定义了从容器中获取实例的方法
  • BootstrapRegistry定义了向容器中注册实例的方法。

那具体是怎么实现的呢?下面登场的就是他们的实现类DefaultBootstrapContext

3. DefaultBootstrapContext

先了解一下他的UML图

在这里插入图片描述

其中ConfigurableBootstrapContext接口继承自上面两个接口,它没有定义任何扩展的方法,仅仅是对获取实例注册实例的汇总。

因此我们可以认为,DefaultBootstrapContext就是一个支持注册和获取的IOC容器

public class DefaultBootstrapContext implements ConfigurableBootstrapContext {
    
    
	// 保存单例对象的bean工厂InstanceSupplier
	private final Map<Class<?>, InstanceSupplier<?>> instanceSuppliers = new HashMap<>();
	// 保存单例对象。采用惰性思想,当首次访问单例对象时,从InstanceSupplier获取并保存
	private final Map<Class<?>, Object> instances = new HashMap<>();
	// 监听器集合
	private final ApplicationEventMulticaster events = new SimpleApplicationEventMulticaster();
}

四、实践

BootstrapContext容器的源码及其作用有了了解后,我们通过demo对其进行演示。

1. 定义项目启动前需要处理的方法

新建两个类:ComponentAComponentB,我们需要在项目启动前,让这两个类做一些事情,如step1()step2()

public class ComponentA {
    
    
    
    public ComponentA() {
    
    
        System.out.println("ComponentA实例化");
    }

    public void step1() {
    
    
        System.out.println("ComponentA.step1()");
    }

    public void step2() {
    
    
        System.out.println("ComponentA.step2()");
    }
}

public class ComponentB {
    
    
    
    public ComponentB() {
    
    
        System.out.println("ComponentB实例化");
    }

    public void step1() {
    
    
        System.out.println("ComponentB.step1()");
    }

    public void step2() {
    
    
        System.out.println("ComponentB.step2()");
    }
}

2. 定义引导类,注册组件到Bootstrap容器

新建一个类:ComponentInit,并实现Bootstrapper接口中的intitialize()方法,将我们定义的ComponentAComponentB注册到容器中。

public class ComponentInit implements Bootstrapper {
    
    

    @Override
    public void intitialize(BootstrapRegistry registry) {
    
    
        System.out.println("向BootStrapContext中注册bean");

        // 向bootstrapContext容器中注册组件A
        ComponentA componentA = new ComponentA();
        System.out.println("componentA:" + componentA);
        registry.register(ComponentA.class, (bootstrapContext) -> componentA);

        // 向bootstrapContext容器中注册组件B
        ComponentB componentB = new ComponentB();
        System.out.println("componentB:" + componentB);
        registry.register(ComponentB.class, (bootstrapContext) -> componentB);
    }
}

3. 定义项目启动监听器

新建一个监听器,并实现SpringApplicationRunListener接口。该接口定义了项目启动前的一些过程如:项目开始启动环境准备就绪

public class BootStrapContextListener implements SpringApplicationRunListener {
    
    

    public BootStrapContextListener(SpringApplication springApplication, String[] strings) {
    
    

    }

    // 当项目开始启动时,执行ComponentA和ComponentB的方法
    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
    
    
        System.out.println("================项目启动================");
        ComponentA componentA = bootstrapContext.get(ComponentA.class);
        componentA.step1();

        ComponentB componentB = bootstrapContext.get(ComponentB.class);
        componentB.step1();
    }
	// 当环境准备就绪时,执行ComponentA和ComponentB的方法
    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
    
    
        System.out.println("================环境准备就绪================");
        ComponentA componentA = bootstrapContext.get(ComponentA.class);
        componentA.step2();

        ComponentB componentB = bootstrapContext.get(ComponentB.class);
        componentB.step2();
    }
}

4. 注册引导类

resources目录下新建META-INF/spring.factories

在这里插入图片描述

将引导类注册到该文件中

# 注册引导类
org.springframework.boot.Bootstrapper = \
com.idealhooray.bootstrapContext.bootstrappers.ComponentInit
# 注册项目启动监听器
org.springframework.boot.SpringApplicationRunListener = \
com.idealhooray.bootstrapContext.listeners.BootStrapContextListener

5. 启动项目

运行项目的main()方法,可以看到,在打印横幅前,我们定义的两个组件ComponentAComponentB的方法已经完成实例化并执行方法了。

在这里插入图片描述

6. 将Bootstrap容器中的实例实例作为bean注册到ApplicationContext容器中

在spring的设计中,当ApplicationContext容器完成初始化后,Bootstrap容器需要调用close()方法进行关闭,并且在关闭时发布一个事件BootstrapContextClosedEvent来表示将要关闭Bootstrap容器了。因此我们可以定义一个监听器,当监听到事件BootstrapContextClosedEvent时,将Bootstrap容器中的单例注册到ApplicationContext容器中。

新建一个监听BootstrapContextClosedEvent事件的监听器类BootStrapContextClosedListener,实现ApplicationListener接口。

public class BootStrapContextClosedListener implements ApplicationListener<BootstrapContextClosedEvent> {
    
    

    @Override
    public void onApplicationEvent(BootstrapContextClosedEvent event) {
    
    
        System.out.println("================即将关闭BootStrapContext================");
        // 从BootStrapContext容器中获取componentA和componentB
        BootstrapContext bootstrapContext = event.getBootstrapContext();
        ComponentA componentA = bootstrapContext.get(ComponentA.class);
        ComponentB componentB = bootstrapContext.get(ComponentB.class);
        System.out.println("关闭bootstrapContext");

        // 将componentA和componentB注册到applicationContext容器
        ConfigurableApplicationContext applicationContext = event.getApplicationContext();
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
        beanFactory.registerSingleton("componentA", componentA);
        beanFactory.registerSingleton("componentB", componentB);

        // 验证是否注册成功
        Object beanA = beanFactory.getBean("componentA");
        Object beanB = beanFactory.getBean("componentB");
        if (!ObjectUtils.isEmpty(beanA) && !ObjectUtils.isEmpty(beanB)) {
    
    
            System.out.println("beanA 和 beanB 已成功注册到ApplicationContext IOC容器");

            // 判断applicationContext容器中的beanA和beanB 是否和 BootStrapContext容器中的componentA和componentB 为同一个对象
            System.out.println("componentA:" + beanA);
            System.out.println("componentB:" + beanB);
        }

        System.out.println("启动applicationContext");
    }
}

7. 注册BootstrapContextClosedEvent事件的监听器

在前面的引导类ComponentInit中,我们已经将ComponetAComponentB注册到BootstrapContext容器中了,现在需要向该容器中注册监听器

public class ComponentInit implements Bootstrapper {
    
    

    @Override
    public void intitialize(BootstrapRegistry registry) {
    
    
        // ...
        
        // 注册监听器
        registry.addCloseListener(new BootStrapContextClosedListener());
    }
}

8. 启动项目

运行项目的main()方法,可以看到,在打印横幅前,我们定义的两个组件ComponentAComponentB的方法已经完成实例化并执行方法了。在打印横幅后,这时applicationContext容器已经完成了初始化,我们可以看到beanAbeanB成功注册ApplicationContext IOC容器中了,而且从打印出的内存地址来看beanA和ComponentA是同一个对象,beanB和ComponentB是同一个对象,完全正确。

在这里插入图片描述

五、为什么这么做?

为什么?当然是读源码知道的。

1. 为什么定义实现Bootstrapper接口的引导类

在springboot启动过程中,会获取所有Bootstrapper接口实现类的集合。

在这里插入图片描述

然后通过Bootstrapper接口实现类的intitialize()方法,对BootstrapContext容器进行初始化

在这里插入图片描述

2. 为什么要将引导类注册到spring.factories文件中

如上图,在获取所有Bootstrapper接口实现类的集合时,是通过getSpringFactoriesInstances()方法获取的。而在该方法中,会从spring.factories文件中获取所有的Bootstrapper接口实现类。

在这里插入图片描述

FACTORIES_RESOURCE_LOCATION变量即为META-INF/spring.factories.

在这里插入图片描述

3. 为什么定义BootStrapContextClosedListener监听器

在springboot项目启动过程中,会对ApplicationContext容器执行大量的代码逻辑,其中在做准备工作时,调用BootstrapContextclose()方法将其关闭,而在关闭BootstrapContext的方法中,发布BootstrapContextClosedEvent事件,具体操作由该事件对应的监听器执行。

在这里插入图片描述

在关闭容器时发布事件

在这里插入图片描述

而我们定义的监听器正好监听的就是这个事件。

在这里插入图片描述

六、总结

ApplicationContext初始化前,通过BootstrapContext创建一些组件实例(需要的话),等ApplicationContext初始化后,再将BootstrapContext创建创建的实例注册到ApplicationContext中作为常规的bean供项目使用。



纸上得来终觉浅,绝知此事要躬行。

——————我是万万岁,我们下期再见——————

猜你喜欢

转载自blog.csdn.net/qq_36234720/article/details/129995414
今日推荐