spring boot和spring cloud的区别_Spring聊聊application和bootstrap

用过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:7d5a901fe4a3faf88d878b6128909dca.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值