从源码看SpringBoot启动过程

引子

对于最近有找工作需求的人来说,这几天是挺难的。最近听闻身边的一些公司裁员的消息,不知道,这悲伤的2020下,是否隐藏着契机。

周末突然想到了之前面试被问到的一道面试题:请简单描述下SpringBoot的启动过程。当时只是简单地说了下,有些点并没有覆盖到。正好,周末有空闲,看了看源码,结合着一些资料,这里就记录下我的理解。

软件版本

SpringBoot   2.1.4.RELEASE

流程分析

在启动方法 main() 方法中,调用了 SpringApplication.run() 方法,在一连串的调用链后,前面的那些简单的调用就不说了,最终,代码会执行到 org.springframework.boot.SpringApplication 类中的这个方法:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return (new SpringApplication(primarySources)).run(args);
}

对于这个方法,我们看其返回值,将这个方法的逻辑分为两部分:

  • new SpringApplication(primarySources)方法
  • run(args) 方法

下面,我们就分别看看这两部分都做了些什么。

new SpringApplication(primarySources)

我们查看这个方法的源码,发现其最终执行的是下面这个方法:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 这个方法,主要看下面这两行代码
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

PS:上面的内容,是下载的Spring源码里面的代码(就是查看源码时,idea会弹出一个黄色的提示条,可以选择Download Sources下载源码),而不是class文件反编译后的代码。我原本是看的反编译的代码,后来因为想看看作者的注释,就下载了源码,结果这个方法体本身的内容也变了——原本这个方法行数很多,但核心逻辑都是一样的。在下文中,我查看的依旧是 下载后的源码。

对于这个方法,从上面代码的备注处隔开,备注的上面的代码,主要做一些启动配置的初始化,比如,初始化resourcesLoadere、Web应用类型(Servlet或Reactive)等。而备注下方的两行代码:

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

这两行代码,我分别查看 set方法,这两行代码是初始化了 ApplicationContextInitializer 的实现和 ApplicationListener 的实现。这两个接口干什么的后面再说。

这里,我们注意到,两个方法都调用了 getSpringFactoriesInstances()方法。追其源码,最终该方法调用的是下面这个方法:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
	ClassLoader classLoader = getClassLoader();
	// Use names and ensure unique to protect against duplicates
	Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
	List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}

对于上面 getSpringFactoriesInstances() 这个方法,逻辑也比较简单,从它调用的方法的方法名来看,该方法主要执行三个动作:

  1. 读取 给定类型(type参数)的工厂实现的全限定类名(实际上是从 resources/META-INF/spring.factories 文件中读取的)
  2. 根据读取到的全限定类名,创建对象实例
  3. 根据@Order注解的值进行排序

记住这个方法,后面讲第二部分 springApplication.run(args) 方法的时候,还会用到这个方法。

下面我们分别仔细看看这个方法的流程。上面读取权限定名的时候,代码跳到了org.springframework.core.io.support.SpringFactoriesLoader 这个类中,调用了 SpringFactoriesLoader.loadFactoryNames() 这个方法,而该方法中又调用了另一个重要方法—— loadSpringFactories()。下面一起看看它们俩的源码:

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

// 这个是 org.springframework.core.io.support.SpringFactoriesLoader 这个类中定义的常量属性,在下面这个方法中会用到,为了方便源码阅读,我放到这里
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    // 首先从缓存中获取,如果获取到了,就直接返回;否则,从 spring.factories 文件中获取
    if (result != null) {
        return result;
    }
    try {
        // 下面这个常量,值为 “META-INF/spring.factories”
        Enumeration<URL> urls = (classLoader != null ? 
            classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : 
            ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryClassName = ((String) entry.getKey()).trim();
                for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        // 注意这里,将结果放到了 缓存中
        cache.put(classLoader, result);
        return result;
    } catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

在上面的代码中我们可以看到,在org.springframework.core.io.support.SpringFactoriesLoader 这个类中维护了一个Map作为缓存。这个缓存的访问权限为 private,在源码中看了下,我发现这个缓存就只用于上面这一块代码中,用于缓存指定类加载器能够从 spring.factories 文件中读取到的工厂实现的全限定类名。

首先我们看 loadSpringFactories() 这个方法,这个方法逻辑很简单:

首先去缓存中拿当前classLoader能读取到的数据;如果缓存中没有,就去读取 spring.factories 文件。将数据读取出来后,放入缓存中,然后将数据返回。这里的数据,指的就是在 spring.factories 文件中配置的相关实现类的全限定名

然后是 loadFactoryNames() 这个方法,该方法首先调用了 loadSpringFactories() 这个方法,拿到了当前ClassLoader能读取到的所有工厂实现,然后再根据参数factoryClass的名称去获取指定类型的全限定名,一个类型可能有多个实现。

在获取全限定名的逻辑执行完后,紧接着就是调用 createSpringFactoriesInstances() 方法完成对象的实例化。这里以及后面的排序,代码都很简单,就不贴了。

现在,我们来回顾一下,new SpringApplication(primarySources) 所做的事情:

  1. 做了一些启动配置的初始化。
  2. 读取 spring.factories 文件中所配置的类型的全限定名,并放入缓存(一个Map);后续访问类型信息时就直接从缓存中获取。
  3. 根据上文拿到的类的全限定名,构建出 ApplicationContextInitializer、ApplicationListener 的所有实现的实例,并赋值给当前 SpringApplication对象。

而对于  ApplicationContextInitializer、ApplicationListener 这两个接口,其作用分别是:

  • ApplicationContextInitializer:该接口定义了一个 initialize()方法。在 ApplicationContext 配置完毕之后、被刷新之前,initialize() 方法会被调用。
  • ApplicationListener:该接口定义了一个 onApplicationEvent() 方法。在 ApplicationContext准备的各个阶段,该方法都会被调用。调用的时机,可以参考 org.springframework.boot.SpringApplicationRunListener 这个接口,而具体的调用动作,则可以参考 SpringApplicationRunListener接口的实现类 org.springframework.boot.context.event.EventPublishingRunListener 这个类。

上面两个接口的使用,后文会提到。至此,Spring加载的第一部分 new SpringApplication(primarySources) 的逻辑就分析完了。

springApplication.run(args)

run()方法要比上面麻烦很多了。但是总来说,逻辑还是比较清晰的,我们一步一步来看。下面是run()方法的源码:

public ConfigurableApplicationContext run(String... args) {
    // 就是一个计时器,没什么玄乎的
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 这个就是后文会配置的ApplicationContext对象
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 这个好像是配置什么AWT属性,感觉不重要,不用管
    configureHeadlessProperty();
    // 下面这一行代码,相当重要。这里就是配置 ApplicationListener 的地方
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        // 将参数 args 封装为一个 ApplicationArguments 对象,有什么用,看后文分析
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 非常重要,准备环境信息
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[]{ConfigurableApplicationContext.class}, context);
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    } catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }
    try {
        listeners.running(context);
    } catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

这个方法不长,但是逻辑却比较复杂。我们将其分而治之,一步一步来看。

getRunListeners(args)

首先,前面的什么计时器什么的我们就不看到,直接看到我们第一处应该关注的代码,就是下面这行:

SpringApplicationRunListeners listeners = getRunListeners(args);

我们查看 getRunListeners() 方法的源码,如下:

private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    return new SpringApplicationRunListeners(logger, 
        // 重点关注下面这个方法
        getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

在继续分析之前,请大家睁大眼睛,注意上面代码中两个名称记为相似的引用:

  • org.springframework.boot.SpringApplicationRunListener:一个用于监听 ApplicationContext配置过程的监听器,里面提供了一些描述该过程的方法。
  • org.springframework.boot.SpringApplicationRunListeners:上面代码块中方法的返回类型,是一个包访问权限的类,里面存储了所有 SpringApplicationRunListener 的实现,其源码很简单,当它调用一个描述ApplicationContext加载过程的方法(例如:starting()、contextPrepared()等方法)时,相当于所有的 SpringApplicationRunListener 实例都调用了这个方法。

大家在阅读源码的时候都看仔细咯,别看错了。下面回到源码。在上面这个 getRunListeners() 方法中,重要的是其调用了getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args) 这个方法。这里,我将方法的参数也带上了,注意第三个参数 this,代表的是 前面的 new SpringApplication() 得到的那个 SpringApplication 对象。

前面我们已经分析过 getSpringFactoriesInstances() 这个方法了,我说该方法主要执行三个动作,第一个就是获取指定类型的工厂实现的全限定类名,这里就是 SpringApplicationRunListener 的所有实现的全限定类名。并且,前文中已经提到了这里的缓存机制,也就是说现在实际上可以直接从缓存中去获取了。

第二个动作就是根据获取到的全限定名,将这些类实例化。之前 createSpringFactoriesInstances() 这个方法我没讲,这里,我们只看它是如何实例化对象的:

private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) {
    // 忽略其他代码
    Class<?> instanceClass = ClassUtils.forName(name, classLoader);
    Assert.isAssignable(type, instanceClass);
    Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
    T instance = (T) BeanUtils.instantiateClass(constructor, args);
    // 忽略剩下的代码
}

从代码上看,这里是通过参数类型去获取有参构造方法,然后通过有参构造方法完成对象的实例化的。而这里,我们需要实例化的是 SpringApplicationRunListener 接口的实现类。通过查阅代码,在默认情况下,该接口只有一个实现类,就是前文我们提到过的 org.springframework.boot.context.event.EventPublishingRunListener 这个类。Mac系统下,我们下载源码后,可以通过 Command + Optionn + 点击 查看 SpringApplicationRunListener 接口的实现类。

也就是说,这里实际上实例化时,构建出的是 EventPublishingRunListener 这个类的实例。这个类提供了一个有参构造方法,实例化的时候也是调用的这个,我们看看其源码:

// 贴出这个属性,只是为了让大家明白,application 指代的是一个 org.springframework.boot.SpringApplication对象
private final SpringApplication application;
private final SimpleApplicationEventMulticaster initialMulticaster;

public EventPublishingRunListener(SpringApplication application, String[] args) {
    this.application = application;
    this.args = args;
    this.initialMulticaster = new SimpleApplicationEventMulticaster();
    // 注意下面遍历的对象,是 application.getListeners() 的返回值
    for (ApplicationListener<?> listener : application.getListeners()) {
        this.initialMulticaster.addApplicationListener(listener);
    }
}

这个方法里面,我们只需要关注for循环的逻辑,实际上就是将 application.getListeners() 的返回值全部都 add 到 initialMulticaster 这个属性中。而 application.getListeners() 的逻辑也很简单,看源码:

public Set<ApplicationListener<?>> getListeners() {
    return asUnmodifiableOrderedSet(this.listeners);
}

这里的 this,也是代表的是 前面的 new SpringApplication() 得到的那个 SpringApplication 对象。asUnmodifiableOrderedSet() 返回的是一个不可变的、已经排好序的 LinkedHashSet。前面分析 new SpringApplication() 的时候,提到了初始化了 ApplicationContextInitializer 和 ApplicationListener 这里两个接口的实现。而这里的 this.listeners,就是前面我们初始化了的 ApplicationListener 接口的实现。

也就是说,当我们构建 EventPublishingRunListener 这个类的对象时,会将前文得到的 ApplicationListener 接口的实现添加到该类的 initialMulticaster 属性中。

至此,getRunListeners(args) 这部分的逻辑咱们就看完了。咱们来梳理一下:

该方法用于获取到所有 SpringApplicationRunListener 的实现,并将这些实现封装为到一个 SpringApplicationRunListeners 中。在默认情况下(可以开发人员实现这个接口),SpringApplicationRunListener 的实现类是 EventPublishingRunListener 类,在构造 EventPublishingRunListener 对象的时候,会将前面从 spring.factories 中定义的 ApplicationListener 接口的实现也添加到 EventPublishingRunListener 对象的 initialMulticaster 属性中。

后续当 SpringApplicationRunListeners 的对象调用类似 starting() 方法时,Spring会根据 事件类型(EventType) 选出所有符合条件的 ApplicationListener 的实现,并异步调用他们的 starting()方法。这里就简单的提一下,要是看具体的逻辑,又得好多时间。

prepareEnvironment(listeners, applicationArguments);

上面获取到所有的 listener 后,需要进行的第一个动作就是准备环境,也就是这里的 prepareEnvironment() 方法的逻辑。下面看看这个方法的源码:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
    // 默认情况下,下面这个方法会 new 一个 StandardServletEnvironment 返回;
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 配置环境,实际核心逻辑是配置了 Properties 和 Profile
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 环境准备完毕后,调用 ApplicationListener 实例的 environmentPrepared() 方法
    listeners.environmentPrepared(environment);
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

下面,我就来分别看看这几个方法。对于 getOrCreateEnvironment() 这个方法,其逻辑很简单,就不展开看了。

configureEnvironment(environment, args)

上面的 args 实际上是 applicationArguments.getSourceArgs() 的值,这里为了方便大家理解,我就直接贴成 args 了。下面,我们看看这个方法的源码:

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
    // 省略 Converter 处理逻辑
    configurePropertySources(environment, args);
    configureProfiles(environment, args);
}

Environment本身的作用就是为应用环境的两个关键方面建模:ProfileProperties,从上面方法看,也的确是这么回事。先看第一个方法configurePropertySources() 的源码:

/* 下面这个变量在 org.springframework.core.env.AbstractEnvironment 类中定义
    environment.getPropertySources() 返回的值实际上就是这个变量。*/
private final MutablePropertySources propertySources = new MutablePropertySources();

/* 下面所有变量、方法都定义在 org.springframework.boot.SpringApplication 类中 */
// 截止到下面代码获取到该变量,我还没有发现有其他代码修改过该变量的值
private Map<String, Object> defaultProperties;
private boolean addCommandLineProperties = true;

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
    // 下面的 sources 指代的是,当前 environment 中已经存在的 PropertySources
    MutablePropertySources sources = environment.getPropertySources();
    if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
        sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
    }
    if (this.addCommandLineProperties && args.length > 0) {
        // 下面这个常量 CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs";
        String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
        if (sources.contains(name)) {
            PropertySource<?> source = sources.get(name);
            CompositePropertySource composite = new CompositePropertySource(name);
            // 将args封装为一个SimpleCommandLinePropertySource对象时,就会自动将args中每个元素解析为对应的键值对
            composite.addPropertySource(new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
            composite.addPropertySource(source);
            sources.replace(name, composite);
        }else {
            sources.addFirst(new SimpleCommandLinePropertySource(args));
        }
    }
}

我们跟着代码的流程走,这个方法总体逻辑比较简单。在默认情况下,执行到上面这个方法时,还没有任何地方修改了 defaultProperties 这个值,所以该变量默认为 null,不会执行到第一个 if 判断中的代码,我这里就不贴出了,实际代码也比较简单,有兴趣的朋友可以去看看。

对于第二个 if 判断,默认情况下,addCommandLineProperties这个变量的值为true。如果我们再启动项目的时候,不加上任何启动参数,这样args也就是一个空数组,该 if 判断中的逻辑也不会执行。不过,当我们加上启动参数时,就不一样了,比如:

java -jar XX.jar --spring.profiles.active=prod

当我们使用上面的命令启动项目时,args数组中就会有一个为"--spring.profiles.active=prod"的值,这样,代码就会走入 if 内的代码块中。此外,args数组中,存放的是一个个类似"--spring.profiles.active=prod" 的字符串,在后面执行new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args)这行代码的时候,才会将其解析为一个一个的键值对,并最终封装为一个PropertySource

紧接着的代码逻辑是,如果当前Enviroment中已维护的 PropertySources 里面已经存在名为 "commandLineArgs" 的资源,那么就把它和args中的PropertySource合并成一个 CompositePropertySource,并使用合并后的CompositePropertySource替换掉原来的PropertySource;如果不存在名为 "commandLineArgs" 的资源,那就将其添加到当前Environment中,并且给很高的优先权(参考addFirst()方法的注释)。

至此,configurePropertySources()方法的逻辑我们就看完了。实际上,当这个方法执行完的时候,Spring没有xxxx.properties文件中的配置数据读取到Environment中。但是,在我们启动项目时添加的启动参数中的配置,会被解析成PropertySource并且被添加到Environment(这里所说的添加到Environment中,实际上是添加到了org.springframework.core.env.AbstractEnvironment类中的propertySources 这个对象中)。下面,我们再看看configureProfiles()方法的源码,如下:

private Set<String> additionalProfiles = new HashSet<>();

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
    environment.getActiveProfiles(); // ensure they are initialized
    // But these ones should go first (last wins in a property key clash)
    Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
    profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
    environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

