Spring Cloud源码阅读(一)

2 篇文章 0 订阅
2 篇文章 0 订阅

问题

  1. Spring Cloud如何创建两个上下文环境的
  2. Spring Cloud如何加载bootstrap.yml配置文件的
  3. Spring Cloud Config是如何获取远程配置的
  4. Spring Cloud多个环境配置项重复优先级问题

源码解析

Spring Cloud如何创建两个上下文环境的

org.springframework.cloud.bootstrap.BootstrapApplicationListener该类执行了创建Spring Cloud父级上下文的创建。

/**
 * A listener that prepares a SpringApplication (e.g. populating its Environment) by
 * delegating to {@link ApplicationContextInitializer} beans in a separate bootstrap
 * context. The bootstrap context is a SpringApplication created from sources defined in
 * spring.factories as {@link BootstrapConfiguration}, and initialized with external
 * config taken from "bootstrap.properties" (or yml), instead of the normal
 * "application.properties".
 *
 * @author Dave Syer
 *
 */
public class BootstrapApplicationListener
		implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
		// 此处省略代码逻辑
}

BootstrapApplicationListener实现了ApplicationListener,对ApplicationEnvironmentPreparedEvent事件进行了监听,从名字中可以判断Spring会在Environment准备完成时广播该事件。

从org.springframework.boot.SpringApplication的源码中也能看到spring对该事件的通知行为。

package org.springframework.boot;

public class SpringApplication {
	// 此处省略其他代码

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
          ApplicationArguments applicationArguments) {
        // Create and configure the environment
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);
        // 此处通知环境加载完毕
        listeners.environmentPrepared(environment);
        bindToSpringApplication(environment);
        if (!this.isCustomEnvironment) {
          environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
              deduceEnvironmentClass());
        }
        ConfigurationPropertySources.attach(environment);
        return environment;
    }
}

这里展示下SpringApplication的执行流程图,方便后面理解。

BootstrapApplicationListener接收到ApplicationEnvironmentPreparedEvent事件后执行了如下代码逻辑。

注意,此时还是子级上下文的处理逻辑。

@Override
	public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
		ConfigurableEnvironment environment = event.getEnvironment();
        // 判断是否开启Spring Cloud,默认开启
		if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
				true)) {
			return;
		}
		// 如果是BootstrapContext则忽略,BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)标识当前上下文为BootstrapContext
		if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
			return;
		}
		ConfigurableApplicationContext context = null;
        // 获取配置名,默认bootstrap
		String configName = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
        // 如果已经加载过BootstrapContext了,这里从ParentContextApplicationContextInitializer反射获取BootstrapContext
		for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
				.getInitializers()) {
			if (initializer instanceof ParentContextApplicationContextInitializer) {
				context = findBootstrapContext(
						(ParentContextApplicationContextInitializer) initializer,
						configName);
			}
		}
		if (context == null) {
            // 如果BootstrapContext未曾初始化,这里进行初始化
			context = bootstrapServiceContext(environment, event.getSpringApplication(),
					configName);
			event.getSpringApplication()
					.addListeners(new CloseContextOnFailureApplicationListener(context));
		}

        // 将BootstrapContext的ApplicationContextInitializer配置给子级上下文的SpringApplication
		apply(context, event.getSpringApplication(), environment);
	}

这里为了区分父级上下文和子级上下文,统一把父级上下文称为BootstrapContext,子级上下文称为ApplicationContext,当然真实项目中可能会有多级上下文,这里只讨论二级的情况。

  1. 判断是否开启Spring Cloud,默认开启。
  2. 因为当前是对ApplicationEnvironmentPreparedEvent事件的监听,BootstrapContext的创建一样会触发该监听器,识别到Environment中包含BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置则识别为BootstrapContext的ApplicationEnvironmentPreparedEvent事件,直接忽略。
  1. 获取配置名,默认bootstrap。
  2. 如果已经加载过BootstrapContext了,这里从ParentContextApplicationContextInitializer反射获取BootstrapContext。
  1. 如果BootstrapContext未曾创建,即ParentContextApplicationContextInitializer反射没有获取到BootstrapContext,则通过bootstrapServiceContext方法创建BootstrapContext。
  2. 将BootstrapContext的ApplicationContextInitializer配置给子级上下文的SpringApplication。
private void apply(ConfigurableApplicationContext context,
			SpringApplication application, ConfigurableEnvironment environment) {
		@SuppressWarnings("rawtypes")
		List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context,
				ApplicationContextInitializer.class);
		application.addInitializers(initializers
				.toArray(new ApplicationContextInitializer[initializers.size()]));
		addBootstrapDecryptInitializer(application);
}

context = bootstrapServiceContext(environment, event.getSpringApplication(), configName); 这段代码完成了对BootstrapContext的创建,让我们来看下该代码的具体逻辑。

	private ConfigurableApplicationContext bootstrapServiceContext(
			ConfigurableEnvironment environment, final SpringApplication application,
			String configName) {
        // 创建BootstrapContext的Environment
		StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
		MutablePropertySources bootstrapProperties = bootstrapEnvironment
				.getPropertySources();
		for (PropertySource<?> source : bootstrapProperties) {
			bootstrapProperties.remove(source.getName());
		}
        // 设置BootstrapContext初始化配置信息
		String configLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
		String configAdditionalLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.additional-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);
		}
		if (StringUtils.hasText(configAdditionalLocation)) {
			bootstrapMap.put("spring.config.additional-location",
					configAdditionalLocation);
		}
		bootstrapProperties.addFirst(
				new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
		for (PropertySource<?> source : environment.getPropertySources()) {
			if (source instanceof StubPropertySource) {
				continue;
			}
			bootstrapProperties.addLast(source);
		}
		// TODO: is it possible or sensible to share a ResourceLoader?
        // 创建BootstrapContext对应的SpringApplication
		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()));
		}
        // 设置Spring Cloud的配置类,使得Spring Cloud新特性可以生效
		builder.sources(BootstrapImportSelectorConfiguration.class);
        // 运行BootstrapContext对应的SpringApplication
		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.
        // 设置BootstrapContext的ID
		context.setId("bootstrap");
		// Make the bootstrap context a parent of the app context
        // 设置AncestorInitializer
		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)
        // 移除BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置
		bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置);
		mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
		return context;
	}
  1. 创建BootstrapContext的Environment。
  2. 设置BootstrapContext初始化配置信息,包括以下配置:

配置项

配置名

默认值

spring.main.web-application-type

应用类型

none

spring.config.location

配置文件地址,可通过spring.cloud.bootstrap.location配置

spring.config.additional-location

额外配置文件地址,可通过spring.cloud.bootstrap.additional-location配置

spring.config.name

环境配置名称,可通过spring.cloud.bootstrap.name配置

