目錄
自定義PropertySourceLocator實現任意配置加載
PropertySourceLocator的BeanDefinition註冊
前言
前面一篇文章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);
}
}