引子
对于最近有找工作需求的人来说,这几天是挺难的。最近听闻身边的一些公司裁员的消息,不知道,这悲伤的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() 这个方法,逻辑也比较简单,从它调用的方法的方法名来看,该方法主要执行三个动作:
- 读取 给定类型(type参数)的工厂实现的全限定类名(实际上是从 resources/META-INF/spring.factories 文件中读取的)
- 根据读取到的全限定类名,创建对象实例
- 根据@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) 所做的事情:
- 做了一些启动配置的初始化。
- 读取 spring.factories 文件中所配置的类型的全限定名,并放入缓存(一个Map);后续访问类型信息时就直接从缓存中获取。
- 根据上文拿到的类的全限定名,构建出 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本身的作用就是为应用环境的两个关键方面建模:Profile和Properties,从上面方法看,也的确是这么回事。先看第一个方法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));
}
未完待续,周末继续~
参考文档