bootstrap

  1. 其中比较重要的是spring.config.name配置项,SpringBoot通过该配置项来获取bootstrap.yml或者bootstrap.properties配置文件(默认bootstrap情况下)。具体如何获取的稍后介绍。
  2. 创建BootstrapContext对应的SpringApplication。
  3. 设置Spring Cloud的配置类,使得Spring Cloud新特性可以生效。具体做了哪些配置后面介绍。
  4. 运行BootstrapContext对应的SpringApplication,获取到ConfigurableApplicationContext(即BootstrapContext)。
  5. 设置BootstrapContext的ID,用于区分ApplicatonContext。
  6. 给子级ApplicationContext设置AncestorInitializer。这个AncestorInitializer实现了ApplicationContextInitializer,ApplicationContextInitializer会在SpringApplication创建ApplicationContext后执行,可参考上面SpringApplication的执行流程图。
    会将当前BootstrapContext作为构造方法的参数传入AncestorInitializer,等子级对应的SpringApplication创建ApplicationContext后会执行该AncestorInitializer。
  7. 移除BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置(BootstrapContext对应的Environment)。
	private void addAncestorInitializer(SpringApplication application,
			ConfigurableApplicationContext context) {
		boolean installed = false;
		for (ApplicationContextInitializer<?> initializer : application
				.getInitializers()) {
			if (initializer instanceof AncestorInitializer) {
				installed = true;
				// New parent
                // 如果子级已经有该AncestorInitializer了,直接将BootstrapContext设置给该AncestorInitializer
				((AncestorInitializer) initializer).setParent(context);
			}
		}
		if (!installed) {
            // 设置AncestorInitializer到子级SpringApplication
			application.addInitializers(new AncestorInitializer(context));
		}

	}

	private static class AncestorInitializer implements
			ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

		private ConfigurableApplicationContext parent;

		AncestorInitializer(ConfigurableApplicationContext parent) {
			this.parent = parent;
		}

		@Override
		public void initialize(ConfigurableApplicationContext context) {
            // 递归查询顶级ApplicationContext
			while (context.getParent() != null && context.getParent() != context) {
				context = (ConfigurableApplicationContext) context.getParent();
			}
			reorderSources(context.getEnvironment());
            // 通过ParentContextApplicationContextInitializer类将Bootstrap设置为顶级上下文
			new ParentContextApplicationContextInitializer(this.parent)
					.initialize(context);
		}
}

public class ParentContextApplicationContextInitializer
		implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            if (applicationContext != this.parent) {
                // 将Bootstrap设置为顶级上下文
                applicationContext.setParent(this.parent);
                // 设置一个监听器,通知ParentContextAvailableEvent事件
                applicationContext.addApplicationListener(EventPublisher.INSTANCE);
            }
        }
}

Spring Cloud如何加载bootstrap.yml配置文件的

org.springframework.boot.context.config.ConfigFileApplicationListener会加载本地配置到当前上下文环境中,默认情况下会加载application.yml/application.properties

package org.springframework.boot.context.config;

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
    
    int Order.HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
   	
    public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;

	private int order = DEFAULT_ORDER;
    
    @Override
	public void onApplicationEvent(ApplicationEvent event) {
        // 监听ApplicationEnvironmentPreparedEvent事件
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
        
        // 监听ApplicationPreparedEvent事件
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}
    
    // 对ApplicationEnvironmentPreparedEvent事件的逻辑处理
    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        // 装载EnvironmentPostProcessor
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        // 因为自身也实现了EnvironmentPostProcessor,所以把自身也装载进去
		postProcessors.add(this);
        // 排序
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
            // 循环处理EnvironmentPostProcessor的逻辑
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}
    
    // 通过spring.factoies加载EnvironmentPostProcessor
    List<EnvironmentPostProcessor> loadPostProcessors() {
		return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
	}
    
    // 获取执行顺序排序值,值越小越先执行,
    @Override
	public int getOrder() {
		return this.order;
	}
}

可以看到ConfigFileApplicationListener也继承了ApplicationListener(通过SmartApplicationListener间接继承)接口,并且监听了ApplicationEnvironmentPreparedEvent事件。

因为执行顺序排寻值为Ordered.HIGHEST_PRECEDENCE + 10,而上面介绍的org.springframework.cloud.bootstrap.BootstrapApplicationListener(创建BootstrapContext的监听器,同样监听了ApplicationEnvironmentPreparedEvent事件),执行顺序排序值为Ordered.HIGHEST_PRECEDENCE + 5, 比Ordered.HIGHEST_PRECEDENCE + 10要小,所以BootstrapApplicationListener要比ConfigFileApplicationListener先执行。

通过上面的代码以及注释我们可以看到,ConfigFileApplicationListener会作为一个EnvironmentPostProcessor的实现类,当监听到ApplicationEnvironmentPreparedEvent时,会通过postProcessEnvironment对Environment进行配置。

@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		addPropertySources(environment, application.getResourceLoader());
	}

	protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
		RandomValuePropertySource.addToEnvironment(environment);
		new Loader(environment, resourceLoader).load();
	}

private class Loader {

		private final ConfigurableEnvironment environment;

		private final ResourceLoader resourceLoader;

		private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();

		Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
			this.environment = environment;
			this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
			this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
			this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
					getClass().getClassLoader());
		}

		void load() {
			FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
					(defaultProperties) -> {
						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 (isDefaultProfile(profile)) {
								addProfileToEnvironment(profile.getName());
							}
							load(profile, this::getPositiveProfileFilter,
									addToLoaded(MutablePropertySources::addLast, false));
							this.processedProfiles.add(profile);
						}
						load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
						addLoadedPropertySources();
						applyActiveProfiles(defaultProperties);
					});
		}
}

这里代码逻辑就不深入了,大概逻辑就是会通过查找本地的配置信息并装载到Environment, 默认的就是application.properties/application.yml,当然也会根据激活的profile信息查找对应的配置文件,如profile=dev情况下会查找application-dev.properties/application-dev.yml,有兴趣的同学可以自行研究。

这里我们需要关心的是获取配置文件名的代码逻辑。

public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

private static final String DEFAULT_NAMES = "application";

private Set<String> getSearchNames() {
    
	if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
        // 如果存在spring.config.name配置的值,则获取该值作为配置文件名
		String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
		Set<String> names = asResolvedSet(property, null);
		names.forEach(this::assertValidConfigName);
		return names;
	}
	return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

该代码的逻辑大概就是如果存在spring.config.name配置的值,则获取该值作为配置文件名。
而通过之前的梳理我们已经知道了,BootstrapApplicationListener在创建BootstrapContext后会将spring.config.name设置为bootstrap(默认),所以这里ConfigFileApplicationListener会通过这个文件名bootstrap.yml/bootstrap.properties来查找配置,并加载到Environment中。当然这里配置比较复杂,还有默认路径配置、文件类型加载等,这里就不展开了。

Spring Cloud Config是如何获取远程配置的

org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration为Spring Cloud加载远程配置。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
		ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
    // 此处省略代码逻辑
}

可以看到这里PropertySourceBootstrapConfiguration也实现了ApplicationContextInitializer,即在ApplicationContext生成后机会执行initialize方法。但这个类是如何被加载到Spring容器中的呢?

// BootstrapApplicationListener#bootstrapServiceContext里为BootstrapContexat设置了一个配置源
builder.sources(BootstrapImportSelectorConfiguration.class);
BootstrapApplicationListener#bootstrapServiceContext里为BootstrapContext设置了一个配置源。

@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration {

}

该配置导入了另外一个配置org.springframework.cloud.bootstrap.BootstrapImportSelector。

public class BootstrapImportSelector implements EnvironmentAware, DeferredImportSelector {

	private Environment environment;

	private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();

	@Override
	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		// Use names and ensure unique to protect against duplicates
		List<String> names = new ArrayList<>(SpringFactoriesLoader
				.loadFactoryNames(BootstrapConfiguration.class, classLoader));
		names.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(
				this.environment.getProperty("spring.cloud.bootstrap.sources", ""))));

		List<OrderedAnnotatedElement> elements = new ArrayList<>();
		for (String name : names) {
			try {
				elements.add(
						new OrderedAnnotatedElement(this.metadataReaderFactory, name));
			}
			catch (IOException e) {
				continue;
			}
		}
		AnnotationAwareOrderComparator.sort(elements);

		String[] classNames = elements.stream().map(e -> e.name).toArray(String[]::new);

		return classNames;
	}

这段代码大致逻辑就是将spring.facories配置文件中BootstrapConfiguration.class对应的实现类(此处实现类非JAVA意义上的实现类)加载为Spring配置类。

最后我们可以在spring.facories配置文件中看到PropertySourceBootstrapConfiguration。

在回到BootstrapApplicationListener#onApplicationEvent方法中,当BootstrapContext创建后会调用org.springframework.cloud.bootstrap.BootstrapApplicationListener#apply方法,这时会从父容器也就是BootstrapContext中获取ApplicationContextInitializer接口对应的Bean,并将其设置为子容器的ApplicationContextInitializer,当子容器生成时就会执行这些定制器。

最后来看下PropertySourceBootstrapConfiguration里是如何加载远程配置的。

	@Autowired(required = false)
	private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

	@Override
	public void initialize(ConfigurableApplicationContext applicationContext) {
		CompositePropertySource composite = new CompositePropertySource(
				BOOTSTRAP_PROPERTY_SOURCE_NAME);
		AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
		boolean empty = true;
		ConfigurableEnvironment environment = applicationContext.getEnvironment();
		for (PropertySourceLocator locator : this.propertySourceLocators) {
			PropertySource<?> source = null;
			source = locator.locate(environment);
			if (source == null) {
				continue;
			}
			logger.info("Located property source: " + source);
			composite.addPropertySource(source);
			empty = false;
		}
		if (!empty) {
			MutablePropertySources propertySources = environment.getPropertySources();
			String logConfig = environment.resolvePlaceholders("${logging.config:}");
			LogFile logFile = LogFile.get(environment);
			if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
				propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
			}
			insertPropertySources(propertySources, composite);
			reinitializeLoggingSystem(environment, logConfig, logFile);
			setLogLevels(applicationContext, environment);
			handleIncludedProfiles(environment);
		}
	}

这里可以看到PropertySourceBootstrapConfiguration通过循环propertySourceLocators列表来获取配置信息。

而propertySourceLocators是PropertySourceLocator接口实现类的列表,通过@Autowired注解注入。我们可以自己实现PropertySourceLocator接口,并注册到Spring父容器中(BootstrapContext)。通过在spring.factories中添加如下配置即可添加Bean到父容器。

# 注册DemoPropertySourceLocator到spring父容器
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
test.customized.bean.DemoPropertySourceLocator
/*
 * Copyright 2013-2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.config.client;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.config.client.ConfigClientProperties.Credentials;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.retry.annotation.Retryable;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION;

/**
 * @author Dave Syer
 * @author Mathieu Ouellet
 *
 */
@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {

	private static Log logger = LogFactory
			.getLog(ConfigServicePropertySourceLocator.class);

	private RestTemplate restTemplate;
	private ConfigClientProperties defaultProperties;

	public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
		this.defaultProperties = defaultProperties;
	}

	@Override
	@Retryable(interceptor = "configServerRetryInterceptor")
	public org.springframework.core.env.PropertySource<?> locate(
			org.springframework.core.env.Environment environment) {
		ConfigClientProperties properties = this.defaultProperties.override(environment);
		CompositePropertySource composite = new CompositePropertySource("configService");
		RestTemplate restTemplate = this.restTemplate == null
				? getSecureRestTemplate(properties)
				: this.restTemplate;
		Exception error = null;
		String errorBody = null;
		try {
			String[] labels = new String[] { "" };
			if (StringUtils.hasText(properties.getLabel())) {
				labels = StringUtils
						.commaDelimitedListToStringArray(properties.getLabel());
			}
			String state = ConfigClientStateHolder.getState();
			// Try all the labels until one works
			for (String label : labels) {
				Environment result = getRemoteEnvironment(restTemplate, properties,
						label.trim(), state);
				if (result != null) {
					log(result);

					if (result.getPropertySources() != null) { // result.getPropertySources()
																// can be null if using
																// xml
						for (PropertySource source : result.getPropertySources()) {
							@SuppressWarnings("unchecked")
							Map<String, Object> map = (Map<String, Object>) source
									.getSource();
							composite.addPropertySource(
									new MapPropertySource(source.getName(), map));
						}
					}

					if (StringUtils.hasText(result.getState())
							|| StringUtils.hasText(result.getVersion())) {
						HashMap<String, Object> map = new HashMap<>();
						putValue(map, "config.client.state", result.getState());
						putValue(map, "config.client.version", result.getVersion());
						composite.addFirstPropertySource(
								new MapPropertySource("configClient", map));
					}
					return composite;
				}
			}
		}
		catch (HttpServerErrorException e) {
			error = e;
			if (MediaType.APPLICATION_JSON
					.includes(e.getResponseHeaders().getContentType())) {
				errorBody = e.getResponseBodyAsString();
			}
		}
		catch (Exception e) {
			error = e;
		}
		if (properties.isFailFast()) {
			throw new IllegalStateException(
					"Could not locate PropertySource and the fail fast property is set, failing",
					error);
		}
		logger.warn("Could not locate PropertySource: " + (errorBody == null
				? error == null ? "label not found" : error.getMessage()
				: errorBody));
		return null;

	}

	private void log(Environment result) {
		if (logger.isInfoEnabled()) {
			logger.info(String.format(
					"Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",
					result.getName(),
					result.getProfiles() == null ? ""
							: Arrays.asList(result.getProfiles()),
					result.getLabel(), result.getVersion(), result.getState()));
		}
		if (logger.isDebugEnabled()) {
			List<PropertySource> propertySourceList = result.getPropertySources();
			if (propertySourceList != null) {
				int propertyCount = 0;
				for (PropertySource propertySource : propertySourceList) {
					propertyCount += propertySource.getSource().size();
				}
				logger.debug(String.format(
						"Environment %s has %d property sources with %d properties.",
						result.getName(), result.getPropertySources().size(),
						propertyCount));
			}

		}
	}

	private void putValue(HashMap<String, Object> map, String key, String value) {
		if (StringUtils.hasText(value)) {
			map.put(key, value);
		}
	}

	private Environment getRemoteEnvironment(RestTemplate restTemplate,
			ConfigClientProperties properties, String label, String state) {
		String path = "/{name}/{profile}";
		String name = properties.getName();
		String profile = properties.getProfile();
		String token = properties.getToken();
		int noOfUrls = properties.getUri().length;
		if (noOfUrls > 1) {
			logger.info("Multiple Config Server Urls found listed.");
		}

		Object[] args = new String[] { name, profile };
		if (StringUtils.hasText(label)) {
			if (label.contains("/")) {
				label = label.replace("/", "(_)");
			}
			args = new String[] { name, profile, label };
			path = path + "/{label}";
		}
		ResponseEntity<Environment> response = null;

		for (int i = 0; i < noOfUrls; i++) {
			Credentials credentials = properties.getCredentials(i);
			String uri = credentials.getUri();
			String username = credentials.getUsername();
			String password = credentials.getPassword();

			logger.info("Fetching config from server at : " + uri);

			try {
				HttpHeaders headers = new HttpHeaders();
				addAuthorizationToken(properties, headers, username, password);
				if (StringUtils.hasText(token)) {
					headers.add(TOKEN_HEADER, token);
				}
				if (StringUtils.hasText(state) && properties.isSendState()) {
					headers.add(STATE_HEADER, state);
				}

				final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
				response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
						Environment.class, args);
			}
			catch (HttpClientErrorException e) {
				if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
					throw e;
				}
			}
			catch (ResourceAccessException e) {
				logger.info("Connect Timeout Exception on Url - " + uri
						+ ". Will be trying the next url if available");
				if (i == noOfUrls - 1)
					throw e;
				else
					continue;
			}

			if (response == null || response.getStatusCode() != HttpStatus.OK) {
				return null;
			}

			Environment result = response.getBody();
			return result;
		}

		return null;
	}

	public void setRestTemplate(RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}

	private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
		SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
		if (client.getRequestReadTimeout() < 0) {
			throw new IllegalStateException("Invalid Value for Read Timeout set.");
		}
		requestFactory.setReadTimeout(client.getRequestReadTimeout());
		RestTemplate template = new RestTemplate(requestFactory);
		Map<String, String> headers = new HashMap<>(client.getHeaders());
		if (headers.containsKey(AUTHORIZATION)) {
			headers.remove(AUTHORIZATION); // To avoid redundant addition of header
		}
		if (!headers.isEmpty()) {
			template.setInterceptors(Arrays.<ClientHttpRequestInterceptor> asList(
					new GenericRequestHeaderInterceptor(headers)));
		}

		return template;
	}

	private void addAuthorizationToken(ConfigClientProperties configClientProperties,
			HttpHeaders httpHeaders, String username, String password) {
		String authorization = configClientProperties.getHeaders().get(AUTHORIZATION);

		if (password != null && authorization != null) {
			throw new IllegalStateException(
					"You must set either 'password' or 'authorization'");
		}

		if (password != null) {
			byte[] token = Base64Utils.encode((username + ":" + password).getBytes());
			httpHeaders.add("Authorization", "Basic " + new String(token));
		}
		else if (authorization != null) {
			httpHeaders.add("Authorization", authorization);
		}

	}

	public static class GenericRequestHeaderInterceptor
			implements ClientHttpRequestInterceptor {

		private final Map<String, String> headers;

		public GenericRequestHeaderInterceptor(Map<String, String> headers) {
			this.headers = headers;
		}

		@Override
		public ClientHttpResponse intercept(HttpRequest request, byte[] body,
				ClientHttpRequestExecution execution) throws IOException {
			for (Entry<String, String> header : headers.entrySet()) {
				request.getHeaders().add(header.getKey(), header.getValue());
			}
			return execution.execute(request, body);
		}

		protected Map<String, String> getHeaders() {
			return headers;
		}

	}
}

这里贴一下Spring Cloud Config的org.springframework.cloud.config.client.ConfigServicePropertySourceLocator是加载远程配置的代码。

Spring Cloud多个环境配置项重复优先级问题

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
	/**
	 * Set the parent of this application context.
	 * <p>The parent {@linkplain ApplicationContext#getEnvironment() environment} is
	 * {@linkplain ConfigurableEnvironment#merge(ConfigurableEnvironment) merged} with
	 * this (child) application context environment if the parent is non-{@code null} and
	 * its environment is an instance of {@link ConfigurableEnvironment}.
	 * @see ConfigurableEnvironment#merge(ConfigurableEnvironment)
	 */
	@Override
	public void setParent(@Nullable ApplicationContext parent) {
		this.parent = parent;
		if (parent != null) {
			Environment parentEnvironment = parent.getEnvironment();
			if (parentEnvironment instanceof ConfigurableEnvironment) {
            	// 合并父级上下文环境
				getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
			}
		}
	}
}

SpringApplication设置父级上线文时候会进行Environment的合并,本文中即合并父级上下文BootstrapContext对应的Environment。

	@Override
	public void merge(ConfigurableEnvironment parent) {
		for (PropertySource<?> ps : parent.getPropertySources()) {
			if (!this.propertySources.contains(ps.getName())) {
				this.propertySources.addLast(ps);
			}
		}
		String[] parentActiveProfiles = parent.getActiveProfiles();
		if (!ObjectUtils.isEmpty(parentActiveProfiles)) {
			synchronized (this.activeProfiles) {
				for (String profile : parentActiveProfiles) {
					this.activeProfiles.add(profile);
				}
			}
		}
		String[] parentDefaultProfiles = parent.getDefaultProfiles();
		if (!ObjectUtils.isEmpty(parentDefaultProfiles)) {
			synchronized (this.defaultProfiles) {
				this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME);
				for (String profile : parentDefaultProfiles) {
					this.defaultProfiles.add(profile);
				}
			}
		}
	}

子级中没有配置名对应的配置项才会保留到子级上下文环境中,此处的配置名并非配置项名,而是某个配置源的配置名,如通过PropertySourceBootstrapConfiguration加载的配置项的配置源名为bootstrapProperties。

