springboot中的第二个IOC容器BootstrapContext

简介: 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图

DefaultBootstrapContext的UML图.png

其中ConfigurableBootstrapContext接口继承自上面两个接口,是对获取实例注册实例的汇总,但它没有做出更多的扩展。实现类DefaultBootstrapContext是springboot提供的默认的唯一的bootstrapIOC容器。他对IOC容器实现了所有获取bean、注册bean以及其他相关的方法。

public class DefaultBootstrapContext implements ConfigurableBootstrapContext {
   
   
    // InstanceSupplier调用get方法得到的对象将放在instances集合中
    private final Map<Class<?>, InstanceSupplier<?>> instanceSuppliers = new HashMap<>();

    private final Map<Class<?>, Object> instances = new HashMap<>();
    // 事件广播器,将事件广播出去后,监听该事件的监听器将做出反应
    private final ApplicationEventMulticaster events = new SimpleApplicationEventMulticaster();

    @Override
    public <T> void register(Class<T> type, InstanceSupplier<T> instanceSupplier) {
   
   
        register(type, instanceSupplier, true);
    }

    @Override
    public <T> void registerIfAbsent(Class<T> type, InstanceSupplier<T> instanceSupplier) {
   
   
        register(type, instanceSupplier, false);
    }

    private <T> void register(Class<T> type, InstanceSupplier<T> instanceSupplier, boolean replaceExisting) {
   
   
        Assert.notNull(type, "Type must not be null");
        Assert.notNull(instanceSupplier, "InstanceSupplier must not be null");
        synchronized (this.instanceSuppliers) {
   
   
            boolean alreadyRegistered = this.instanceSuppliers.containsKey(type);
            if (replaceExisting || !alreadyRegistered) {
   
   
                Assert.state(!this.instances.containsKey(type), () -> type.getName() + " has already been created");
                this.instanceSuppliers.put(type, instanceSupplier);
            }
        }
    }

    @Override
    public <T> boolean isRegistered(Class<T> type) {
   
   
        synchronized (this.instanceSuppliers) {
   
   
            return this.instanceSuppliers.containsKey(type);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> InstanceSupplier<T> getRegisteredInstanceSupplier(Class<T> type) {
   
   
        synchronized (this.instanceSuppliers) {
   
   
            return (InstanceSupplier<T>) this.instanceSuppliers.get(type);
        }
    }

    @Override
    public void addCloseListener(ApplicationListener<BootstrapContextClosedEvent> listener) {
   
   
        this.events.addApplicationListener(listener);
    }

    @Override
    public <T> T get(Class<T> type) throws IllegalStateException {
   
   
        return getOrElseThrow(type, () -> new IllegalStateException(type.getName() + " has not been registered"));
    }

    @Override
    public <T> T getOrElse(Class<T> type, T other) {
   
   
        return getOrElseSupply(type, () -> other);
    }

    @Override
    public <T> T getOrElseSupply(Class<T> type, Supplier<T> other) {
   
   
        synchronized (this.instanceSuppliers) {
   
   
            InstanceSupplier<?> instanceSupplier = this.instanceSuppliers.get(type);
            return (instanceSupplier != null) ? getInstance(type, instanceSupplier) : other.get();
        }
    }

    @Override
    public <T, X extends Throwable> T getOrElseThrow(Class<T> type, Supplier<? extends X> exceptionSupplier) throws X {
   
   
        synchronized (this.instanceSuppliers) {
   
   
            InstanceSupplier<?> instanceSupplier = this.instanceSuppliers.get(type);
            if (instanceSupplier == null) {
   
   
                throw exceptionSupplier.get();
            }
            return getInstance(type, instanceSupplier);
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T getInstance(Class<T> type, InstanceSupplier<?> instanceSupplier) {
   
   
        T instance = (T) this.instances.get(type);
        if (instance == null) {
   
   
            instance = (T) instanceSupplier.get(this);
            if (instanceSupplier.getScope() == Scope.SINGLETON) {
   
   
                this.instances.put(type, instance);
            }
        }
        return instance;
    }

    /**
     * Method to be called when {@link BootstrapContext} is closed and the
     * {@link ApplicationContext} is prepared.
     * @param applicationContext the prepared context
     */
    public void close(ConfigurableApplicationContext applicationContext) {
   
   
        this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext));
    }

}

从源码中我们也可以看到,在容器中无论获取bean还是注册bean,都使用了synchronized修饰的代码块,因此该IOC容器还是线程安全的。

讲了上面两个接口一个实现类后,我们对该IOC容器的原理已经有了认识,但如何使用呢?

使用

其中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

spring-factories.png

将引导类注册到该文件中

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

5. 启动项目

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

输出1.png

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是同一个对象,完全正确。

输出2.png

五、为什么这么做?

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

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

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

寻找Bootstrapper接口的实现类集合.png

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

使用引导类对BootstrapContext进行初始化.png

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

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

从spring-factories文件中获取实现类.png

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

spring-factories文件位置.png

3. 为什么定义BootStrapContextClosedListener监听器

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

关闭BootstrapContext容器.png

在关闭容器时发布事件

发布事件.png

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

BootstrapContext容器关闭监听器.png

六、总结

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




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

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

相关文章
|
1月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
1月前
|
XML Java 数据格式
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
这篇文章是Spring5框架的入门教程,详细讲解了IOC容器中Bean的自动装配机制,包括手动装配、`byName`和`byType`两种自动装配方式,并通过XML配置文件和Java代码示例展示了如何在Spring中实现自动装配。
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
|
1月前
|
XML Java 数据格式
Spring5入门到实战------2、IOC容器底层原理
这篇文章深入探讨了Spring5框架中的IOC容器,包括IOC的概念、底层原理、以及BeanFactory接口和ApplicationContext接口的介绍。文章通过图解和实例代码,解释了IOC如何通过工厂模式和反射机制实现对象的创建和管理,以及如何降低代码耦合度,提高开发效率。
Spring5入门到实战------2、IOC容器底层原理
|
1月前
|
XML Java 数据格式
Spring5入门到实战------8、IOC容器-Bean管理注解方式
这篇文章详细介绍了Spring5框架中使用注解进行Bean管理的方法,包括创建Bean的注解、自动装配和属性注入的注解,以及如何用配置类替代XML配置文件实现完全注解开发。
Spring5入门到实战------8、IOC容器-Bean管理注解方式
|
1月前
|
Java Spring 容器
建模底层逻辑问题之以Spring IOC容器为例,使用因果法建模,如何操作
建模底层逻辑问题之以Spring IOC容器为例,使用因果法建模,如何操作
|
1月前
|
XML Dubbo Java
Spring之Ioc容器
该文章主要介绍了Spring框架中的IoC(Inversion of Control,控制反转)容器,包括IoC容器的概念、IoC容器在Spring中的实现以及IoC容器的基础包等内容。
Spring之Ioc容器
|
2月前
|
前端开发 Java 应用服务中间件
Spring Boot 2.x 嵌入式 Servlet 容器
Spring Boot使用内嵌Tomcat,默认端口8080,可通过`application.properties`配置端口、上下文路径等。配置方式有两种:1) 直接在配置文件中添加`server.port`和`server.servlet.context-path`;2) 创建`WebServerFactoryCustomizer` Bean来自定义配置,如设置端口`factory.setPort(8083)`,这种方式优先级更高。
|
2月前
|
Java Spring 容器
深入理解Spring Boot中的容器与依赖注入
深入理解Spring Boot中的容器与依赖注入
|
5天前
|
弹性计算 运维 持续交付
探索Docker容器化技术及其在生产环境中的应用
探索Docker容器化技术及其在生产环境中的应用
31 5
|
16天前
|
NoSQL 关系型数据库 Redis
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo
mall在linux环境下的部署(基于Docker容器),docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongodb、minio详细教程,拉取镜像、运行容器
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo