Springcloud环境中bootstrap.yml加载原理

如果是Springcloud 项目,一般会将配置中心、注册中心等地址存入bootstrap.yml 中,同时将其他的配置存入配置中心。

也就是说bootstrap.yml 的读取会比较靠前。 下面研究其机制。

1. Springcloud 启动

​ Springcloud 环境启动过个中有一个重要的listener。BootstrapApplicationListener. 这个listener 在Spring-cloud-context-xxx.jar 中,且在spring.factories 文件自动配置。

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
  1. 查看org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent 调用时机

将断点下到org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent 方法,查看其调用时机如下:

核心过程如下:

(1).org.springframework.boot.SpringApplication#prepareEnvironment boot启动过程中准备环境

(2).org.springframework.boot.context.event.EventPublishingRunListener#environmentPrepared 广播环境准备事件

(3).org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType) 获取到所有的ApplicationListener, 开始广播

(4) 调用到org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent

  1. org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent 调用
    准备一系列的Sprinigcloud 环境,注册一些PropertySource, 处理SpringApplication、environment 对象等。

核心代码如下:org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext

	private ConfigurableApplicationContext bootstrapServiceContext(
			ConfigurableEnvironment environment, final SpringApplication application,
			String configName) {
    // 重置environment
		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);
		// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap
		// will fail
		// force the environment to use none, because if though it is set below in the
		// builder
		// the environment overrides it
		bootstrapMap.put("spring.main.web-application-type", "none");
		if (StringUtils.hasText(configLocation)) {
			bootstrapMap.put("spring.config.location", configLocation);
		}
    // 添加一个boostrap的MapPropertySource,后面解析bootstrap.yml 会用到
		bootstrapProperties.addFirst(
				new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
		for (PropertySource<?> source : environment.getPropertySources()) {
			if (source instanceof StubPropertySource) {
				continue;
			}
			bootstrapProperties.addLast(source);
		}
		
    // 重建一个builder
		SpringApplicationBuilder builder = new SpringApplicationBuilder()
				.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
				.environment(bootstrapEnvironment)
				// Don't use the default properties in this builder
				.registerShutdownHook(false).logStartupInfo(false)
				.web(WebApplicationType.NONE);
		final SpringApplication builderApplication = builder.application();
		if (builderApplication.getMainApplicationClass() == null) {
			// gh_425:
			// SpringApplication cannot deduce the MainApplicationClass here
			// if it is booted from SpringBootServletInitializer due to the
			// absense of the "main" method in stackTraces.
			// But luckily this method's second parameter "application" here
			// carries the real MainApplicationClass which has been explicitly
			// set by SpringBootServletInitializer itself already.
			builder.main(application.getMainApplicationClass());
		}
		if (environment.getPropertySources().contains("refreshArgs")) {
			// If we are doing a context refresh, really we only want to refresh the
			// Environment, and there are some toxic listeners (like the
			// LoggingApplicationListener) that affect global static state, so we need a
			// way to switch those off.
			builderApplication
					.setListeners(filterListeners(builderApplication.getListeners()));
		}
		builder.sources(BootstrapImportSelectorConfiguration.class);
    
    // builder.run() 相当于会重亲启动ApplicationContext (这一步会启动Springboot环境,加载配置也是在这一步骤进行加载的)
		final ConfigurableApplicationContext context = builder.run();
		// gh-214 using spring.application.name=bootstrap to set the context id via
		// `ContextIdApplicationContextInitializer` prevents apps from getting the actual
		// spring.application.name
		// during the bootstrap phase.
		context.setId("bootstrap");
		// Make the bootstrap context a parent of the app context
		addAncestorInitializer(application, context);
		// It only has properties in it now that we don't want in the parent so remove
		// it (and it will be added back later)
		bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
    // 合并propertySource
		mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
    // 返回Context,相当于替换Context
		return context;
	}

简单理解过程如下:springboot启动-》准备环境-》springcloudlistener操作(1. 设置springcloud环境-》2.builder.run启动springcloud环境(大体类似boot启动)-》3.清理环境中springcloud相关信息-》合并environment,重置context)-》boot其他listener-》boot其他启动逻辑

这里return 之前的environment 如下,如图:

2. bootstrap.yml加载过程

断点查看Springcloud 环境完全启动的environment的相关source :

过程解释:

​ 上面说了org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext 方法builder.run(); 会使用上面springcloud 初始化之后的一些环境进行启动,相当于原来springboot环境的启动。 启动完成之后替换Springboot的context和environment 对象。

  • builder.run(); 重新启动
  1. 调用到:org.springframework.boot.SpringApplication#run(java.lang.String...)

  2. 然后继续调用到:org.springframework.boot.SpringApplication#prepareEnvironment 方法

  3. org.springframework.boot.context.config.ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = this.loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        Iterator var3 = postProcessors.iterator();

        while(var3.hasNext()) {
            EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var3.next();
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }

    }

