spring-cloud-config 源碼解析:
本文主要針對 spring-cloud-dependencies Hoxton.SR4版本, spring-cloud-config-server/client的 2.2.2.RELEASE 版本進行源碼的解析。
對於未接觸過 Config 的小伙伴可以參考 https://www.cnblogs.com/wuzhenzhao/p/10670580.html 進行一些基礎知識的了解。
本文主要從以下幾個點來分析:
- @Value 的源碼解析
- Environment的初始化
- SpringBoot 配置的加載過程
- Config Client 配置加載過程
- Config Server獲取配置過程
- 實現自定義配置中心 PropertySourceLocator (基於拓展org.springframework.cloud.bootstrap.BootstrapConfiguration)
@Value 的源碼解析
在 spring-cloud-config 配置中心的使用中,我們通常只需要使用 @Value 注解,就能完成屬性的注入。而 @Value 基於 Environment 的機制。我們先來看一下 @Value的注入過程
以如下代碼為例:
@Value("${wuzz.name}") private String name;
@Value 發生於 依賴注入的階段,對於依賴注入不熟悉的朋友可以先參考:https://www.cnblogs.com/wuzhenzhao/p/10882050.html
在注入的流程中有個至關重要的方法:AbstractAutowireCapableBeanFactory#populateBean
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { //........ PropertyDescriptor[] filteredPds = null; if (hasInstAwareBpps) { if (pvs == null) { pvs = mbd.getPropertyValues(); } for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; // @Value 的注入入口,基於 AutowiredAnnotationBeanPostProcessor PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName); if (pvsToUse == null) { if (filteredPds == null) { filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); } pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); if (pvsToUse == null) { return; } } pvs = pvsToUse; } } } //....... }
基於 AutowiredAnnotationBeanPostProcessor#postProcessProperties ,這個東西有點類似於類的后置處理器。
@Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { // 掃描注解元數據
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { // 注入
metadata.inject(bean, beanName, pvs); } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); } return pvs; }
調用重載的方法 inject,這里有很多的實現,基於目前我們注入是一個 屬性,調用到 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject ,最終會進入 DefaultListableBeanFactory#doResolveDependency
@Nullable public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor); try { Object shortcut = descriptor.resolveShortcut(this); if (shortcut != null) { return shortcut; } Class<?> type = descriptor.getDependencyType(); // 獲取 @Value 的表達式
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); if (value != null) { if (value instanceof String) { //處理表達式的值,其實就是從 Eviromengt里面去獲取
String strVal = resolveEmbeddedValue((String) value); BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); value = evaluateBeanDefinitionString(strVal, bd); } TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); try { return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor()); } catch (UnsupportedOperationException ex) { // A custom TypeConverter which does not support TypeDescriptor resolution... // 轉換類型,進行實際注入
return (descriptor.getField() != null ? converter.convertIfNecessary(value, type, descriptor.getField()) : converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); } } // ........
} finally { ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint); } }
DefaultListableBeanFactory#doResolveDependency 作用是處理 bean中的依賴。由此可見,處理 @Value 注解的時機是在getBean
方法中,即SpringApplication#run
的最后一步,實例化 bean。當獲取 @Value 注解中的表達式之后,進入了 resolveEmbeddedValue 方法,來替換表達式的值:
public String resolveEmbeddedValue(@Nullable String value) { if (value == null) { return null; } String result = value; // 遍歷 StringValueResolver for (StringValueResolver resolver : this.embeddedValueResolvers) { result = resolver.resolveStringValue(result); if (result == null) { return null; } } return result; }
通過代碼邏輯我們看到,對於屬性的解析已經委托給了StringValueResolver 對應的實現類,接下來我們就要分析一下這個 StringValueResolver 是如何初始化的。
StringValueResolver 的初始化 依賴於 PropertySourcesPlaceholderConfigurer ,來看一下該類類圖:
很明顯 ,該類實現了 BeanFactoryPostProcessor ,必然會執行 postProcessBeanFactory 方法。
@Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { if (this.propertySources == null) { this.propertySources = new MutablePropertySources(); if (this.environment != null) { this.propertySources.addLast( new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { @Override @Nullable public String getProperty(String key) { return this.source.getProperty(key); } } ); } try { PropertySource<?> localPropertySource = new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties()); if (this.localOverride) { this.propertySources.addFirst(localPropertySource); } else { this.propertySources.addLast(localPropertySource); } } catch (IOException ex) { throw new BeanInitializationException("Could not load properties", ex); } } // 創建替換 ${...} 表達式的處理器 processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources)); this.appliedPropertySources = this.propertySources; }
然后進入 processProperties :
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, final ConfigurablePropertyResolver propertyResolver) throws BeansException { // 設置占位符的前綴:"{" propertyResolver.setPlaceholderPrefix(this.placeholderPrefix); // 設置占位符的后綴:"}" propertyResolver.setPlaceholderSuffix(this.placeholderSuffix); // 設置默認值分隔符:":" propertyResolver.setValueSeparator(this.valueSeparator); // 生成處理 ${...} 表達式的處理器 StringValueResolver valueResolver = strVal -> { String resolved = (this.ignoreUnresolvablePlaceholders ? propertyResolver.resolvePlaceholders(strVal) : propertyResolver.resolveRequiredPlaceholders(strVal)); if (this.trimValues) { resolved = resolved.trim(); } return (resolved.equals(this.nullValue) ? null : resolved); }; // 將處理器放入 Spring 容器 doProcessProperties(beanFactoryToProcess, valueResolver); } protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver); String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames(); for (String curName : beanNames) { // Check that we're not parsing our own bean definition, // to avoid failing on unresolvable placeholders in properties file locations. if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) { BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName); try { visitor.visitBeanDefinition(bd); } catch (Exception ex) { throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex); } } } // New in Spring 2.5: resolve placeholders in alias target names and aliases as well. beanFactoryToProcess.resolveAliases(valueResolver); // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes. // 將 StringValueResolver 存入 BeanFactory 中 beanFactoryToProcess.addEmbeddedValueResolver(valueResolver); } @Override public void addEmbeddedValueResolver(StringValueResolver valueResolver) { Assert.notNull(valueResolver, "StringValueResolver must not be null"); this.embeddedValueResolvers.add(valueResolver); }
回到 AbstractBeanFactory#resolveEmbeddedValue。這個時候我們能知道 embeddedValueResolvers 里面就一個實現,即 PropertySourcesPlaceholderConfigurer#processProperties 方法內構造的匿名內部類:
StringValueResolver valueResolver = strVal -> { // 默認是 false 走后面的邏輯 String resolved = (this.ignoreUnresolvablePlaceholders ? propertyResolver.resolvePlaceholders(strVal) : propertyResolver.resolveRequiredPlaceholders(strVal)); if (this.trimValues) { resolved = resolved.trim(); } return (resolved.equals(this.nullValue) ? null : resolved); };
然后這里會走propertyResolver.resolveRequiredPlaceholders(strVal)) ,而這里的 propertyResolver 即 PropertySourcesPropertyResolver,在 PropertySourcesPlaceholderConfigurer#postProcessBeanFactory 創建出來的。
由於其為重寫該方法,所以進入其父類 AbstractPropertyResolver#resolveRequiredPlaceholders:
@Override public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { if (this.strictHelper == null) { this.strictHelper = createPlaceholderHelper(false); } return doResolvePlaceholders(text, this.strictHelper); } private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) { return helper.replacePlaceholders(text, this::getPropertyAsRawString); }
然后來到了很關鍵的一步 this::getPropertyAsRawString :
@Override @Nullable protected String getPropertyAsRawString(String key) { return getProperty(key, String.class, false); } @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.isTraceEnabled()) { logger.trace("Could not find key '" + key + "' in any property source"); } return null; }
這里的 propertySources 也是 PropertySourcesPlaceholderConfigurer#postProcessBeanFactory 創建出來的:
if (this.environment != null) { this.propertySources.addLast( new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { @Override @Nullable public String getProperty(String key) { return this.source.getProperty(key); } } ); }
通過 Debug 可以看到相關信息
然后就會來到 PropertySourcesPlaceholderConfigurer#postProcessBeanFactory 匿名內部類中的 getProperty:
獲取到了一個 StandardServletEnvironment,然后通過該 Environment 類型進行獲取配置源,這個時候又會進到 PropertySourcesPropertyResolver#getProperty,但是這個時候所看到的 propertySources 屬性則如下圖所示
這就是所有的本環境下的配置源,然后遍歷這些配置源進行屬性的讀取。因為我們這里是直接通過application.properties 配置的 ,所以在 ConfigurationPropertySourcesPropertySource#getProperty 獲取到配置。
然后回到 DefaultListableBeanFactory#doResolveDependency 會通過 converter.convertIfNecessary 將獲取到的值進行注入到對應的Bean 里面,完成 @Value 的注入操作. 流程圖如下:
Environment的初始化
上面的 @Value 代碼的注入流程我們大致是了解了,但是這一過程有個 StandardServletEnvironment 是怎么初始化的 ,也就是spring boot需要解析的外部資源文件的路徑是如何初始化的。在spring boot的啟動流程中,有一個 prepareEnvironment 方法,這個方法就是用來准備Environment這個對象的。
springApplication.run -> prepareEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // Create and configure the environment // 根據上下文,創建一個合適的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; }
getOrCreateEnvironment 這個方法,就是根據當前的webApplication類型匹配對應的environment,當前默認的應該就是StandardServletEnvironment ,如果是spring webflflux,則是StandardReactiveWebEnvironment .
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 ,在這個類的構造方法中,會調用一個自定義配置文件的方法
public AbstractEnvironment() { customizePropertySources(this.propertySources); }
customizePropertySources 這個方法被 StandardServletEnvironment 重寫了,所以會調用StandardServletEnvironment 中的 customizePropertySources 方法。相信大家不難看出,這里是將幾個不同的配置源封裝成 StubPropertySource 添加到MutablePropertySources 中,調用 addLast 是表示一直往最后的位置添加。
- SERVLET_CONFIG_PROPERTY_SOURCE_NAME:servlet的配置信息,也就是在中配置的
- SERVLET_CONTEXT_PROPERTY_SOURCE_NAME: 這個是servlet初始化的上下文,也就是以前我們在web.xml中配置的 context-param 。
- JNDI_PROPERTY_SOURCE_NAME: 加載jndi.properties配置信息。
- SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME: 系統變量,通過System.setProperty設置的變量,默認可以看到 java.version 、 os.name 等。
- SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME: 系統環境變量,也就是我們配置JAVA_HOME的地方。
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
接着,我們來看一下它怎么用的,找到 AbstractEnvironment 這個類,在這里定義了 MutablePropertySources。並且把這個MutablePropertySources作為參數傳遞給了 ConfigurablePropertyResolver 配置解析器中,而這個配置解析器是一個 PropertySourcesPropertyResolver 實例。
private final MutablePropertySources propertySources = new MutablePropertySources(); private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
我們來看一下這個類關系圖, AbstractEnvironment 實現了文件解析器ConfigurablePropertyResolver ,而在上面這段代碼中我們把 MutablePropertySources 傳遞到PropertySourcesPropertyResolver 中。這樣就可以讓 AbstractEnvironment 具備文件解析的功能,只是這個功能,委托給了PropertySourcesPropertyResolver來實現。 這跟我們上面的分析是一致的.
通過上面的代碼,spring構造了一個 StandardServletEnvironment 對象並且初始化了一些需要解析的propertySource。我們繼續來看 configureEnvironment 這個方法,這個方法有兩個作用
- addConversionService 添加類型轉化的服務,我們知道properties文件中配置的屬性都是String類型的,而轉化為Java對象之后要根據合適的類型進行轉化,而 ConversionService 是一套通用的轉化方案,這里把這個轉化服務設置到當前的Environment,很顯然,就是為Environment配置解析時提供一個類型轉化的解決方案。這塊大家有空可以去研究一下,也是一個值的學習的設計。
- configurePropertySources 配置Environment中的propertysources,在上面五個屬性來源的基礎上又加了兩個configureProfiles :這個方法就比較容易理解,就是配置當前激活的profifiles,將當前的activeProfifiles設置到enviroment中。這樣就能夠使得我們完成不同環境下配置的獲取問題。
- 設置 defaultProperties 屬性來源
- 設置commandLineProperties來源,如果設置了命令行參數,則會加載SimpleCommandLinePropertySource 作為propertySource
這個defaultProperties是什么呢?給大家演示一個東西,就是說我們可以設置一個默認的屬性,如果設置過,則需要加載。否則就不需要。
public static void main(String[] args) { // SpringApplication.run(ConfigClientApp.class,args); // log.info("服務啟動成功"); SpringApplication springApplication=new SpringApplication(ConfigClientApp.class); Map<String, Object> pro = new HashMap<>(); pro.put("key", "value"); springApplication.setDefaultProperties(pro); springApplication.run(args); }
上述工作完成之后,就是發布一個environmentPrepared環境准備就緒的通知,具體的時間監聽過程的代碼就不再分析了,我們直接進入到 ConfigFileApplicationListener 這個監聽器,這個監聽器就是用來處理項目配置的。
SpringBoot 配置的加載過程
ConfigFileApplicationListener.onApplicationEvent 收到事件之后,會執行如下代碼onApplicationEnvironmentPreparedEvent
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { List<EnvironmentPostProcessor> postProcessors = loadPostProcessors(); // 把自己加進去了,意味着會執行 this.postProcessEnvironment postProcessors.add(this); AnnotationAwareOrderComparator.sort(postProcessors); for (EnvironmentPostProcessor postProcessor : postProcessors) { postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication()); } }
最終執行到 ConfigFileApplicationListener.addPropertySources 方法中,這個方法做兩個事情
- 添加一個RandomValuePropertySource到Environment的MutablePropertySources中
- 加載spring boot中的配置信息,比如application.yml或者application.properties
這塊的代碼就不繼續深入分析了,小伙伴自己感興趣的深入去看看。load 所做的事情如下:
- 獲取默認的配置文件路徑,有4種。private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";
- 遍歷所有的路徑,拼裝配置文件名稱。
- 再遍歷解析器,選擇yml或者properties解析,將解析結果添加到集合MutablePropertySources當中。
至此,springBoot中的資源文件加載完畢,解析順序從上到下,所以前面的配置文件會覆蓋后面的配置文件。可以看到 application.properties 的優先級最低,系統變量和環境變量的優先級相對較高。
結合上面 @Value 的流程,是不是就串起來了呢? 顯示SpringBoot 啟動的時候將加載相關配置到 Environment ,然后注入的時候去這個里面拿.
基於springboot得拓展 是 org.springframework.boot.env.EnvironmentPostProcessor 接口,需要實現該類,然后在 META-INF/spring.factories 指定該類實現即可。
Config Client 配置加載過程
- 如何將配置加載到 Environment
- 配置變更時,如何控制 Bean 是否需要 create,重新觸發一次 Bean 的初始化,才能將 @Value 注解指定的字段從 Environment 中重新注入。
- 配置變更時,如何控制新的配置會更新到 Environment 中,才能保證配置變更時可注入最新的值。
- PropertySourceLocator:抽象出這個接口,就是讓用戶可定制化的將一些配置加載到Environment。這部分的配置獲取遵循了 Spring Cloud Config 的理念,即希望能從外部儲存介質中來 loacte。
- RefreshScope: Spring Cloud 定義這個注解,是擴展了 Spring 原有的 Scope 類型。用來標識當前這個 Bean 是一個refresh 類型的 Scope。其主要作用就是可以控制 Bean 的整個生命周期。
- ContextRefresher:抽象出這個 Class,是讓用戶自己按需來刷新上下文(比如當有配置刷新時,希望可以刷新上下文,將最新的配置更新到 Environment,重新創建 Bean 時,就可以從Environment 中注入最新的配置)。
從前面的代碼分析過程中我們知道,Environment中所有外部化配置,針對不同類型的配置都會有與之對應的PropertySource,比如(SystemEnvironmentPropertySource、CommandLinePropertySource)。以及PropertySourcesPropertyResolver來進行解析。那Config Client在啟動的時候,必然也會需要從遠程服務器上獲取配置加載到Environment中,這樣才能使得應用程序通過@value進行屬性的注入,而且我們一定可以猜測到的是,這塊的工作一定又和spring中某個機制有關系。
在spring boot項目啟動時,有一個prepareContext的方法,它會回調所有實現了ApplicationContextInitializer 的實例,來做一些初始化工作。
public ConfigurableApplicationContext run(String... args) { //省略代碼... prepareContext(context, environment, listeners,applicationArguments, printedBanner); //省略代碼 return context; } protected void applyInitializers(ConfigurableApplicationContext context) { for (ApplicationContextInitializer initializer : getInitializers()) { Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class); Assert.isInstanceOf(requiredType, context, "Unable to call initializer."); initializer.initialize(context); } }
PropertySourceBootstrapConfiguration 實現了 ApplicationContextInitializer 接口,其目的就是在應用程序上下文初始化的時候做一些額外的操作.根據默認的 AnnotationAwareOrderComparator 排序規則對propertySourceLocators數組進行排序獲取運行的環境上下文ConfifigurableEnvironment 遍歷propertySourceLocators時 ,調用 locate 方法,傳入獲取的上下文environment,將source添加到PropertySource的鏈表中,設置source是否為空的標識標量empty。source不為空的情況,才會設置到environment中,返回Environment的可變形式,可進行的操作如addFirst、addLast,移除propertySources中的bootstrapProperties,根據config server覆寫的規則,設置propertySources,處理多個active profiles的配置信息 。
public void initialize(ConfigurableApplicationContext applicationContext) { List<PropertySource<?>> composite = new ArrayList<>(); //對propertySourceLocators數組進行排序,根據默認的AnnotationAwareOrderComparator AnnotationAwareOrderComparator.sort(this.propertySourceLocators); boolean empty = true; //獲取運行的環境上下文 ConfigurableEnvironment environment = applicationContext.getEnvironment(); for (PropertySourceLocator locator : this.propertySourceLocators) { //回調所有實現PropertySourceLocator接口實例的locate方法, Collection<PropertySource<?>> source = locator.locateCollection(environment); if (source == null || source.size() == 0) { continue; } List<PropertySource<?>> sourceList = new ArrayList<>(); for (PropertySource<?> p : source) { sourceList.add(new BootstrapPropertySource<>(p)); } logger.info("Located property source: " + sourceList); composite.addAll(sourceList);//將source添加到數組 empty = false;//表示propertysource不為空 } //只有propertysource不為空的情況,才會設置到environment中 if (!empty) { MutablePropertySources propertySources = environment.getPropertySources(); String logConfig = environment.resolvePlaceholders("${logging.config:}"); LogFile logFile = LogFile.get(environment); for (PropertySource<?> p : environment.getPropertySources()) { if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) { propertySources.remove(p.getName()); } } insertPropertySources(propertySources, composite); reinitializeLoggingSystem(environment, logConfig, logFile); setLogLevels(applicationContext, environment); handleIncludedProfiles(environment); } }
PropertySourceLoader.locateCollection,這個方法會調用子類的locate方法,來獲得一個PropertySource,然后將PropertySource集合返回。接着它會調用 ConfigServicePropertySourceLocator 的locate方法。
ConfigServicePropertySourceLocator.locate :這個就是Confifig Client的關鍵實現了,它會通過RestTemplate調用一個遠程地址獲得配置信息,getRemoteEnvironment 。然后把這個配置PropertySources,然后將這個信息包裝成一個OriginTrackedMapPropertySource,設置到 Composite 中。
Config Server獲取配置過程
服務器端去遠程倉庫加載配置的流程就比較簡單了,核心接口是: EnvironmentRepository ,提供了配置讀取的功能。我們先從請求入口開始看
EnvironmentController :Spring Cloud Config Server提供了EnvironmentController,這樣通過在瀏覽器訪問即可從git中獲取配置信息
在這個controller中,提供了很多的映射,最終會調用的是 getEnvironment 。
public Environment getEnvironment(String name, String profiles, String label, boolean includeOrigin) { name = Environment.normalize(name); label = Environment.normalize(label); Environment environment = this.repository.findOne(name, profiles, label, includeOrigin); if (!this.acceptEmpty && (environment == null || environment.getPropertySources().isEmpty())) { throw new EnvironmentNotFoundException("Profile Not found"); } return environment; }
this.repository.findOne ,調用某個repository存儲組件來獲得環境配置信息進行返回。repository是一個 EnvironmentRepository 對象,它有很多實現,其中就包含RedisEnvironmentRepository 、 JdbcEnvironmentRepository 等。默認實現是MultipleJGitEnvironmentRepository ,表示多個不同地址的git數據源。
MultipleJGitEnvironmentRepository.findOne :MultipleJGitEnvironmentRepository 代理遍歷每個 JGitEnvironmentRepository,JGitEnvironmentRepository 下使用 NativeEnvironmentRepository 代理讀取本地文件。
public Environment findOne(String application, String profile, String label, boolean includeOrigin) {
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); }
AbstractScmEnvironmentRepository.findOne :調用抽象類的findOne方法,主要有兩個核心邏輯
- 調用getLocations從GIT遠程倉庫同步到本地
- 使用 NativeEnvironmentRepository 委托來讀取本地文件內容
@Override public synchronized Environment findOne(String application, String profile, String label, boolean includeOrigin) { NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(), new NativeEnvironmentProperties()); 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 如下:
@Override 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)); } // 調用 Git 相關API 進行獲取相關配置 public String refresh(String label) { Git git = null; try { git = createGitClient(); if (shouldPull(git)) { FetchResult fetchStatus = fetch(git, label); if (this.deleteUntrackedBranches && fetchStatus != null) { deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git); } // checkout after fetch so we can get any new branches, tags, ect. checkout(git, label); tryMerge(git, label); } else { // nothing to update so just checkout and merge. // Merge because remote branch could have been updated before checkout(git, label); tryMerge(git, label); } // always return what is currently HEAD as the version return git.getRepository().findRef("HEAD").getObjectId().getName(); } // ........ }
總體來說,spring-cloud-config 的相關代碼流程並不復雜,主要是我們需要熟悉 @Value 的原理 、Environment 的原理。這樣看起來就很輕松了。
實現自定義配置中心 PropertySourceLocator (基於拓展org.springframework.cloud.bootstrap.BootstrapConfiguration)
看過了 Spring-Cloud-Config 相關源碼,我們怎么基於同樣的機制來實現我們的配置中心呢? 只需要以下幾個步驟,以JSON 為例:
- 編寫類JsonPropertySourceLocator實現 org.springframework.cloud.bootstrap.config.PropertySourceLocator 接口,實現 locate 方法
- 編寫類繼承 org.springframework.core.env.EnumerablePropertySource
- 新建 resource/META-INF/spring.factories 文件拓展 org.springframework.cloud.bootstrap.BootstrapConfiguration
- 新建配置源文件 json
1.編寫類JsonPropertySourceLocator實現 org.springframework.cloud.bootstrap.config.PropertySourceLocator 接口,實現 locate 方法
@Component public class JsonPropertySourceLocator implements PropertySourceLocator{ private final static String DEFAULT_LOCATION="classpath:gupao.json"; private final ResourceLoader resourceLoader=new DefaultResourceLoader(getClass().getClassLoader()); @Override public PropertySource<?> locate(Environment environment) { WuzzDefineJsonPropertySource jsonPropertySource= new WuzzDefineJsonPropertySource("jsonPropertyConfig",mapPropertySource()); return jsonPropertySource; } private Map<String,Object> mapPropertySource(){ //訪問遠程配置?http接口。 Resource resource=this.resourceLoader.getResource(DEFAULT_LOCATION); if(resource==null){ return null; } Map<String,Object> result=new HashMap<>(); JsonParser jsonParser= JsonParserFactory.getJsonParser(); Map<String,Object> fileMap=jsonParser.parseMap(readFile(resource)); processorMap("",result,fileMap); return result; } private void processorMap(String prefix,Map<String,Object> result,Map<String,Object> fileMap){ if(prefix.length()>0){ prefix+="."; } for (Map.Entry<String,Object> entrySet:fileMap.entrySet()){ if(entrySet.getValue() instanceof Map){ processorMap(prefix+entrySet.getKey(),result,(Map<String,Object>)entrySet.getValue()); }else{ result.put(prefix+entrySet.getKey(),entrySet.getValue()); } } } /** * JSON格式 * @param resource * @return */ private String readFile(Resource resource){ FileInputStream fileInputStream=null; try{ fileInputStream=new FileInputStream(resource.getFile()); byte[] readByte=new byte[(int)resource.getFile().length()]; //TODO 錯誤演示 fileInputStream.read(readByte); return new String(readByte,"UTF-8"); }catch (Exception e){ e.printStackTrace(); }finally { if(fileInputStream!=null){ try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } }
2.編寫類繼承 org.springframework.core.env.EnumerablePropertySource
public class WuzzDefineJsonPropertySource extends EnumerablePropertySource<Map<String, Object>> { public WuzzDefineJsonPropertySource(String name, Map<String, Object> source) { super(name, source); } @Override public String[] getPropertyNames() { return StringUtils.toStringArray(this.source.keySet()); } @Override public Object getProperty(String name) { return this.source.get(name); } }
3.新建 resource/META-INF/spring.factories 文件拓展 org.springframework.cloud.bootstrap.BootstrapConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.wuzz.demo.sourceloader.JsonPropertySourceLocator
4.新建配置源文件 json
{ "custom":{ "property":{ "hello": "hello Wuzz" } } }
5.測試類
@Value("${custom.property.hello}") private String config; @GetMapping("/config") public String config(){ return config; }
啟動的時候可以斷點進去看看是否被加載:
訪問配置好的路徑 :
更多的信息請參考官網。