Spring-聊聊application.yml和bootstrap.yml

用过Spring 的小伙伴都知道,application.yml或者application.properties 是Spring 的引导配置文件,但是有了解过其中区别吗?
本文将给从这个问题入手,深入源码中,研究application.ymlbootstrap.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));
	}

经过其他监听器后,会进入到 BootstrapApplicationListeneronApplicationEvent 方法。
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 有什么用?

BootstrapApplicationListeneronApplicationEvent 有以下流程:

  1. 幂等性,只会启动一次(相对于多容器)
  2. 如果需要,会启动一个父容器
  3. 将父容器部分信息merge到子容器中

这里,父容器是什么概念?
简单的来说,父容器,就是一个新的Spring 容器,会new出一个新的容器,并执行其run方法,即 SpringApplication 的run方法。
BootstrapApplicationListenerbootstrapServiceContext 方法:

	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;
	}
  1. 由于启动中时指定的source优先,故不会全量刷新所有配置,而只刷新部分配置。
    但是,如果是代码里面是通过spring.factories 来获取的数据,则需要在对应的bean 的方法 的幂等性。
  2. 传入的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 中配置类进行增删,例如自定义一个 AnlaEnvironmentPostProcessoranla.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();
	}
  1. RandomValuePropertySource 加入进去,这样可以在配置文件中,使用el表达式来增加随机数。
  2. 加载配置文件,即 spring.config.name 对应配置文件

Loaderload方法:

		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支持 ymlproperties 两种格式配置 的 PropertySourceLoader
另外,Spring同样会去根据 spring.config.locationspring.config.additional-location 去寻找配置,另外默认加载配置的地方为:
classpath:/,classpath:/config/,file:./,file:./config/

另外,读完bootstrap配置,会去读application配置吗?留个疑惑,下篇文章解决。

还有一个问题,微服务配置中心是如何刷新配置的呢?一起下篇解决!

关注博主公众号: 六点A君。
哈哈哈,一起研究Spring:
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在Spring Cloud Config中,有几种常见的配置文件,包括`bootstrap.yml`、`bootstrap-dev.yml`和`bootstrap-local.yml`。这些配置文件具有不同的优先级和用途。 1. `bootstrap.yml`是用于获取外部配置的特殊配置文件,它的加载优先级比其他配置文件(如`application.yml`或`application.properties`)更高[^1]。它主要用于加载一些启动阶段需要用到的配置信息,例如连接到Spring Cloud Config Server的配置、加密/解密信息、连接到外部数据库的配置等。由于这些配置信息在应用程序启动的早期阶段就需要用到,所以放在`bootstrap.yml`中确保它们在其他配置之前加载。此外,`bootstrap.yml`不会被本地配置文件覆盖,它的优先级更高,确保这些关键配置的安全性和准确性。 2. `bootstrap-dev.yml`是`bootstrap.yml`的一个特定的开发环境profile,用于在开发环境下加载特定的外部配置信息。在开发环境中,可能需要与生产环境不同的配置,例如连接到不同的数据库、使用不同的服务地址等。通过使用`bootstrap-dev.yml`可以轻松地区分开发环境和其他环境的配置。当在开发环境启动应用程序时,Spring Cloud Config将会加载`bootstrap.yml`和`bootstrap-dev.yml`的配置,并根据当前激活的profile(如通过`spring.profiles.active`属性指定)加载对应的配置。 3. `bootstrap-local.yml`是一个自定义的本地配置文件,用于加载本地环境下的特定配置信息。它的优先级比`bootstrap.yml`和`bootstrap-dev.yml`更高,可以用于覆盖这两个文件中的配置。通过使用`bootstrap-local.yml`,可以在本地环境中加载特定的配置,例如本地数据库的连接信息、本地服务的地址等。 综上所述,`bootstrap.yml`具有最低的优先级,用于加载一些启动阶段需要用到的配置信息。`bootstrap-dev.yml`是用于开发环境的特定配置文件,用于加载开发环境下的配置。`bootstrap-local.yml`是一个自定义的本地配置文件,用于加载本地环境下的特定配置信息。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值