Spring Boot技术内幕:架构设计与实现原理
上QQ阅读APP看书,第一时间看更新

3.5 ApplicationContextInitializer加载

3.5.1 源码解析

ApplicationContextInitializer是Spring IOC容器提供的一个接口,它是一个回调接口,主要目的是允许用户在ConfigurableApplicationContext类型(或其子类型)的ApplicationContext做refresh方法调用刷新之前,对ConfigurableApplicationContext实例做进一步的设置或处理。通常用于应用程序上下文进行编程初始化的Web应用程序中。

ApplicationContextInitializer接口只定义了一个initialize方法,代码如下。


public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    void initialize(C applicationContext);
}

ApplicationContextInitializer接口的initialize方法主要是为了初始化指定的应用上下文。而对应的上下文由参数传入,参数为ConfigurableApplicationContext的子类。

在完成了Web应用类型推断之后,ApplicationContextInitializer便开始进行加载工作,该过程可分两步骤:获得相关实例和设置实例。对应的方法分别为getSpringFactoriesInstances和setInitializers。

SpringApplication中获得实例相关方法代码如下。


private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // 加载对应配置,这里采用LinkedHashSet和名称来确保加载的唯一性
    Set<String> names = new LinkedHashSet<>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 创建实例
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    // 排序操作
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

getSpringFactoriesInstances方法依然是通过SpringFactoriesLoader类的loadFactoryNames方法来获得META-INF/spring.factories文件中注册的对应配置。在Spring Boot 2.2.1版本中,该文件内具体的配置代码如下。


# 应用程序上下文的初始化器配置
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

配置代码中等号后面的类为接口ApplicationContextInitializer的具体实现类。当获取到这些配置类的全限定名之后,便可调用createSpringFactoriesInstances方法进行相应的实例化操作。


private <T> List<T> createSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
        Set<String> names) {
    List<T> instances = new ArrayList<>(names.size());
    // 遍历加载到的类名(全限定名)
    for (String name : names) {
        try {
            // 获取Class
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            Assert.isAssignable(type, instanceClass);
            // 获取有参构造器
            Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
            // 创建对象
            T instance = (T) BeanUtils.instantiateClass(constructor, args);
            instances.add(instance);
        } catch (Throwable ex) {
            throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
        }
    }
    return instances;
}

完成获取配置类集合和实例化操作之后,调用setInitializers方法将实例化的集合添加到SpringApplication的成员变量initializers中,类型为List<ApplicationContextInitiali-zer<?>>,代码如下。


private List<ApplicationContextInitializer<?>> initializers;
public void setInitializers(
        Collection<? extends ApplicationContextInitializer<?>> initializers) {
    this.initializers = new ArrayList<>( initializers); 
}

setInitializers方法将接收到的initializers作为参数创建了一个新的List,并将其赋值给SpringApplication的initializers成员变量。由于是创建了新的List,并且直接赋值,因此该方法一旦被调用,便会导致数据覆盖,使用时需注意。

3.5.2 实例讲解

阅读完源代码,我们进行一些拓展,来自定义一个ApplicationContextInitializer接口的实现,并通过配置使其生效。

这里以实现ApplicationContextInitializer接口,并在initialize方法中打印容器中初始化了多少个Bean对象为例来进行演示,代码如下。


@Order(123) // @Order的value值越小越早执行
public class LearnApplicationContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 打印容器里面初始化了多少个Bean
        System.out.println("容器中初始化Bean数量:" + applicationContext.getBean-
DefinitionCount());
    }
}

上面就完成了一个最基础的ApplicationContextInitializer接口的实现类。当我们定义好具体的类和功能之后,可通过3种方法调用该类。

第一种方法就是参考Spring Boot源代码中的操作,将该实现类配置于META-INF/spring.factories文件中,这种方法与上面讲到的源代码配置方法一致。

第二种方法是通过application.properties或application.yml文件进行配置,格式如下。


context.initializer.classes=com.secbro2.learn.initializer.LearnApplicationCont-
extInitializer

这种方法是通过DelegatingApplicationContextInitializer类中的initialize方法获取到配置文件中对应的context.initializer.classes的值,并执行对应的initialize方法。

第三种方法是通过SpringApplication提供的addInitializers方法进行追加配置,代码如下。


public static void main(String[] args) {
    SpringApplication app = new SpringApplication(SpringLearnApplication.class, Person.class);
    // 添加自定义ContextInitializer,注意会覆盖掉默认配置的
    app.addInitializers(new LearnApplicationContextInitializer());
    app.run(args);
}

无论通过以上3种方法的哪一种,配置完成后,执行启动程序都可以看到控制台打印容器中Bean数量的日志。