这部分代码,目前来看也比较简单。总来来说,就涉及到environment对象的两个方法:getActiveProfiles()setActiveProfiles()两个方法,而getActiveProfiles()实际上调用的是doGetActiveProfiles()方法。这两个方法都定义在org.springframework.core.env.AbstractEnvironment类中,下面我们看看这俩方法的逻辑:

/* 当前Environment中维护的一些配置信息(Profile)
   当代码第一次执行到 doGetActiveProfiles(),也就是代码第一次执行到
   environment.getActiveProfiles() 时,还没有其他程序向activeProfiles中添加元素    */
private final Set<String> activeProfiles = new LinkedHashSet<>();
public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";

// environment.getActiveProfiles() 实际上底层逻辑是下面的 doGetActiveProfiles() 方法
protected Set<String> doGetActiveProfiles() {
    synchronized (this.activeProfiles) {
        if (this.activeProfiles.isEmpty()) {
            String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
            if (StringUtils.hasText(profiles)) {
                //下一行调用的这个 setActiveProfiles()方法,就是下面贴出的那个方法
                setActiveProfiles(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(profiles)));
            }
        }
        return this.activeProfiles;
    }
}

public void setActiveProfiles(String... profiles) {
    Assert.notNull(profiles, "Profile array must not be null");
    if (logger.isDebugEnabled()) {
        logger.debug("Activating profiles " + Arrays.asList(profiles));
    }
    synchronized (this.activeProfiles) {
        this.activeProfiles.clear();
        for (String profile : profiles) {
            validateProfile(profile);
            this.activeProfiles.add(profile);
        }
    }
}

既然doGetActiveProfiles()方法中也调用了 setActiveProfiles()方法,我们就先看看setActiveProfiles()方法的逻辑:

逻辑很简单,接收一个profile数组(可变参数),然后然后将当前Environment中维护的Profile清理掉,再将传进来的所有Profile放进当前Environment中。

接下来,我们再看看doGetActiveProfiles()方法的逻辑:

通过查看activeProfiles的引用链,我发现当代码第一次执行到doGetActiveProfiles()方法,也就是当第一次执行environment.getActiveProfiles()方法时,还没有代码向activeProfiles中添加过元素。也就是说,第一次执行doGetActiveProfiles()方法时,activeProfiles的是个空集合,第一个if代码块被执行。

首先会去获取“spring.profiles.active”这个Property的值,这里这个getProperty()方法,我们得看看其源码:

/* 上文已经说过,如果在项目启动的时候,指定了启动参数,
   那么启动参数会被解析为PropertySource并添加到下面这个propertySources变量中    */
private final MutablePropertySources propertySources = new MutablePropertySources();
// 注意下面这个对象,在构造时,传入了上面这个 propertySources
private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);

public String getProperty(String key) {
    return this.propertyResolver.getProperty(key);
}

从上面我们可以看出,第一次执行到这里时,getProperty()实际上是在 从启动参数中解析出的PropertySource 找对应的key

现在回到上面doGetActiveProfiles()方法中的getProperty(spring.profiles.active)逻辑。

如果在启动项目时指定了启动参数"--spring.profiles.active=prod",那么这里就能获取到profiles=prod,然后就执行setActiveProfiles()方法,此时,activeProfiles 变量中,只存在一个值,就是 "prod"

如果在启动项目时没有指定该参数,那么这里获取到的profiles就为null,下面的if块中的代码就不会执行。此时,activeProfiles 仍然是一个空集合(集合中没有元素,但并不为null)。如前文所说,现在Spring还没有加载这些配置文件,所以,即使我们在 boostrap.properties、application.properties 中定义了spring.profiles.active=prod,只要启动参数中没有指定,这里getProperty(spring.profiles.active)方法仍然会返回null

到这里,doGetActiveProfiles()setActiveProfiles()两个方法逻辑就看完了,再回到configureProfiles()方法:

private Set<String> additionalProfiles = new HashSet<>();

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
    environment.getActiveProfiles(); // ensure they are initialized
    // But these ones should go first (last wins in a property key clash)
    Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
    profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
    environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

 

 

未完待续,周末继续~

 

参考文档

1、https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/ApplicationContextInitializer.html

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值