Spring Boot系列之三:從Apollo看Spring的擴展機制

Apollo架構圖

本文主要針對Apollo(架構見上圖,再也不作詳細介紹)結合Spring的實現方式介紹下Spring擴展機制,Apollo實現方式並不複雜,其很好利用了Spring豐富的擴展機制,構建起實時強大的配置中心java

  • 怎麼把Apollo配置插進去

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-1
ConfigFileApplicationListener-2
經過上圖能夠看到ConfigFileApplicationListener既是ApplicationListener仍是EnvironmentPostProcessor,一專多能,咱們系統中經常使用的application.yml就是經過ConfigFileApplicationListener注入的。

在Environment(Environment實際上是由一個個順序的PropertySource組成)準備好以後,Spring開始建立ApplicationContext(ApplicationContext顧名思義就是當前應用的上下文,默認是AnnotationConfigServletWebServerApplicationContext,該類主要組合了DefaultListableBeanFactory,DefaultListableBeanFactory主要用來註冊all bean definitions),而後在prepareContext()中執行以前註冊的initializers: 服務器

ApplicationContextInitializers-1
ApplicationContextInitializers-2
這些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!) 網絡

RemoteConfigRepository

問題又來了,若是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作了以下工做:

ConfigurationClassPostProcessor
進一步調用到ConfigurationClassParser
ConfigurationClassParser
parser.parse()負責處理當前的ConfigurationClass,注意下圖中的deferredImportSelectors,後面會用到
parser.parse-1
parser.parse-2
進一步調用到doProcessConfigurationClass()
for循環處理ConfigurationClass
回想一下,咱們寫Bean時有多種方式,@Configuration註解在class上,@Bean註解在method上...,若是跟啓動類不在一個目錄,還須要添加@ComponentSan,前面說到了@EnableApolloConfig要和@Configuration一塊兒寫,不然不生效,就是由於@EnableApolloConfig這種自定義註解是爲了引入@Import註解,而@Import能夠經過ImportBeanDefinitionRegistrar機制擴展引入BeanDefinition,@Import就是在@Configuration解析時處理的,具體可見下面代碼的Import部分(Spring的AutoConfiguration機制也是如此):

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是怎麼就此擴展機制處理的呢?

EnableApolloConfig
ApolloConfigRegistrar
能夠看到在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:

  1. PropertySourcesProcessor
    PropertySourcesProcessor
    進一步調用initializePropertySources,起到的做用實際上是和以前的iniitializer相似的。
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);
    }
}
複製代碼
  1. ApolloAnnotationProcessor
    ApolloAnnotationProcessor-1
    ApolloAnnotationProcessor-2
    其中processMethod()處理了@ApolloConfigChangeListener,ApolloConfigChangeListener用於配置中心配置發生變化時觸發屬性值修改&Bean的refresh。
    ApolloAnnotationProcessor-3
    可見ApolloAnnotationProcessor繼承了BeanPostProcessor,BeanPostProcessor是在Bean初始化的時候應用的,對外開放了初始化以前以後的接口,多說一句,beanFactory.getBean()時會生成Bean實例並初始化,除去BeanFactoryPostProcessor和BeanPostProcessor等一些特殊的Bean,Bean的實例初始化是在ApplicationContext.refresh()時調用BeanFactory.getBean()處理的。
    ApplicationContext.refresh
  2. SpringValueProcessor
    SpringValueProcessor一樣繼承了ApolloProcessor,並且實現了BeanFactoryPostProcessor接口,對應有兩個重載方法
    SpringValueProcessor-1
    SpringValueProcessor-2
    下圖中的processField()在上圖的super.postProcessBeforeInitialization()執行時被調用,做用就是解析Bean中帶有@Value的Field,註冊到map中,方便後續拉取最新配置後實時刷新Bean屬性值,還有processMethod()和processBeanPropertyValues()(此方法稍微有點特殊,是爲了處理Bean的TypedStringValue類型屬性,且須要和4一塊兒做用),做用相似。
    SpringValueProcessor-3
    SpringValueProcessor-4
  3. SpringValueDefinitionProcessor
    SpringValueDefinitionProcessor
    SpringValueDefinitionProcessor是個BeanDefinitionRegistryPostProcessor,爲了將Bean和TypedStringValue類型屬性值放到map中,後續由3去實時更新。
  • 最後一個問題,Apollo配置是如何實時更新的呢?
    上文中提到過RemoteConfigRepository,其負責拉取配置中心配置,若配置發生變化,則觸發監聽器RepositoryChangeListener,進而觸發ConfigChangeListener,其中的AutoUpdateConfigChangeListener負責自動更新Bean屬性變化,咱們也能夠自定義ConfigChangeListener,去refresh特定的Bean,下圖爲RemoteConfigRepository的同步配置方法:
    RemoteConfigRepository-1
    RemoteConfigRepository-2
    RemoteConfigRepository-3
    RemoteConfigRepository-4
    其中AutoUpdateConfigChangeListener的onChange邏輯見下面代碼,其中的springValueRegistry和上文提到的SpringValueProcessor中的springValueRegistry是同一個對象。
@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);
      }
    }
}
複製代碼

AutoUpdateConfigChangeListener-1
AutoUpdateConfigChangeListener-2
AutoUpdateConfigChangeListener-3
至此,更新配置已經實時更新到了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");
    }
複製代碼

歷史文章:

相關文章
相關標籤/搜索