目录
自定义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);
}
}