spring cloud config扩展实践

徐键
什么是配置中心

配置中心用来集中管理应用不同环境(dev、qa、production)、不同集群的配置,配置修改后能够实时推送到应用端。

配置中心提供的核心功能(不仅限如此):
  • 提供配置管理中心, 支持在线管理配置信息;
  • 集中管理各环境的配置文件,支持版本管理(可以用来回滚);
  • 后台配置修改之后,客户端能快速的生效;
  • 配置集的导出\导入功能;
  • 需保证高性能、高可用;
  • 客户端支持各种语言;
配置中心案例
名称介绍特点
淘宝(Diamond)[http://code.taobao.org/svn/diamond/trun]配置实时生效,只支持java
百度(Disconf)[https://github.com/knightliao/disconf]配置实时生效,只支持java
携程(Apollo)[https://github.com/ctripcorp/apollo]配置实时生效,支持java、.Net
360(QConf)[https://github.com/Qihoo360/QConf]配置实时生效,支持c/c++、shell、php、python、lua、java、go、node 等语言
(Spring Cloud Config)[http://cloud.spring.io/spring-cloud-config/]Spring 配置管理基于git,配置修改后不生效
Spring Cloud Config

相较于其他几种配置中心服务,spring cloud config实现比较简单,可以基于git/svn/vault这些版本控制服务提供配置的版本化管理,使用也很简单,跟spring生态紧密结合。缺点是没有配置管理界面,并且刷新需要手动请求endpoint,没有公共配置功能。

基于以上不足做了相应的扩展

  • 使用git api做了配置管理的web界面
  • 扩展EnvironmentRepository增加公共配置库拉取配置
  • 配置endpoint请求刷新后通过应用ack通知刷新实例数
  • 通过服务发现找出实例列表并可查看实例配置
先来看看spring boot应用的启动以及配置的加载过程

默认启动非常简单

public static void main(String[] args) {  
    SpringApplication.run(Application.class, args); //Application为当前main启动类
}

run这个静态方法会构造SpringApplication实例并初始化一些属性:

  • 把参数sources设置到SpringApplication属性中,这个sources可以是任何类型的参数。本文的例子中这个sources就是Application的class对象 spring boot会通过BeanDefinitionLoader根据source的不同类型加载sources

    • Class类型:使用AnnotatedBeanDefinitionReader读取器加载
    • Resource类型:使用XmlBeanDefinitionReader读取器加载
    • Package类型:使用ClassPathBeanDefinitionScanner扫描器加载
    • CharSequence:识别这个字符串信息。如果是Class类型,用第1种;如果是Resource类型,用第2种;如果是Package类型,用第3种
  • 判断是否是web程序,并设置到webEnvironment这个boolean属性中
  • 找出所有的初始化器,默认有5个,设置到initializers属性中
  • 找出所有的应用程序监听器,默认有9个,设置到listeners属性中
  • 找出运行的主类(main class)

可以通过SpringApplicationBuilder自定义配置进行启动(可以设置main启动类、是否web、设置environment、初始化器、监听器等等):

new SpringApplicationBuilder(ConfigClientApplication.class).main(ConfigClientApplication.class)  
        .web(true).environment(new StandardEnvironment()).initializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println("initialize");
    }
}).listeners(new ApplicationListener<ApplicationEvent>() {
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        System.out.println(applicationEvent);
    }
}).build().run(args);

之后会通过SpringApplication的实例方法run启动应用:

  • 构造一个StopWatch,观察SpringApplication的执行
  • 找出所有的SpringApplicationRunListener并封装到SpringApplicationRunListeners中,用于监听run方法的执行。监听的过程中会封装成事件并广播出去让初始化过程中产生的应用程序监听器进行监听
  • 构造Spring容器(ApplicationContext),并返回
    • 创建Spring容器的判断是否是web环境,是的话构造AnnotationConfigEmbeddedWebApplicationContext,否则构造AnnotationConfigApplicationContext
    • 初始化过程中产生的初始化器在这个时候开始工作
    • Spring容器的刷新(完成bean的解析、各种processor接口的执行、条件注解的解析等等)
  • 从Spring容器中找出ApplicationRunner和CommandLineRunner接口的实现类并排序后依次执行

启动过程中有几个节点:

  • started(run方法执行的时候立马执行;对应事件的类型是ApplicationStartedEvent)
  • environmentPrepared(ApplicationContext创建之前并且环境信息准备好的时候调用;对应事件的类型是ApplicationEnvironmentPreparedEvent)
  • contextPrepared(ApplicationContext创建好并且在source加载之前调用一次;没有具体的对应事件)
  • contextLoaded(ApplicationContext创建并加载之后并在refresh之前调用;对应事件的类型是ApplicationPreparedEvent)
  • finished(run方法结束之前调用;对应事件的类型是ApplicationReadyEvent或ApplicationFailedEvent)

本文主要讲应用的配置加载过程,来看看加载environment的过程:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {  
    ConfigurableEnvironment environment = this.getOrCreateEnvironment();
    this.configureEnvironment(environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared(environment);
    if(this.isWebEnvironment(environment) && !this.webEnvironment) {
        environment = this.convertToStandardEnvironment(environment);
    }

    return environment;
}

首先创建environment对象,根据是否web应用会创建StandardEnvironment(非web)、StandardServletEnvironment(web),environment对象包含MutablePropertySources对象, 里面包含一个类型为PropertySource的list,配置的优先级是通过这个list的位置决定的。 configureEnvironment方法会判断defaultProperties属性是否存在,有的话会设置为应用默认配置(优先级最低);然后检查run方法传入的args是否存在,
如果存在会设置为commandLineArgs作为优先级最高的配置,最后会设置应用的active profiles。

environment准备好之后会发布ApplicationEnvironmentPreparedEvent事件,这个事件发布之后主要来关注两个ApplicationListener
1.BootstrapApplicationListener

BootstrapApplicationListener会根据

String configName = environment.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");  

这个配置默认加载bootstrap配置文件,通过工厂加载机制加载key为org.springframework.cloud.bootstrap.BootstrapConfiguration的配置类作为sources启动一个初始化SpringApplication。 这些sources里面关注这个类

public class PropertySourceBootstrapConfiguration implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered  

这是一个初始化器,通过ApplicationEnvironmentPreparedEvent传过来的主应用SpringApplication的addInitializers方法加入了这个初始化器,这个初始化器通过List propertySourceLocators这个属性,这是一个PropertySourceLocator实现组成的list列表,内部包含ConfigServicePropertySourceLocator, 这个类通过config server的http服务暴露的配置加载了spring cloud config server的配置内容。

2.ConfigFileApplicationListener

这个监听器的优先级比上一个低,这个类也是一个EnvironmentPostProcessor的实现类,EnvironmentPostProcessor是spring boot对environment的一个扩展组件,可以对当前environment做出变更,内部通过PropertySourceLoader加载4个位置的配置文件,默认识别4种目录优先级依次为: file:./config/[当前目录下的config目录]
file:./[当前目录]
classpath:/config/[类加载目录下的config目录]
classpath:/[类加载目录]

应用端的配置加载说道这里,接下来说说config server的扩展。 对于spring cloud config server端,增加了对公共配置的支持,spring cloud config server端使用如下接口获取environment

public interface EnvironmentRepository {  
    Environment findOne(String var1, String var2, String var3);
}

支持三种方式获取配置git、svn、vault,默认git方式,默认最终实现类为SearchPathCompositeEnvironmentRepository,这个类会组合配置的EnvironmentRepository实现,获取配置内容。扩展获取公共配置库功能通过扩展该类,定义了公共配置库配置项config.public.projects,读取该配置的内容,从该配置项的配置仓库拉取配置:

public class CommonSearchPathCompositeEnvironmentRepository extends SearchPathCompositeEnvironmentRepository {  
    private Logger logger = LoggerFactory.getLogger(CommonSearchPathCompositeEnvironmentRepository.class);


    public CommonSearchPathCompositeEnvironmentRepository(List<EnvironmentRepository> environmentRepositories) {
        super(environmentRepositories);
    }

    private static final String PUBLIC_CONFIG_PROJECTS = "config.public.projects";

    @Override
    public Environment findOne(String application, String profile, String label) {
        Environment environment = super.findOne(application,profile,label);
        String[] publicProjects = publicProjects(environment.getPropertySources());
        if (publicProjects != null){
            for(String publicProject : publicProjects){
                try{
                    Environment publicEnvironment = super.findOne(publicProject,profile,label);
                    environment.addAll(publicEnvironment.getPropertySources());
                } catch (Exception e){
                    logger.error("get public config fail,e:{}",e.getMessage());
                }
            }
        }
        return environment;
    }

    private String[] publicProjects(List<PropertySource> propertySources){
        for (PropertySource propertySource:propertySources){
            if (propertySource.getSource().containsKey(PUBLIC_CONFIG_PROJECTS)) {
                return propertySource.getSource().get(PUBLIC_CONFIG_PROJECTS).toString().split(",");
            }
        }
        return null;
    }
}

通过import自定义的配置类exclude原有的配置类替换config server的配置类:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ExtendConfigServerConfiguration.class})
public @interface EnableCustomConfigServer {  
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication(exclude = {ConfigServerAutoConfiguration.class})
@EnableDiscoveryClient
@EnableCustomConfigServer
public @interface SpringCloudConfigApplication {  
}

spring cloud config的分布式配置更新依赖于spring cloud bus,通过请求/bus/refresh endpoint,应用会从spring cloud bus消息总线发送RefreshRemoteApplicationEvent,通过定义destinationService可以定义刷新的目标应用,应用刷新完毕会ack一个AckRemoteApplicationEvent,收集该消息可以知道什么应用的几个实例刷新完毕。
先说到这里吧,配置的web管理界面:

http://spring-config-center.nidianwo.com/#/dashboard/app
wiki地址:

http://wiki.nidianwo.com/pages/viewpage.action?pageId=13249588

欢迎多多使用~