SpringBoot源码之:预备知识

139 阅读11分钟

本系列文章中的源码均以SpringBoot的2.5.1版本为准

1. 概述

我们知道,SpringBoot的最简单的启动程序如下所示:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

上面的程序会根据DemoApplication配置类来创建一个Spring容器,并在Spring容器刷新时启动web容器
为了证明这个结论,读者不妨自己通过SpringBoot来写一个简单的SSM整合的demo,并通过如下程序来启动整个项目:

@MapperScan
@SpringBootApplication
@PropertySource("classpath:application.properties")
public class DemoApplication {

    /**
     * 创建一个AnnotationConfigServletWebServerApplicationContext类型的Spring容器
     * 这个Spring容器会在构造时自动刷新,并且会在刷新时自动启动web容器
     * 还需要注意的是,如果在配置文件中自定义了一些配置,则此时需要手动通过@PropertySource注解来加载配置文件
     */
    public static void main(String[] args) {
        new AnnotationConfigServletWebServerApplicationContext(DemoApplication.class);
    }
}

我们发现,项目依旧能正常运行,这就说明SpringApplication的静态run()方法的主要功能就是创建Spring容器并启动web容器

2. 属性绑定相关

2.1. Binder

这里我们不用关心Binder的源码,而是通过一个简单的示例来了解一下该组件的使用方式:

@Data
public class User {
    private Long userId;
    private String userName;
    private Timestamp createTime;
    private List<String> friends;
}
public class BinderDemo {
    public static void main(String[] args) {
        
        // 先创建一个Map集合,用于存放用户的信息
        // 注意,这里的user-id和createtime属性都不是标准的下划线命名,但最终还是能绑定成功的(原因稍后再说)
        HashMap<String, Object> source = new HashMap<>();
        source.put("user.user-id", "123");
        source.put("user.user_name", "NightDW");
        source.put("user.createtime", "2020-01-01 00:00:00");
        source.put("user.friends", "zs,ls, ww,zl");

        // 创建一个Environment对象,并将上面的Map集合封装成一个属性源,然后将该属性源添加到Environment对象中
        StandardEnvironment environment = new StandardEnvironment();
        environment.getPropertySources().addLast(new MapPropertySource("userData", source));

        // 根据Environment对象来创建一个Binder实例
        Binder binder = Binder.get(environment);

        // 将名称以"user"开头的所有属性绑定到User类上
        // 这里会先创建一个User实例,然后将user.xxx属性的值赋给该User实例的相应字段
        User user = binder.bind("user", User.class).get();

        // 打印:User(userId=123, userName=NightDW, createTime=2020-01-01 00:00:00.0, friends=[zs, ls, ww, zl])
        System.out.println(user);

        // 也可以先创建一个User实例,然后再对该实例进行属性绑定
        // 此时bind()方法返回的实例就是我们自己创建的User实例
        User user1 = new User();
        User user2 = binder.bind("user", Bindable.ofInstance(user1)).get();

        // 打印:User(userId=123, userName=NightDW, createTime=2020-01-01 00:00:00.0, friends=[zs, ls, ww, zl])
        System.out.println(user1);

        // 打印:true
        System.out.println(user1 == user2);
    }
}

2.2. ConfigurationPropertySourcesPropertySource

ConfigurationPropertySourcesPropertySource是一个属性源装饰器,负责对已有的属性源集合进行增强
我们知道,配置文件中的属性名称支持下划线命名中划线命名驼峰命名,甚至可以是字段名的小写形式
而该装饰器就是用来处理属性命名的问题的;它可以将实体类的字段名称和属性源中的以各种方式命名的属性名称对应起来
正因为如此,Binder组件才可以将属性源中的user-idcreatetime属性映射到userIdcreateTime字段
该装饰器主要是为了方便@ConfigurationProperties注解进行属性绑定(从该属性源的类名就可以看出来)

@ConfigurationProperties注解在进行属性绑定时,会将实体类的字段名称转成中划线命名,如下所示:

class JavaBeanBinder implements DataObjectBinder {

    /**
     * 本类用于存储实体类的字段信息
     */
    static class BeanProperty {

        /**
         * 构造器;这里的name参数代表get/set/is方法对应的字段名称
         * DataObjectPropertyName的toDashedForm()方法会将驼峰/下划线/中划线命名统一转成中划线命名
         * 即:该方法会将"userId"/"user_id"/"user-id"统一转成"user-id";如果入参是"userid",则返回"userid"
         */
        BeanProperty(String name, ResolvableType declaringClassType) {
            this.name = DataObjectPropertyName.toDashedForm(name);
            this.declaringClassType = declaringClassType;
        }
    }
}

@ConfigurationProperties注解在进行属性绑定时,会通过该装饰器来获取目标字段的名称(中划线形式)对应的值
这里我们直接通过测试程序来展示一下ConfigurationPropertySourcesPropertySource装饰器的作用:

public class ConfigurationPropertySourcesPropertySourceDemo {

    /**
     * 将指定键值对存到Map集合中,并将该Map封装成一个属性源并返回
     */
    private static PropertySource<?> getUserPropertySource(String key, Object value) {
        Map<String, Object> map = new HashMap<>();
        map.put(key, value);
        return new MapPropertySource("user", map);
    }

    /**
     * 将指定键值对封装成普通的属性源,并通过该属性源来获取user-id字段对应的值
     */
    private static void showUserIdByNormal(String key, Object value) {
        System.out.println(getUserPropertySource(key, value).getProperty("user-id"));
    }

    /**
     * 将指定键值对封装成ConfigurationPropertySourcesPropertySource属性源,并通过该属性源来获取user-id字段对应的值
     */
    private static void showUserIdByConfiguration(String key, Object value) {
        
        // 先创建一个普通的属性源
        PropertySource<?> propertySource = getUserPropertySource(key, value);

        // 将该属性源添加到Environment对象底层的属性源集合中
        StandardEnvironment environment = new StandardEnvironment();
        environment.getPropertySources().addFirst(propertySource);

        // 为该Environment对象新增一个ConfigurationPropertySourcesPropertySource类型的属性源
        ConfigurationPropertySources.attach(environment);

        // 获取到刚刚新增的属性源,并通过该属性源来获取user-id字段对应的值
        propertySource = environment.getPropertySources().get("configurationProperties");
        System.out.println(propertySource.getProperty("user-id"));
    }

    /**
     * 测试程序
     */
    public static void main(String[] args) {
        
        // 当配置文件中的属性名称是"userid"时,通过普通属性源是获取不到user-id字段对应的值的;这里打印:null
        showUserIdByNormal("userid", 1);
        
        // 同理,这两行代码的打印结果也为:null
        showUserIdByNormal("userId", 2);
        showUserIdByNormal("user_id", 3);
        
        // 当配置文件中的属性名称是"user-id"时,通过普通属性源可以获取到user-id字段对应的值;这里打印:4
        showUserIdByNormal("user-id", 4);
        
        // 当通过装饰器属性源来获取user-id字段对应的值时,不管属性名是什么命名方式的,都可以获取到相应的值;这里会分别打印相应的数字
        showUserIdByConfiguration("userid", 1);
        showUserIdByConfiguration("userId", 2);
        showUserIdByConfiguration("user_id", 3);
        showUserIdByConfiguration("user-id", 4);
    }
}

3. Instantiator

为了方便后续的源码阅读,我们先来了解一下Instantiator组件的使用:

/**
 * 自定义一个接口;为了方便,该接口并没有定义任何方法
 */
interface MyComponent {
}

/**
 * 实现自定义的接口;该实现类的构造方法需要接收一个Integer类型的参数
 */
class IntComponent implements MyComponent {
    public IntComponent(Integer param) {
        System.out.println("IntComponent构造方法接收到整数参数 -> " + param);
    }
}

/**
 * 实现自定义的接口;该实现类的构造方法需要接收一个String类型的参数
 */
class StringComponent implements MyComponent {
    public StringComponent(String param) {
        System.out.println("StringComponent构造方法接收到字符串参数 -> " + param);
    }
}
public class InstantiatorDemo {

    /**
     * 测试程序
     */
    public static void main(String[] args) {
        
        // 创建一个Instantiator实例,该组件可以同时为某个接口(或父类)的多个子类创建实例;这里需要指定两个参数:
        // 1. 第一个参数代表要创建的实例的类型,一般传接口(或父类);这里传的是MyComponent.class
        // 2. 第二个参数代表参数注册中心的处理器,可以通过它来往参数注册中心注册一些在构造实例时可能会用到的一些参数
        Instantiator<MyComponent> instantiator = new Instantiator<>(MyComponent.class, availableParameters -> {
            
            // 注册一个Integer类型的参数,其值为123
            availableParameters.add(Integer.class, 123);
            
            // 注册一个String类型的参数工厂;该工厂会根据目标Class对象(即这里的MyComponent.class)来生成相应的字符串
            availableParameters.add(String.class, Class::getSimpleName);
        });

        // 获取到要实例化的类的全限定类名
        List<String> classNames = Arrays.asList(IntComponent.class.getName(), StringComponent.class.getName());
        
        // 通过Instantiator组件来创建这些类的实例,这里的打印结果如下:
        // IntComponent构造方法接收到整数参数 -> 123
        // StringComponent构造方法接收到字符串参数 -> MyComponent
        List<MyComponent> myComponents = instantiator.instantiate(classNames);
    }
}

4. DefaultBootstrapContext

4.1. 概述

SpringApplication在创建Spring容器之前,会先创建一个DefaultBootstrapContext实例
DefaultBootstrapContext相当于一个轻量级的Spring容器,会在Spring容器准备完成前临时起到Spring容器的作用
DefaultBootstrapContext主要用于存放一些在Spring容器准备完成前就需要被共享的组件

/**
 * 本类实现了ConfigurableBootstrapContext接口
 * ConfigurableBootstrapContext接口又继承自BootstrapRegistry和BootstrapContext接口
 * 本类是这3个接口的唯一实现
 */
public class DefaultBootstrapContext implements ConfigurableBootstrapContext {

    /**
     * 注册中心;key为Bean的类型,value为对应的Bean工厂(这么做是为了支持懒加载和多例)
     */
    private final Map<Class<?>, InstanceSupplier<?>> instanceSuppliers = new HashMap<>();

    /**
     * 用于缓存已经创建出来了的单例Bean,避免重复创建;key为Bean的类型,value为Bean本身
     */
    private final Map<Class<?>, Object> instances = new HashMap<>();

    /**
     * 事件广播器;当本组件关闭时,本组件会通过该事件广播器来发布一个BootstrapContextClosedEvent事件
     */
    private final ApplicationEventMulticaster events = new SimpleApplicationEventMulticaster();
}

4.2. 更新方法

public class DefaultBootstrapContext implements ConfigurableBootstrapContext {

    /**
     * 注册新的Bean;会替换掉已有的Bean
     */
    @Override
    public <T> void register(Class<T> type, InstanceSupplier<T> instanceSupplier) {
        register(type, instanceSupplier, true);
    }

    /**
     * 注册Bean(如果不存在的话)
     */
    @Override
    public <T> void registerIfAbsent(Class<T> type, InstanceSupplier<T> instanceSupplier) {
        register(type, instanceSupplier, false);
    }

    /**
     * 真正注册Bean;replaceExisting代表是否替换已有的Bean
     */
    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) {
            
            // 先判断是否已经注册了该类型的Bean
            boolean alreadyRegistered = this.instanceSuppliers.containsKey(type);
            
            // 如果未注册该类型的Bean,或者允许替换已有的Bean,则注册新的Bean
            // 在注册之前,会先判断旧的单例Bean是否已经创建出来了;如果是,则报错,因为这样会破坏单例
            if (replaceExisting || !alreadyRegistered) {
                Assert.state(!this.instances.containsKey(type), () -> type.getName() + " has already been created");
                this.instanceSuppliers.put(type, instanceSupplier);
            }
        }
    }

    /**
     * 注册关闭事件的监听器
     * 这里会将这个监听器注册到事件广播器中
     */
    @Override
    public void addCloseListener(ApplicationListener<BootstrapContextClosedEvent> listener) {
        this.events.addApplicationListener(listener);
    }

    /**
     * 关闭该BootstrapContext;当Spring容器(即这里的applicationContext参数)准备完成后,会调用本方法
     * 这里会通过事件广播器来广播一个BootstrapContextClosedEvent事件,从而执行上面注册的事件监听器
     */
    public void close(ConfigurableApplicationContext applicationContext) {
        this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext));
    }
}

4.3. 查询方法

public class DefaultBootstrapContext implements ConfigurableBootstrapContext {

    /**
     * 判断是否注册了指定类型的Bean
     */
    @Override
    public <T> boolean isRegistered(Class<T> type) {
        synchronized (this.instanceSuppliers) {
            return this.instanceSuppliers.containsKey(type);
        }
    }

    /**
     * 获取指定类型的Bean的工厂
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> InstanceSupplier<T> getRegisteredInstanceSupplier(Class<T> type) {
        synchronized (this.instanceSuppliers) {
            return (InstanceSupplier<T>) this.instanceSuppliers.get(type);
        }
    }

    /**
     * 真正获取Bean
     * 这里会先查询this.instances缓存;如果没有,则通过Bean工厂来获取Bean,并将单例Bean缓存起来
     */
    @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;
    }

    /**
     * 获取指定类型的Bean;要求该Bean必须存在
     */
    @Override
    public <T> T get(Class<T> type) throws IllegalStateException {
        return getOrElseThrow(type, () -> new IllegalStateException(type.getName() + " has not been registered"));
    }

    /**
     * 获取指定类型的Bean;如果不存在,则抛异常
     */
    @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);
        }
    }

    /**
     * 获取指定类型的Bean;如果不存在,则返回给定的默认值
     */
    @Override
    public <T> T getOrElse(Class<T> type, T other) {
        return getOrElseSupply(type, () -> other);
    }

    /**
     * 获取指定类型的Bean;如果不存在,则通过给定的提供者来获取
     */
    @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();
        }
    }
}

5. SpringApplicationRunListener

/**
 * SpringApplication在创建Spring容器的过程中,提供了很多回调机制
 * 而SpringApplicationRunListener就是这些回调机制中的最重要的一个
 * 
 * SpringApplicationRunListener需要注册在spring.factories文件中
 * SpringApplication会自动读取spring.factories文件,并自动创建这些SpringApplicationRunListener实例
 * SpringApplicationRunListener的实现类必须提供一个只接收SpringApplication和String[]类型的参数的公开的构造方法
 */
public interface SpringApplicationRunListener {

    /**
     * SpringApplication的run()方法开始执行时的回调方法
     */
    default void starting(ConfigurableBootstrapContext bootstrapContext) {
    }

    /**
     * 环境准备完成后、Spring容器开始创建之前的回调方法
     */
    default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,ConfigurableEnvironment environment){
    }

    /**
     * Spring容器准备完成后、BeanDefinition加载之前的回调方法
     */
    default void contextPrepared(ConfigurableApplicationContext context) {
    }

    /**
     * Spring容器的BeanDefinition加载完成后、刷新之前的回调方法
     */
    default void contextLoaded(ConfigurableApplicationContext context) {
    }

    /**
     * Spring容器刷新完成且应用启动起来了后、CommandLineRunner和ApplicationRunner执行之前的回调方法
     */
    default void started(ConfigurableApplicationContext context) {
    }

    /**
     * CommandLineRunner和ApplicationRunner执行完成后的回调方法
     * 当回调完该方法后,SpringApplication的run()方法也就执行完成了
     */
    default void running(ConfigurableApplicationContext context) {
    }

    /**
     * 程序运行出错时的回调方法
     * 注意,本方法只会处理从环境准备到调用CommandLineRunner/ApplicationRunner期间出现的异常
     * 也就是说,如果监听器的running()方法抛出了异常,则本方法不会被执行
     * 这里的context参数可能为null,代表在Spring容器创建之前(即环境准备期间)就出现了异常
     */
    default void failed(ConfigurableApplicationContext context, Throwable exception) {
    }
}

6. CommandLineRunner/ApplicationRunner

/**
 * 本组件会在SpringApplication的run()方法接近尾声时调用
 * SpringApplication会自动从Spring容器中获取CommandLineRunner类型的Bean,并调用这些Bean的run()方法
 * 从这我们可以看出,要使CommandLineRunner组件生效,需要满足如下两个条件:
 * 1. 将该组件注册到Spring容器中(而SpringApplicationRunListener是注册到spring.factories文件中的)
 * 2. 该Spring容器是通过SpringApplication来创建的
 */
@FunctionalInterface
public interface CommandLineRunner {

    /**
     * 回调方法;这里的args是指主程序中的main()方法中的参数
     */
    void run(String... args) throws Exception;
}
/**
 * 本组件和CommandLineRunner基本相同
 */
@FunctionalInterface
public interface ApplicationRunner {

    /**
     * 回调方法;这里的args是对主程序中的main()方法中的参数的封装
     */
    void run(ApplicationArguments args) throws Exception;
}

7. SpringApplicationShutdownHook

7.1. 成员变量和构造方法

/**
 * 本组件会在JVM关闭时执行(需要通过Runtime类的静态addShutdownHook()方法手动注册)
 * 只有当本组件的run()方法执行完成时,JVM才会真正开始关闭,因此我们可以通过本组件来优雅地关闭SpringBoot应用
 * 本组件会维护SpringApplication创建的所有Spring容器,并且会等到所有的Spring容器都关闭完成后,才允许JVM关闭
 * 一般来说,本组件不应该被多次实例化,单个SpringApplicationShutdownHook实例就已经足够了
 */
class SpringApplicationShutdownHook implements Runnable {

    /**
     * 在关闭单个Spring容器时,轮询该Spring容器是否已经关闭完成的间隔时间
     */
    private static final int SLEEP = 50;

    /**
     * 关闭单个Spring容器时的超时时间;如果某个Spring容器的关闭时间超过了该限制,则不再等待该Spring容器关闭完成
     */
    private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(10);

    /**
     * 在所有Spring容器都关闭完成后,需要执行的Runnable实例;Handlers实例底层持有一个Runnable集合
     */
    private final Handlers handlers = new Handlers();

    /**
     * 所有未关闭的Spring容器
     */
    private final Set<ConfigurableApplicationContext> contexts = new LinkedHashSet<>();

    /**
     * 所有已关闭(但不一定已经关闭完成)的Spring容器;注意该集合的元素使用的是弱引用
     */
    private final Set<ConfigurableApplicationContext> closedContexts = Collections.newSetFromMap(new WeakHashMap<>());

    /**
     * 该监听器会监听Spring容器发出的ContextClosedEvent事件,然后将该Spring容器从this.contexts转移到this.closedContexts中
     */
    private final ApplicationContextClosedListener contextCloseListener = new ApplicationContextClosedListener();

    /**
     * 本组件是否正在执行,即JVM是否正准备关闭
     */
    private boolean inProgress;

    /** 
     * 构造器;会将当前组件注册为JVM的关闭钩子
     */
    SpringApplicationShutdownHook() {
        try {
            addRuntimeShutdownHook();
        } catch (AccessControlException ex) {
            // Not allowed in some environments
        }
    }

    /**
     * 将本组件注册为JVM关闭的钩子;JVM在关闭时,会等到本组件的run()方法执行完成后才真正开始关闭
     */
    protected void addRuntimeShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
    }
}

7.2. ApplicationContextClosedListener

class SpringApplicationShutdownHook implements Runnable {

    /**
     * 本监听器会监听Spring容器的ContextClosedEvent事件,然后将其从this.contexts转移到this.closedContexts中
     * 
     * 这里涉及到了ApplicationContext的close()方法的一些知识点:
     * 1. 在有多个线程同时调用close()方法时,只有第一个CAS成功的线程才会去真正关闭Spring容器,其它线程则不做任何事
     * 2. 换句话说,当close()方法执行完成时,不代表Spring容器真的已经关闭了
     * 3. 另一方面,在真正关闭Spring容器时,一开始就会发布一个ContextClosedEvent事件
     * 4. 也就是说,在接收到ContextClosedEvent事件时,Spring容器可能还未关闭完成
     *
     * 不妨假设本监听器会直接将Spring容器从this.contexts中移除,然后想象一下如下情形:
     * 1. 某个线程主动关闭了Spring容器(我们将该Spring容器称为"A"),然后发出一个ContextClosedEvent事件
     * 2. 执行本监听器,将A容器从this.contexts中移除,此时A容器还在执行关闭操作
     * 3. 这时候JVM要关闭了,因此触发外部类的run()方法,将this.contexts中的Spring容器关闭并等待其关闭完成,然后再关闭JVM
     * 4. 但是,由于A容器之前已被移除出this.contexts了,因此,如果A的关闭过程非常耗时的话,就会导致JVM在A容器关闭完成前就关闭了
     *
     * 这就解释了为什么要在Spring容器关闭时,将其转移到this.closedContexts中,且this.closedContexts的元素是弱引用:
     * 1. this.closedContexts中的元素都是还在关闭中的Spring容器,因此需要等待它们关闭完成
     * 2. 而当this.closedContexts中的Spring容器关闭完成时,它会因为弱引用而被GC,避免内存泄露
     */
    private class ApplicationContextClosedListener implements ApplicationListener<ContextClosedEvent> {

        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            synchronized (SpringApplicationShutdownHook.class) {
                ApplicationContext applicationContext = event.getApplicationContext();
                SpringApplicationShutdownHook.this.contexts.remove(applicationContext);
                SpringApplicationShutdownHook.this.closedContexts
                        .add((ConfigurableApplicationContext) applicationContext);
            }
        }
    }
}

7.3. 主要方法

class SpringApplicationShutdownHook implements Runnable {

    /**
     * 确保当前组件未在运行中(也就是JVM还没有准备关闭)
     */
    private void assertNotInProgress() {
        Assert.state(!SpringApplicationShutdownHook.this.inProgress, "Shutdown in progress");
    }

    /**
     * 注册Spring容器
     * 这里会先确保当前组件还未运行,然后将Spring容器添加到this.context中,并为其注册一个this.contextCloseListener监听器
     */
    void registerApplicationContext(ConfigurableApplicationContext context) {
        synchronized (SpringApplicationShutdownHook.class) {
            assertNotInProgress();
            context.addApplicationListener(this.contextCloseListener);
            this.contexts.add(context);
        }
    }

    /**
     * JVM关闭前会先调用本方法;本方法会在所有Spring容器都关闭完成后才返回
     */
    @Override
    public void run() {
        Set<ConfigurableApplicationContext> contexts;
        Set<ConfigurableApplicationContext> closedContexts;
        Set<Runnable> actions;
        
        // 加锁,然后将this.inProgress更新为true,并获取到各个集合的快照(避免并发修改异常)
        synchronized (SpringApplicationShutdownHook.class) {
            this.inProgress = true;
            contexts = new LinkedHashSet<>(this.contexts);
            closedContexts = new LinkedHashSet<>(this.closedContexts);
            actions = new LinkedHashSet<>(this.handlers.getActions());
        }
        
        // 等待所有Spring容器关闭完成,然后回调所有注册的Runnable实例
        contexts.forEach(this::closeAndWait);
        closedContexts.forEach(this::closeAndWait);
        actions.forEach(Runnable::run);
    }

    /**
     * 等待指定的Spring容器关闭完成;每SLEEP毫秒轮询一次,最多等待TIMEOUT毫秒
     */
    private void closeAndWait(ConfigurableApplicationContext context) {
        context.close();
        try {
            int waited = 0;
            while (context.isActive()) {
                if (waited > TIMEOUT) {
                    throw new TimeoutException();
                }
                Thread.sleep(SLEEP);
                waited += SLEEP;
            }
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            logger.warn("Interrupted waiting for application context " + context + " to become inactive");
        } catch (TimeoutException ex) {
            logger.warn("Timed out waiting for application context " + context + " to become inactive", ex);
        }
    }

    SpringApplicationShutdownHandlers getHandlers() {
        return this.handlers;
    }

    boolean isApplicationContextRegistered(ConfigurableApplicationContext context) {
        synchronized (SpringApplicationShutdownHook.class) {
            return this.contexts.contains(context);
        }
    }

    void reset() {
        synchronized (SpringApplicationShutdownHook.class) {
            this.contexts.clear();
            this.closedContexts.clear();
            this.handlers.getActions().clear();
            this.inProgress = false;
        }
    }
}