获取所有的环境后置处理器,进行后置处理。这里包括:

  1. 继续调用到org.springframework.boot.context.config.ConfigFileApplicationListener

这个里面加载重要的配置信息并且进行加载配置。

    private static final String DEFAULT_PROPERTIES = "defaultProperties";
    private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
    private static final String DEFAULT_NAMES = "application";
    private static final Set<String> NO_SEARCH_NAMES = Collections.singleton((Object)null);
    private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);
    private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);
    private static final Set<String> LOAD_FILTERED_PROPERTY;
    public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
    public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
    public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
    public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
    public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
    public static final int DEFAULT_ORDER = -2147483638;      

        private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            this.getSearchLocations().forEach((location) -> {
                boolean isFolder = location.endsWith("/");
                Set<String> names = isFolder ? this.getSearchNames() : ConfigFileApplicationListener.NO_SEARCH_NAMES;
                names.forEach((name) -> {
                    this.load(location, name, profile, filterFactory, consumer);
                });
            });
        }

        private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            if (!StringUtils.hasText(name)) {
                Iterator var13 = this.propertySourceLoaders.iterator();

                PropertySourceLoader loader;
                do {
                    if (!var13.hasNext()) {
                        throw new IllegalStateException("File extension of config file location '" + location + "' is not known to any PropertySourceLoader. If the location is meant to reference a directory, it must end in '/'");
                    }

                    loader = (PropertySourceLoader)var13.next();
                } while(!this.canLoadFileExtension(loader, location));

                this.load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
            } else {
                Set<String> processed = new HashSet();
                Iterator var7 = this.propertySourceLoaders.iterator();

                while(var7.hasNext()) {
                    PropertySourceLoader loaderx = (PropertySourceLoader)var7.next();
                    String[] var9 = loaderx.getFileExtensions();
                    int var10 = var9.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        String fileExtension = var9[var11];
                        if (processed.add(fileExtension)) {
                            this.loadForFileExtension(loaderx, location + name, "." + fileExtension, profile, filterFactory, consumer);
                        }
                    }
                }

            }
        }

        private Set<String> getSearchNames() {
            if (this.environment.containsProperty("spring.config.name")) {
                String property = this.environment.getProperty("spring.config.name");
                return this.asResolvedSet(property, (String)null);
            } else {
                return this.asResolvedSet(ConfigFileApplicationListener.this.names, "application");
            }
        }

org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#load传入的参数如下:name = bootstrap, location=classpath:/

​ 这里的核心逻辑是

(1). 获取搜索目录searchLocations,成员变量设置的默认值;

(2). 获取搜索的name; 如果包含spring.config.name ( 就获取cloud启动设置的bootstrap);如果不包含就获取application.xxx (这里也就是为什么bootstrap.yml 优先获取)。

​ 当springcloud 环境启动完成builder.run()执行完之后, 有一个remove bootstrap 的操作。 所以springcloud 环境启动完成之后相当于删除掉标记,然后合并context和environment 之后继续springboot 的启动。

(3). 然后遍历当前的loaders(也就是property和yml加载器),然后获取当前loader 支持的后缀(property),最后拼接传递参数让loader 去依次到指定位置获取。支持的后缀如下:(简单理解就是调用方给一个文件的basename和location,比如bootstrap,这里会分别调用两个loader 两次,然后传递不同的后缀去指定路径找文件加载。这也是为什么properties\yml\yaml都支持的原因。)

org.springframework.boot.env.PropertiesPropertySourceLoader#getFileExtensions
    public String[] getFileExtensions() {
        return new String[]{"properties", "xml"};
    }

org.springframework.boot.env.YamlPropertySourceLoader#getFileExtensions
    public String[] getFileExtensions() {
        return new String[]{"yml", "yaml"};
    }
  1. 继续调用到:org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#loadDocuments

  2. 继续调用到: org.springframework.boot.env.YamlPropertySourceLoader#load 解析classpath:./bootstrap.yml

  3. 创建一个OriginTrackedMapPropertySource然后加入到

        public OriginTrackedMapPropertySource(String name, Map source, boolean immutable) {
            super(name, source);
            this.immutable = immutable;
        }
    

    这里传入的参数如下:

这里就完成了加载bootstrap.yaml。

可能不通的Springboot 加载的类以及后置处理器不一样,但大体思路都是这样。

springoot 启动-》environment准备-》广播事件-》获取EnvironmentPostProcessor 环境后置处理器-》执行org.springframework.boot.env.EnvironmentPostProcessor#postProcessEnvironment 方法进行初始化配置加载(可能不同的版本采用不通的后置处理器处理)。

posted @ 2023-02-19 23:08  QiaoZhi  阅读(3246)  评论(0编辑  收藏  举报