Spring Boot系列之三:从Apollo看Spring的扩展机制

3,728 阅读8分钟

Apollo架构图

本文主要针对Apollo(架构见上图,不再做详细介绍)结合Spring的实现方式介绍下Spring扩展机制,Apollo实现方式并不复杂,其很好利用了Spring丰富的扩展机制,构建起实时强大的配置中心

  • 怎么把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等诸多事件),如下:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    ...  
	setInitializers((Collection) getSpringFactoriesInstances(
			ApplicationContextInitializer.class));
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	...  
	}

在Spring启动时也就是SpringApplication.run()执行过程中,先初始化Environment(此时主要包括应用启动时的命令行,系统环境变量等组成的Environment),如下

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的事件触发方法:

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加载初始化扩展机制。
下面我们看看这种是怎么扩展的:

@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)。

在上文提到的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");
    }

历史文章: