用过Spring 的小伙伴都知道,application.yml
或者application.properties
是Spring 的引导配置文件,但是有了解过其中区别吗?
本文将给从这个问题入手,深入源码中,研究application.yml
和bootstrap.yml
到底有什么区别。
配置
首先,我们在程序中,可以通过 spring.profiles.active
来制定生效的配置类,这样可以来区分配置。
其次,可以通过更改 spring.config.name
来更改引导的配置文件名字,例如可以将 anla.properties
也改为可加载的配置。
Spring 启动过程
Spring启动过程中,会首先加载配置,然后才去初始化Ioc容器,以Spring Boot为例,主要逻辑可以如下:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
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;
}
和配置相关的,主要在以下代码中:
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
...
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
Spring配置加载
很多资料在说这两个区别时,都会先说bootstrap.yml优先于application.yml加载,但是都没有给出代码上的证据,接下来一起盘他。
BootstrapApplicationListener引导
从 prepareEnvironment
方法进入,里面会有对外发布一个 ApplicationEnvironmentPreparedEvent
事件:
prepareEnvironment
方法:
listeners.environmentPrepared(environment);
SpringApplicationRunListeners.java
:
public void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.environmentPrepared(environment);
}
}
EventPublishingRunListener.java
:
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster
.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}
经过其他监听器后,会进入到 BootstrapApplicationListener
的 onApplicationEvent
方法。
BootstrapApplicationListener
定义:
public class BootstrapApplicationListener
implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
}
为什么 BootstrapApplicationListener
会被加载呢?
其实当我们引入 spring-cloud-context
依赖时,它下面会有一个spring的spi配置文件 spring.factories
,里面定义了一个ApplicationListener
:
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
org.springframework.cloud.context.restart.RestartListener
BootstrapApplicationListener 有什么用?
BootstrapApplicationListener
的 onApplicationEvent
有以下流程:
- 幂等性,只会启动一次(相对于多容器)
- 如果需要,会启动一个父容器
- 将父容器部分信息merge到子容器中
这里,父容器是什么概念?
简单的来说,父容器,就是一个新的Spring 容器,会new出一个新的容器,并执行其run方法,即 SpringApplication
的run方法。
BootstrapApplicationListener
的 bootstrapServiceContext
方法:
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
MutablePropertySources bootstrapProperties = bootstrapEnvironment
.getPropertySources();
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}
String configLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
Map<String, Object> bootstrapMap = new HashMap<>();
bootstrapMap.put("spring.config.name", configName);
bootstrapMap.put("spring.main.web-application-type", "none");
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configLocation);
}
bootstrapProperties.addFirst(
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
.environment(bootstrapEnvironment)
.registerShutdownHook(false).logStartupInfo(false)
.web(WebApplicationType.NONE);
final SpringApplication builderApplication = builder.application();
if (builderApplication.getMainApplicationClass() == null) {
builder.main(application.getMainApplicationClass());
}
if (environment.getPropertySources().contains("refreshArgs")) {
builderApplication
.setListeners(filterListeners(builderApplication.getListeners()));
}
builder.sources(BootstrapImportSelectorConfiguration.class);
final ConfigurableApplicationContext context = builder.run();
context.setId("bootstrap");
addAncestorInitializer(application, context);
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}
- 由于启动中时指定的source优先,故不会全量刷新所有配置,而只刷新部分配置。
但是,如果是代码里面是通过spring.factories
来获取的数据,则需要在对应的bean 的方法 的幂等性。 - 传入的configName将
spring.config.name
重写了,所以会去加载bootstrap
配置,而当父容器加载完之后,轮到子容器时,仍然会使用application
配置,这就说明了,为什么bootstrap优先于application配置
ConfigFileApplicationListener 有什么用
在idea中,点击 spring.profiles.name
会跳转到 ConfigFileApplicationListener
中。在 ConfigFileApplicationListener
中,会加载所有的EnvironmentPostProcessor
并执行他们的postProcessorEnvironment
方法:
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
正如Spring容器中,有各种处理器PostProcessor
来处理BeanFactory
生命周期事件,或者来处理Bean
生命周期事件,从而可以实现很多特色功能一样。对于Evironment
来说,EnvironmentPostProcessor
就是干这个的。
Spring中定义了一个顶层接口 PropertySource
,作为配置类抽象,利用 EnvironmentPostProcessor
可以对Spring 中配置类进行增删,例如自定义一个 AnlaEnvironmentPostProcessor
将 anla.properties
读入:
@Slf4j
public class AnlaEnvironmentPostProcessor implements EnvironmentPostProcessor {
private PropertiesPropertySourceLoader loader = new PropertiesPropertySourceLoader();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
MutablePropertySources propertySources = environment.getPropertySources();
Resource resource = new ClassPathResource("anla.properties");
try {
PropertySource ps = loader.load("YetAnotherPropertiesFile", resource)
.get(0);
propertySources.addFirst(ps);
} catch (Exception e) {
log.error("Exception!", e);
}
}
}
配置读取
ConfigFileApplicationListener
本身也是一个 EnvironmentPostProcessor
配置类,看他的 postProcessEnvironment
方法:
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
addPropertySources
方法:
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}
- 将
RandomValuePropertySource
加入进去,这样可以在配置文件中,使用el表达式来增加随机数。 - 加载配置文件,即
spring.config.name
对应配置文件
Loader
的load
方法:
public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
resetEnvironmentProfiles(this.processedProfiles);
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
}
根据是否有profile,config的名字,去路径下搜索配置文件:
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
}
Set<String> processed = new HashSet<>();
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
目前Spring支持 yml
和 properties
两种格式配置 的 PropertySourceLoader
。
另外,Spring同样会去根据 spring.config.location
和 spring.config.additional-location
去寻找配置,另外默认加载配置的地方为:
classpath:/,classpath:/config/,file:./,file:./config/
。
另外,读完bootstrap
配置,会去读application
配置吗?留个疑惑,下篇文章解决。
还有一个问题,微服务配置中心是如何刷新配置的呢?一起下篇解决!
关注博主公众号: 六点A君。
哈哈哈,一起研究Spring: