本文主要針對Apollo(架構見上圖,再也不作詳細介紹)結合Spring的實現方式介紹下Spring擴展機制,Apollo實現方式並不複雜,其很好利用了Spring豐富的擴展機制,構建起實時強大的配置中心java
Apollo的配置說白了就是放置在服務器上的application.yml(.properties),優勢是利用其中心化的特徵作到統一配置,實時拉取,既能夠減小重複配置和犯錯的機會,也能夠針對某些動態變化的屬性作到實時生效,特別是像SecretKey這種有動態變化需求的密鑰。
問題來了,如何把Apollo配置作到像application.yml同樣初始化,並且在某些場景下須要在Bean加載以前就能獲取到配置,由於Bean加載過程當中常常要根據配置變量值來決定是否加載一個Bean,如Conditionalxxx註解算法
咱們先來看看Spring是如何支持擴展的
Spring啓動類爲SpringApplication,其實例屬性包含initializers和listeners(均經過spring.factories機制引入),initializers爲ApplicationContextInitializer(即ApplicationContext初始化器),listeners爲ApplicationListener(即Application啓動運行過程當中各類事件的監聽器,如SpringApplicationEvent、ApplicationContextEvent、Environment相關Event等諸多事件),以下:spring
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
...
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
...
}
複製代碼
在Spring啓動時也就是SpringApplication.run()執行過程當中,先初始化Environment(此時主要包括應用啓動時的命令行,系統環境變量等組成的Environment),以下bootstrap
sources.addFirst(new SimpleCommandLinePropertySource(args));
...
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
複製代碼
這是初始Environment,其餘各式各樣的Environment就是經過前文描述的ApplicationListener注入的,這裏不究listener的細節,重點看下其中的ConfigFileApplicationListener,它監聽了ApplicationEnvironmentPreparedEvent,事件觸發後調用註冊在其中的EnvironmentPostProcessor來擴展Environment(若是咱們要提供第三方jar,jar裏面的Environment須要對外暴露,能夠經過擴展EnvironmentPostProcessor的方式);還有Spring Cloud的BootstrapApplicationListener,它經過listener機制擴展了更多的內容,下圖爲ConfigFileApplicationListener的事件觸發方法: bash
經過上圖能夠看到ConfigFileApplicationListener既是ApplicationListener仍是EnvironmentPostProcessor,一專多能,咱們系統中經常使用的application.yml就是經過ConfigFileApplicationListener注入的。在Environment(Environment實際上是由一個個順序的PropertySource組成)準備好以後,Spring開始建立ApplicationContext(ApplicationContext顧名思義就是當前應用的上下文,默認是AnnotationConfigServletWebServerApplicationContext,該類主要組合了DefaultListableBeanFactory,DefaultListableBeanFactory主要用來註冊all bean definitions),而後在prepareContext()中執行以前註冊的initializers: 服務器
這些initializers的執行時機在Bean加載以前,如圖標註,Apollo註冊了ApolloApplicationContextInitializer,看下這個initializer作了什麼操做:ConfigurableEnvironment environment = context.getEnvironment();
String enabled = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "false");
if (!Boolean.valueOf(enabled)) {
return;
}
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
}
String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
for (String namespace : namespaceList) {
Config config = `ConfigService.getConfig(namespace);`
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
}
`environment.getPropertySources().addFirst(composite);`
複製代碼
如上面代碼所示,initializer工做前提是apollo.bootstrap.enabled爲true,具體操做爲向Environment增長CompositePropertySource(該PropertySource提供的屬性經過ConfigService.getConfig同步拉取),且放置到PropertySource列表第一位(Environment獲取屬性時會依次調用PropertySource獲取,取到即止),因此此時Apollo的配置已所有拉取到本地文件和應用進程中(前提是網絡沒問題),Spring後續的Bean加載初始化過程當中Apollo配置開始生效(若是不是本地模式,Apollo會默認經過RemoteConfigRepository定時拉取配置中心配置,bingo!) 網絡
問題又來了,若是apollo.bootstrap.enabled爲false
,Apollo是怎麼玩的呢?
Apollo提供了註解@EnableApolloConfig支持配置獲取(注意,這種方式下配置的生效時機就再也不是Bean加載以前了,Bean建立的Condition條件中若是含有配置屬性,是沒法獲取到的),該種方式主要採用了Spring的Bean加載初始化擴展機制。
下面咱們看看這種是怎麼擴展的:session
@Configuration
@EnableApolloConfig
public class AppConfig {
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
複製代碼
@EnableApolloConfig要和@Configuration一塊兒配置,不然沒法生效,這主要是借用了Spring的ConfigurationClass加載初始化機制, 此處Spring是怎麼玩的呢?詳見下文分解。架構
先說最關鍵的兩個類,以前的Spring Boot系列也屢次提到過BeanFactoryPostProcessor(allows for custom modification of an application context's bean definitions,A BeanFactoryPostProcessor may interact with and modify bean definitions, but never bean instances)和BeanPostProcessor(Factory hook that allows for custom modification of new bean instances,e.g. checking for marker interfaces or wrapping them with proxies)。app
在上文提到的AnnotationConfigServletWebServerApplicationContext(即Spring Boot默認的ApplicationContext)裏面有個AnnotatedBeanDefinitionReader屬性, AnnotatedBeanDefinitionReader會向BeanFactory註冊各類BeanFactoryPostProcessor和BeanPostProcessor,其中的ConfigurationClassPostProcessor主要負責ConfigurationClass的解析(used for bootstrapping processing of{@link Configuration @Configuration} classes),此處爲Bean加載的入口,在後續的處理過程當中會陸續加入各類BeanFactoryProcessor和BeanPostProcessor用於擴展(若有須要,咱們都可以自定義),以下:
if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(`ConfigurationClassPostProcessor.class`);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}
複製代碼
ConfigurationClassPostProcessor作了以下工做:
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass);
// Process any @PropertySource annotations
...
// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);
// Process any @ImportResource annotations
...
// Process individual @Bean methods
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
// Process default methods on interfaces
processInterfaces(configClass, sourceClass);
// Process superclass, if any
...
複製代碼
Apollo是怎麼就此擴展機制處理的呢?
能夠看到在ApolloConfigRegistrar中加入了各類BeanDefinition(主要爲PostProcessor)。再進一步想一個問題,日常開發中,咱們自定義的Bean老是先生效,Spring Boot各類Starter自帶的Bean在加載時常常會先判斷該Bean是否已定義,如DataSource Bean等,這就要求設計時有優先級順序處理,Spring在ConfigurationClass parse時會判斷是否爲DeferredImportSelector和ImportBeanDefinitionRegistrar類型,若是是,則會先放到List中,後面再去處理,可見上面提到的deferredImportSelectors,還有下面代碼中的loadBeanDefinitionsFromRegistrars。
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
...
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
複製代碼
言歸正傳,咱們重點看下Apollo經過ImportBeanDefinitionRegistrar添加的幾個processor:
private void initializePropertySources() {
...
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
//sort by order asc
ImmutableSortedSet<Integer> orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet());
Iterator<Integer> iterator = orders.iterator();
while (iterator.hasNext()) {
int order = iterator.next();
for (String namespace : NAMESPACE_NAMES.get(order)) {
Config config = ConfigService.getConfig(namespace);
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
}
}
// add after the bootstrap property source or to the first
if (environment.getPropertySources()
.contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
environment.getPropertySources()
.addAfter(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, composite);
} else {
environment.getPropertySources().addFirst(composite);
}
}
複製代碼
@Override
public void onChange(ConfigChangeEvent changeEvent) {
...
for (String key : keys) {
// 1. check whether the changed key is relevant
Collection<SpringValue> targetValues = `springValueRegistry`.get(key);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}
// 2. check whether the value is really changed or not (since spring property sources have hierarchies)
if (!shouldTriggerAutoUpdate(changeEvent, key)) {
continue;
}
// 3. update the value
for (SpringValue val : targetValues) {
updateSpringValue(val);
}
}
}
複製代碼
至此,更新配置已經實時更新到了Bean的Field和Method中,還有種場景,Bean(下面代碼中的DataSource)是經過上文方式拉取的配置值建立的,但如何在配置值變化時同時更新這個Bean呢,僅修改屬性值確定是不行的,由於屬性值未必指向同一個配置值對象(如基礎數據類型),該種場景是經過Spring的RefreshScope機制處理的,在Bean上加@RefreshScope註解,而後註冊ConfigChangeListener,在配置變化時調用RefreshScope的refresh方法銷燬Bean,則下一次獲取Bean時會從新建立Bean,該種方式使用時須要特別注意,如非必要,不要這樣處理,由於會有意想不到的坑在等着你。
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
@RefreshScope
public class MetadataDataSourceConfig {
private String url;
private String username;
...
@Bean(name = "masterDataSource")
@RefreshScope
public DataSource masterDataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(this.url);
...
}
@ApolloConfigChangeListener
public void onChange(ConfigChangeEvent changeEvent) {
refreshScope.refresh("masterDataSource");
}
複製代碼
歷史文章: