Spring Cloud Config原理


最近学习SpringCloud感觉要疯了,硬着头皮写完了原理分析,有些原理理解的不是很透彻,还需要在琢磨琢磨。坚持吧!

1 @Value注解

通过上篇代码可知,我们只需要使用 @Value 注解,就能完成属性的注入。而 @Value 基于 Environment 的机制。我们先来看一下 @Value的注入过程,而 @Value 基于 Environment 的机制,具体请参考spring-cloud-config 源码解析,本文主要讲Environment初始化过程。

2 Spring Environment的初始化

在spring boot的启动流程中,有一个 prepareEnvironment 方法,这个方法就是用来准备Environment这个对象的,

  • springApplication.run -> prepareEnvironment
    这个方法主要就是创建和配置spring容器的环境信息,源码如下
	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		//根据上下文,创建一个合适的Environment对象
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		//配置Environment的propertySource、以及profile
		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;
	}

2.1 getOrCreateEnvironment

点击getOrCreateEnvironment() ,这个方法是根据当前的webApplication类型匹配对应的environment,当前默认的应该就是StandardServletEnvironment

	private ConfigurableEnvironment getOrCreateEnvironment() {
		if (this.environment != null) {
			return this.environment;
		}
		switch (this.webApplicationType) {
		case SERVLET:
			return new StandardServletEnvironment();
		case REACTIVE:
			return new StandardReactiveWebEnvironment();
		default:
			return new StandardEnvironment();
		}
	}

StandardServletEnvironment 是整个spring boot应用运行环境的实现类,后面所有的关于环境相关的配置操作都是基于这个类,它的类的结构图如下:
在这里插入图片描述
StandardServletEnvironment 的初始化过程
StandardServletEnvironment创建实例时会调用父类AbstractEnvironment无参构造器,AbstractEnvironment ,在这个类的构造方法中,会
调用一个自定义配置文件的方法customizePropertySources(). 这个方法被StandardServletEnvironment重写,所以最终调用StandardServletEnvironment 中的 customizePropertySources 方法

protected void customizePropertySources(MutablePropertySources propertySources) {
		propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
		propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
		if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
			propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
		}
		super.customizePropertySources(propertySources);
	}

这里是将几个不同的配置源封装成 StubPropertySource 和JndiPropertySource添加到
MutablePropertySources 中,调用 addLast 是表示一直往最后的位置添加。
SERVLET_CONFIG_PROPERTY_SOURCE_NAME:servlet的配置信息
SERVLET_CONTEXT_PROPERTY_SOURCE_NAME: 这个是servlet初始化的上下文,也就是以前我们在web.xml中配置的 context-param 。
JNDI_PROPERTY_SOURCE_NAME: 加载jndi.properties配置信息。

MutablePropertySources
在上面的代码中可以看到,所有的外部资源配置都是添加到了一个MutablePropertySources对象中,这个对象封装了属性资源的集合。
在AbstractEnvironment 这个类里定义了MutablePropertySources。并且把这个MutablePropertySources作为参数传递给了ConfigurablePropertyResolver 配置解析器中,而这个配置解析器是一个PropertySourcesPropertyResolver 实例。

public abstract class AbstractEnvironment implements ConfigurableEnvironment { 
	private final MutablePropertySources propertySources = new MutablePropertySources(); 
	private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources); 
	}

来看一下这个类关系图
在这里插入图片描述
AbstractEnvironment 实现了文件解析器ConfigurablePropertyResolver ,而在上面这段代码中我们把 MutablePropertySources 传递到PropertySourcesPropertyResolver 中。这样就可以让 AbstractEnvironment 具备文件解析的功能,只是这个功能,委托给了PropertySourcesPropertyResolver来实现

2.2 configureEnvironment()

