springboot源碼解讀之使用springcloud加載配置

目錄

前言

springcloud啓動與bootstrap文件加載

springcloud啓動

bootstrap文件加載

配置合併

自定義PropertySourceLocator實現任意配置加載

PropertySourceLocator的BeanDefinition註冊

PropertySourceLocator加載配置


前言

前面一篇文章https://blog.csdn.net/hongxingxiaonan/article/details/105129792講了springboot如何加載本地配置。默認情況下,springboot會加載classpath下的application.properties等文件。可能會遇到有的工程裏配置了bootstrap.properties也是生效的,但並沒有自定義spring boot加載本地配置文件的路徑或名字。實際上bootstrap.properties是由spring cloud加載完成的,利用它還可以完成遠程配置的加載。

springcloud啓動與bootstrap文件加載

springcloud啓動

我們知道springboot利用SPI機制提供了很多擴展點,打開spring-cloud-context的spring.factories文件可以看到一系列擴展的實現。其中,BootstrapApplicationListener實現了ApplicationListener

org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
org.springframework.cloud.context.restart.RestartListener

BootstrapApplicationListener監聽了ApplicationEnvironmentPreparedEvent事件,也就是說在spring剛剛創建好environment的時候springcloud啓動了。

bootstrap文件加載

BootstrapApplicationListener自己新創建了一個ApplicationContext,並設置了spring.config.name爲bootstrap(上一篇文章講過這個擴展點),所以在新創建的context默認會在location下搜索名字bootstrap的文件。當然,我們也可以用spring.cloud.bootstrap.name自定義文件名,用spring.cloud.bootstrap.location自定義文件路徑。

public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
	ConfigurableEnvironment environment = event.getEnvironment();
	//......省略無關代碼.....
	ConfigurableApplicationContext context = null;
	String configName = environment
			.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
	//......省略無關代碼.....
	if (context == null) {
		context = bootstrapServiceContext(environment, event.getSpringApplication(),
				configName);
		event.getSpringApplication()
				.addListeners(new CloseContextOnFailureApplicationListener(context));
	}

	apply(context, event.getSpringApplication(), environment);
}
private ConfigurableApplicationContext bootstrapServiceContext(
		ConfigurableEnvironment environment, final SpringApplication application,
		String configName) {
	StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
	MutablePropertySources bootstrapProperties = bootstrapEnvironment
			.getPropertySources();
	//......省略無關代碼.....
	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);	
	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));
	//......省略無關代碼.....
	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();
	//......省略無關代碼.....
	builder.sources(BootstrapImportSelectorConfiguration.class);
	final ConfigurableApplicationContext context = builder.run();
	
	context.setId("bootstrap");
	addAncestorInitializer(application, context);
	//......省略無關代碼.....
	mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
	return context;
}

配置合併

新的context加載完配置之後,需要把配置合併到原來的context的environment中。代碼在mergeAdditionalPropertySources方法中,過程爲

  • 先創建一個新的ExtendedDefaultPropertySource,用於裝新加載的配置。它有個特性就是隻能添加OriginTrackedMapPropertySource類型(即ConfigFileApplicationListener中加載的文件)
  • 差分出新context多出的配置,添加到ExtendedDefaultPropertySource中
  • 將ExtendedDefaultPropertySource添加或替換到原context的environment中
private void mergeAdditionalPropertySources(MutablePropertySources environment,
		MutablePropertySources bootstrap) {
	PropertySource<?> defaultProperties = environment.get(DEFAULT_PROPERTIES);
	ExtendedDefaultPropertySource result = defaultProperties instanceof ExtendedDefaultPropertySource
			? (ExtendedDefaultPropertySource) defaultProperties
			: new ExtendedDefaultPropertySource(DEFAULT_PROPERTIES,
					defaultProperties);
	for (PropertySource<?> source : bootstrap) {
		if (!environment.contains(source.getName())) {
			result.add(source);
		}
	}
	for (String name : result.getPropertySourceNames()) {
		bootstrap.remove(name);
	}
	addOrReplace(environment, result);
	addOrReplace(bootstrap, result);
}

自定義PropertySourceLocator實現任意配置加載

在spring程序中,創建Bean的時候需要用到相關的配置項,所以配置的加載總是要先於Bean的創建。

springcloud使用PropertySourceLocator加載配置,並通過實現springboot的兩個關鍵擴展點達到在Bean創建之前加載配置。首先註冊一個DeferredImportSelector,它將完成PropertySourceLocator的beanDefinition的註冊。然後註冊一個springApplication的initializer在prepareContext階段調用PropertySourceLocator。

PropertySourceLocator的BeanDefinition註冊

上面springcloud在創建新的context的時候,有一行代碼builder.sources(BootstrapImportSelectorConfiguration.class);

BootstrapImportSelectorConfiguration這個配置類沒有別的作用,就是爲了引入BootstrapImportSelector

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

}

BootstrapImportSelector是一個延遲的beanDefinition加載器DeferredImportSelector,掃描完所有的@Configuration註解之後開始執行。它的selectImports方法實現了beanDefinition查找的邏輯。

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;
}

BootstrapImportSelector通過springboot的SPI工具類SpringFactoriesLoader,查找BootstrapConfiguration的實現類。然後將他們的beanDefinition註冊到bootstrap context中。所以我們自定義的配置加載器就可以擴展這個BootstrapConfiguration,如在工程中創建一個META-INF/spring.factories文件,內容爲

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.example.demo.CustomRemoteSourceLocator

現在,bootstrap context中已經有了PropertySourceLocator,等待後面來使用。

PropertySourceLocator加載配置

文章開頭說到了springcloud的啓動過程。springboot啓動,然後創建environment併發布EnvironmentPrepared事件,BootstrapApplicationListener監聽到消息,創建新的context。在處理EnvironmentPrepared事件的onApplicationEvent方法的最後調用了一個apply方法。

private void apply(ConfigurableApplicationContext context,
      SpringApplication application, ConfigurableEnvironment environment) {
   if (application.getAllSources().contains(BootstrapMarkerConfiguration.class)) {
      return;
   }
   application.addPrimarySources(Arrays.asList(BootstrapMarkerConfiguration.class));
   @SuppressWarnings("rawtypes")
   Set target = new LinkedHashSet<>(application.getInitializers());
   target.addAll(
         getOrderedBeansOfType(context, ApplicationContextInitializer.class));
   application.setInitializers(target);
   addBootstrapDecryptInitializer(application);
}

apply方法從bootstrap context用getBean的方式查詢出ApplicationContextInitializer,並補充到SpringApplication中。這裏面實際上會添加PropertySourceBootstrapConfiguration,它也是通過上面的BootstrapConfiguration擴展註冊到bootstrap context。

spring-cloud-context-2.2.2.RELEASE-sources.jar!/META-INF/spring.factories部分內容:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
		ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
//...
	@Autowired(required = false)
	private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
}

 getBean會使所有的propertySourceLocator都注入到PropertySourceBootstrapConfiguration中。現在它被創建並完成了注入,並且作爲ApplicationContextInitializer被添加到了SpringApplication中。

 SpringApplication在refreshContext之前prepareContext階段執行所有的ApplicationContextInitializer。在PropertySourceBootstrapConfiguration的initialize方法中,調用所有的propertySourceLocator,並將它們返回的PropertySource添加到environment中。 這樣就保證了在bean創建之前配置已經準備好。

public void initialize(ConfigurableApplicationContext applicationContext) {
	List<PropertySource<?>> composite = new ArrayList<>();
	AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
	boolean empty = true;
	ConfigurableEnvironment environment = applicationContext.getEnvironment();
	for (PropertySourceLocator locator : this.propertySourceLocators) {
		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);
		empty = false;
	}
	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);
	}
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章