PropertySourceBootstrapConfiguration是在上下文合并后,子级上下文创建后执行的,比ConfigFileApplicationListener执行要晚,所以此时已经有application.properties/application.yml的配置信息在子级Environment中了。

PropertySourceBootstrapConfiguration#insertPropertySources方法定义了远程配置的顺序逻辑。

private void insertPropertySources(MutablePropertySources propertySources,
			CompositePropertySource composite) {
		MutablePropertySources incoming = new MutablePropertySources();
		incoming.addFirst(composite);
		PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
		// 绑定配置信息到remoteProperties
    	Binder.get(environment(incoming)).bind("spring.cloud.config", Bindable.ofInstance(remoteProperties));
		if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
				&& remoteProperties.isOverrideSystemProperties())) {
            // 如果不允许额外配置被覆盖配置或者 允许覆盖额外配置但覆盖级别为高优先级并且允许覆盖系统配置
            // 则将额外配置放在第一位
			propertySources.addFirst(composite);
			return;
		}
		if (remoteProperties.isOverrideNone()) {
            // 如果额外配置为低优先级,则将额外配置放末尾
			propertySources.addLast(composite);
			return;
		}
		if (propertySources
				.contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
			if (!remoteProperties.isOverrideSystemProperties()) {
                // 原先已经包含系统配置,并且不允许覆盖系统配置,则将额外配置放在系统配置之后
				propertySources.addAfter(
						StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
						composite);
			}
			else {
                // 否则放在系统配置之前
				propertySources.addBefore(
						StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
						composite);
			}
		}
		else {
            // 没有系统配置则放在配置末尾
			propertySources.addLast(composite);
		}
}

@ConfigurationProperties("spring.cloud.config")
public class PropertySourceBootstrapProperties {

	/**
	 * Flag to indicate that the external properties should override system properties.
	 * Default true.
	 */
	private boolean overrideSystemProperties = true;

	/**
	 * Flag to indicate that {@link #isOverrideSystemProperties()
	 * systemPropertiesOverride} can be used. Set to false to prevent users from changing
	 * the default accidentally. Default true.
	 */
	private boolean allowOverride = true;

	/**
	 * Flag to indicate that when {@link #setAllowOverride(boolean) allowOverride} is
	 * true, external properties should take lowest priority, and not override any
	 * existing property sources (including local config files). Default false.
	 */
	private boolean overrideNone = false;
}

根据源码可以看到PropertySourceBootstrapConfiguration根据PropertySourceBootstrapProperties的配置信息来决定额外配置(远程配置)的优先级。

配置项

配置信息

默认值

overrideSystemProperties

允许额外配置覆盖系统配置

true

allowOverride

额外配置是否允许被覆盖

true

overrideNone

额外配置是否为低优先级

false

默认情况下额外配置不允许被覆盖,并且为高优先级,并且覆盖系统配置。

public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
    
	@Nullable
	protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
		if (this.propertySources != null) {
			for (PropertySource<?> propertySource : this.propertySources) {
				if (logger.isTraceEnabled()) {
					logger.trace("Searching for key '" + key + "' in PropertySource '" +
							propertySource.getName() + "'");
				}
				Object value = propertySource.getProperty(key);
				if (value != null) {
					if (resolveNestedPlaceholders && value instanceof String) {
						value = resolveNestedPlaceholders((String) value);
					}
					logKeyFound(key, propertySource, value);
					return convertValueIfNecessary(value, targetValueType);
				}
			}
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Could not find key '" + key + "' in any property source");
		}
		return null;
	}
}

Environment委托PropertySourcesPropertyResolver来获取环境变量值,而上面代码中可以看到PropertySourcesPropertyResolver通过循环propertySources列表来获取具体的PropertySource,并通过key从每个PropertySource中获取具体的属性值,如果获取到具体值则转换后返回。所以排在前面的PropertySource获取配置值的优先级更高。

Spring Cloud加载活动图

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值