接下来再看SpringApplication.configureEnvironment()方法

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        //添加类型转化的服务
		if (this.addConversionService) {
			ConversionService conversionService = ApplicationConversionService.getSharedInstance();
			environment.setConversionService((ConfigurableConversionService) conversionService);
		}
		//配置Environment中的propertysources
		configurePropertySources(environment, args);
		//配置profiles
		configureProfiles(environment, args);
	}

1 点击configurePropertySources()

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
		MutablePropertySources sources = environment.getPropertySources();
		//设置defaultProperties属性来源,如果设置了默认属性,则会加载
		if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
			sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
		}
		//设置commandLineProperties来源,如果设置了命令行参数,则会加载
		if (this.addCommandLineProperties && args.length > 0) {
			String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
			if (sources.contains(name)) {
				PropertySource<?> source = sources.get(name);
				CompositePropertySource composite = new CompositePropertySource(name);
				composite.addPropertySource(
						new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
				composite.addPropertySource(source);
				sources.replace(name, composite);
			}
			else {
				sources.addFirst(new SimpleCommandLinePropertySource(args));
			}
		}
	}

2 configureProfiles()

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
		Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
		profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
		environment.setActiveProfiles(StringUtils.toStringArray(profiles));
	}

2.3 listeners.environmentPrepared(environment)

通知监听器,加载配置文件
点击SpringApplicationRunListeners.environmentPrepared() 方法

	void environmentPrepared(ConfigurableEnvironment environment) {
   	for (SpringApplicationRunListener listener : this.listeners) {
   		listener.environmentPrepared(environment);
   	}
   

再点击EventPublishingRunListener.environmentPrepared() 方法最终进入SimpleApplicationEventMulticaster.multicastEvent()

	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
   	ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
   	Executor executor = getTaskExecutor();
   	for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
   		if (executor != null) {
   			executor.execute(() -> invokeListener(listener, event));
   		}
   		else {
   			invokeListener(listener, event);
   		}
   	}
   }

getApplicationListeners()会返回多个监听器
在这里插入图片描述

其中ConfigFileApplicationListener.onApplicationEvent 收到事件之后,会执行如下代码

public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}

因为我们是环境准备的监听事件,会进入onApplicationEnvironmentPreparedEvent()方法,最终进入ConfigFileApplicationListener.addPropertySources()

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
		//添加一个RandomValuePropertySource到Environment的MutablePropertySources中
		RandomValuePropertySource.addToEnvironment(environment);
		//加载spring boot中的配置信息,比如application.yml或者application.prop
		new Loader(environment, resourceLoader).load();
	}

Loader(environment, resourceLoader) 构造器:

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
			this.environment = environment;
			this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
			this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
			//解析不同类型的配置文件(yml和properties)
			this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
					getClass().getClassLoader());
		}

点击Loader(environment, resourceLoader).load() 方法,
这个方法比较复杂,总的来说,就是加载所有可能的profiles

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<>();
						//TODO 加载存在已经激活的 profiles
						initializeProfiles();
						//遍历所有profiles
						while (!this.profiles.isEmpty()) {
							Profile profile = this.profiles.poll();/
							if (isDefaultProfile(profile)) {
								addProfileToEnvironment(profile.getName());
							}
							// TODO 确定搜索范围,获取对应的配置文件名,并使用相应加载器加载
							load(profile, this::getPositiveProfileFilter,
									addToLoaded(MutablePropertySources::addLast, false));
							// 将处理完的 profile添加到 processedProfiles列表当中,表示已经处理完成
							this.processedProfiles.add(profile);
						}
						load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
						addLoadedPropertySources();
						// 更新 activeProfiles列表
						applyActiveProfiles(defaultProperties);
					});
		}

再点击上述代码中initializeProfiles() 方法

private void initializeProfiles() {
			// The default profile for these purposes is represented as null. We add it
			// first so that it is processed first and has lowest priority.
			this.profiles.add(null);
			Binder binder = Binder.get(this.environment);
			//判断当前环境是否配置 spring.profiles.active属性
			Set<Profile> activatedViaProperty = getProfiles(binder, ACTIVE_PROFILES_PROPERTY);
			//判断当前环境是否配置 spring.profiles.include属性
			Set<Profile> includedViaProperty = getProfiles(binder, INCLUDE_PROFILES_PROPERTY);
			//如果没有特别指定,就是 application.properties 和 application- default.properties配置
			List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
			this.profiles.addAll(otherActiveProfiles);
			// Any pre-existing active profiles set via property sources (e.g.
			// System properties) take precedence over those added in config files.
			this.profiles.addAll(includedViaProperty);
			addActiveProfiles(activatedViaProperty);
			// 如果 profiles集仍然为null,即没有指定,就会创建默认的profile
			if (this.profiles.size() == 1) { // only has null profile
				for (String defaultProfileName : this.environment.getDefaultProfiles()) {
					Profile defaultProfile = new Profile(defaultProfileName, true);
					this.profiles.add(defaultProfile);
				}
			}
		}

继续跟进load方法,通过 getSearchLoacations 进行搜索,并且进行迭代

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
			//获取需要遍历的目标路径
			getSearchLocations().forEach((location) -> {
				boolean isDirectory = location.endsWith("/");
				Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
				//拼接对应路径,选择合适的yml或者properties解析器进行解析
				names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
			});
		}

点击getSearchLocations()
主要功能就是获取需要遍历的目标路径,默认情况下,会去
DEFAULT_SEARCH_LOCATIONS中查找,也就是classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/

private Set<String> getSearchLocations() {
			Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
			if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
				locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY));
			}
			else {
				locations.addAll(
						asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
			}
			return locations;
		}

整理流程如下:
1)获取默认的配置文件路径,有4种。
2)遍历所有的路径,拼装配置文件名称。
3)再遍历解析器,选择yml或者properties解析,将解析结果添加到集合MutablePropertySources当中。

至此,springBoot中的资源文件加载完毕,解析顺序从上到下,所以前面的配置文件会覆盖后面的配置文件。可以看到 application.properties 的优先级最低,系统变量和环境变量的优先级相对较高。

3 config中的environment

在Spring Cloud Config中,我们通过@Value注解注入了一个属性,但是这个属性不存在于本地配置中,那么Config是如何将远程配置信息加载到Environment中的呢?
这里我们需要思考几个问题:

  • 如何将配置加载到 Environment
  • 配置变更时,如何控制 Bean 是否需要 create,重新触发一次 Bean 的初始化,才能将 @Value 注解指定的字段从 Environment 中重新注入。
  • 配置变更时,如何控制新的配置会更新到 Environment 中,才能保证配置变更时可注入最新的值

为了解决这三个问题,Spring Cloud Config规范中定义了三个核心的接口
PropertySourceLocator:抽象出这个接口,就是让用户可定制化的将一些配置加载到Environment。这部分的配置获取遵循了 Spring Cloud Config 的理念,即希望能从外部储存介质中来 loacte。
RefreshScope: Spring Cloud 定义这个注解,是扩展了 Spring 原有的 Scope 类型。用来标识当前这个 Bean 是一个refresh 类型的 Scope。其主要作用就是可以控制 Bean 的整个生命周期。
ContextRefresher:抽象出这个 Class,是让用户自己按需来刷新上下文(比如当有配置刷新时,希望可以刷新上下文,将最新的配置更新到 Environment,重新创建 Bean 时,就可以从Environment 中注入最新的配置)。

那么我们去探索一下Environment是如何在启动过程中从远程服务器上加载配置的呢?
ConfigServicePropertySourceLocator实现了PropertySourceLocator,最终会调用
locate()方法

public org.springframework.core.env.PropertySource<?> locate(
			org.springframework.core.env.Environment environment) {
		ConfigClientProperties properties = this.defaultProperties.override(environment);
		CompositePropertySource composite = new OriginTrackedCompositePropertySource(
				"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);

					// result.getPropertySources() can be null if using xml
					if (result.getPropertySources() != null) {
						for (PropertySource source : result.getPropertySources()) {
							@SuppressWarnings("unchecked")
							Map<String, Object> map = translateOrigins(source.getName(),
									(Map<String, Object>) source.getSource());
							composite.addPropertySource(
									new OriginTrackedMapPropertySource(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;
				}
			}
			errorBody = String.format("None of labels %s found", Arrays.toString(labels));
		}
		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"
							+ (errorBody == null ? "" : ": " + errorBody),
					error);
		}
		logger.warn("Could not locate PropertySource: "
				+ (error != null ? error.getMessage() : errorBody));
		return null;

	}

4 Config Server获取配置过程

Spring Cloud Config Server提供了EnvironmentController,这样通过在浏览器访问即可从git中获取配置信息,在这个controller中,提供了很多的映射,最终会调用的是 getEnvironment

public Environment getEnvironment(String name, String profiles, String label, boolean includeOrigin) {
        name = this.normalize(name);
        label = this.normalize(label);
        Environment environment = this.repository.findOne(name, profiles, label, includeOrigin);
        if (this.acceptEmpty || environment != null && !environment.getPropertySources().isEmpty()) {
            return environment;
        } else {
            throw new EnvironmentNotFoundException("Profile Not found");
        }
    }

this.repository.findOne ,调用某个repository存储组件来获得环境配置信息进行返回。
repository是一个 EnvironmentRepository 对象,它有很多实现,其中就包含
RedisEnvironmentRepository 、 JdbcEnvironmentRepository 等。默认实现是
MultipleJGitEnvironmentRepository ,表示多个不同地址的git数据源。
点击MultipleJGitEnvironmentRepository.findOne()

public Environment findOne(String application, String profile, String label,
			boolean includeOrigin) {
			//遍历所有Git源
		for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {
			if (repository.matches(application, profile, label)) {
				for (JGitEnvironmentRepository candidate : getRepositories(repository,
						application, profile, label)) {
					try {
						if (label == null) {
							label = candidate.getDefaultLabel();
						}
						Environment source = candidate.findOne(application, profile,
								label, includeOrigin);
						if (source != null) {
							return source;
						}
					}
					catch (Exception e) {
						if (this.logger.isDebugEnabled()) {
							this.logger.debug(
									"Cannot load configuration from " + candidate.getUri()
											+ ", cause: (" + e.getClass().getSimpleName()
											+ ") " + e.getMessage(),
									e);
						}
						continue;
					}
				}
			}
		}
		JGitEnvironmentRepository candidate = getRepository(this, application, profile,
				label);
		if (label == null) {
			label = candidate.getDefaultLabel();
		}
		if (candidate == this) {
			return super.findOne(application, profile, label, includeOrigin);
		}
		return candidate.findOne(application, profile, label, includeOrigin);
	}

点击super.findOne 进入父类AbstractScmEnvironmentRepository.findOne()

public synchronized Environment findOne(String application, String profile,
			String label, boolean includeOrigin) {
		NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(
				getEnvironment(), new NativeEnvironmentProperties());
				//从GIT远程仓库同步到本地
		Locations locations = getLocations(application, profile, label);
		delegate.setSearchLocations(locations.getLocations());
		Environment result = delegate.findOne(application, profile, "", includeOrigin);
		result.setVersion(locations.getVersion());
		result.setLabel(label);
		return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(),
				getUri());
	}

点击getLocations() 进入JGitEnvironmentRepository.getLocations()

public synchronized Locations getLocations(String application, String profile,
			String label) {
		if (label == null) {
			label = this.defaultLabel;
		}
		String version = refresh(label);
		return new Locations(application, profile, label, version,
				getSearchLocations(getWorkingDirectory(), application, profile, label));
	}

其中refresh方法,每次浏览器请求时都会重新去远程服务器拉取,所以config server端可以实时感知配置内容的变化

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值