Apollo配置中心源码分析

1. apollo的核心代码分享

  • SpringApplication启动的关键步骤

  • 在SpringApplication中,会加载所有实现了Init方法的类

protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}
  • 通过上述步骤,Apollo自己实现的ApplicationContextInitializer也就 被加载到容器中了。具体的加载流程如下:

    1.initialize->
    2.initializeSystemProperty(environment) 读取项目中Apollo相关的配置文件,在首次读取的时候都是为空的,配置文件还没有加载进来;如果读到了相关配置,就会将配置信息放到容器的环境变量中。
    3.
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
//循环遍历项目配置的namespace,
for (String namespace : namespaceList) {
//1.调用trySync(),来同步apollo的和本地缓存的配置信息
//2.将这些配置信息转换为应用的全局property
Config config = ConfigService.getConfig(namespace); composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
} environment.getPropertySources().addFirst(composite);
}

2.Apollo启动一览

2.1 ApolloApplicationContextInitializer的作用

定义apollo的容器启动的时候具体的工作事项

ApolloApplicationContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>

容器启动的时候调用init方法


@Override
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment(); ------
//关键步骤
for (String namespace : namespaceList) {
//关键步骤: Config config = ConfigService.getConfig(namespace);
/*
1.调用ConfigService.getService
public static Config getConfig(String namespace) {
return s_instance.getManager().getConfig(namespace);
}
2.DefaultConfigManager.getConfig
if (config == null) {
ConfigFactory factory = m_factoryManager.getFactory(namespace);
config = factory.create(namespace);
m_configs.put(namespace, config);
}
3.DefaultConfigFactory.create(String namespace)
DefaultConfig defaultConfig =
new DefaultConfig(namespace, createLocalConfigRepository(namespace));
4.createLocalConfigRepository-->new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
5.调用 LocalFileConfigRepository的构造方法 --> RemoteConfigRepository
6.调用RemoteConfigRepository构造方法
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;
m_configCache = new AtomicReference<>();
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
m_longPollServiceDto = new AtomicReference<>();
m_remoteMessages = new AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());
m_configNeedForceRefresh = new AtomicBoolean(true);
m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),
m_configUtil.getOnErrorRetryInterval() * 8);
gson = new Gson();
this.trySync();
this.schedulePeriodicRefresh();
this.scheduleLongPollingRefresh();
} */
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
} environment.getPropertySources().addFirst(composite);
}

终上,在容器启动的时候,会调用RemoteConfigRepository的构造方法,而实现配置中心的同步主要是调用trySync,schedulePeriodicRefresh,scheduleLongPollingRefresh这个三个方法来实现配置的实时同步

2.2trySync()
protected boolean trySync() {
try {
sync();
return true;
} catch (Throwable ex) {
Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
logger
.warn("Sync config failed, will retry. Repository {}, reason: {}", this.getClass(), ExceptionUtil
.getDetailMessage(ex));
}
return false;
}
//实际调用
@Override
protected synchronized void sync() {
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig"); try {
//从缓存中获取,如果有的话,启动的时候previos唯恐
ApolloConfig previous = m_configCache.get();
//获取当前的配置文件
ApolloConfig current = loadApolloConfig();
//比较两者是否有差异,
if (previous != current) {
logger.debug("Remote Config refreshed!");
//如果缓存的配置信息与当前查数据库获取到的信息不同,那么就将从数据库中获取到的配置信息放到缓存中。这样在程序启动的时候,configCache就完成了初始化
m_configCache.set(current);
this.fireRepositoryChange(m_namespace, this.getConfig());
}
----
} //如果两者有差异,就触发此操作
protected void fireRepositoryChange(String namespace, Properties newProperties) {
for (RepositoryChangeListener listener : m_listeners) {
try {
//如果两者有差异,那么刷新缓存配置,并且将重写本地的缓存文件
listener.onRepositoryChange(namespace, newProperties);
} catch (Throwable ex) {
Tracer.logError(ex);
logger.error("Failed to invoke repository change listener {}", listener.getClass(), ex);
}
}
}
2.3 schedulePeriodicRefresh

开启多线程,调用 trySync();

private void schedulePeriodicRefresh() {
logger.debug("Schedule periodic refresh with interval: {} {}",
m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
m_executorService.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace));
logger.debug("refresh config for namespace: {}", m_namespace);
trySync();
Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION);
}
}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
m_configUtil.getRefreshIntervalTimeUnit());
}
2.4 scheduleLongPollingRefresh

private void scheduleLongPollingRefresh() {
remoteConfigLongPollService.submit(m_namespace, this);
}
public boolean submit(String namespace, RemoteConfigRepository remoteConfigRepository) {
boolean added = m_longPollNamespaces.put(namespace, remoteConfigRepository);
m_notifications.putIfAbsent(namespace, INIT_NOTIFICATION_ID);
if (!m_longPollStarted.get()) {
startLongPolling();
}
return added;
}

整个apollo配置中心的逻辑就是这样,简单的说就是无线循环的去获取配置信息,当获取到的配置信息与上次获取到的不同那么就刷新容器缓存的配置项并且更新客户端缓存的配置信息。

3. 注解ApolloConfigChangeListener分析

3.1@ApolloConfigChangeListener实现原理

Apollo配置中心有声明一个后置处理器,所以在程序启动的时候,spring容器会自动加载这个PostProcessor。

类图如下

/**
*
*/
public abstract class ApolloProcessor implements BeanPostProcessor, PriorityOrdered { @Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class clazz = bean.getClass();
for (Field field : findAllField(clazz)) {
processField(bean, beanName, field);
}
for (Method method : findAllMethod(clazz)) {
processMethod(bean, beanName, method);
}
return bean;
}

由ApolloProcessor的具体实现可以看到,在postProcessBeforeInitialization(后置处理器生成之前,会调用子类的processField、processMethod方法)。就是说在ApolloProcessor构造后置处理器之前,会调用ApolloAnnotationProcessor的processMethod

ApolloAnnotationProcessor的具体实现


@Override
protected void processMethod(final Object bean, String beanName, final Method method) {
//判断方法上是否加上ApolloConfigChangeListener注解
ApolloConfigChangeListener annotation = AnnotationUtils
.findAnnotation(method, ApolloConfigChangeListener.class);
if (annotation == null) {
return;
}
Class<?>[] parameterTypes = method.getParameterTypes();
Preconditions.checkArgument(parameterTypes.length == 1,
"Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length,
method);
Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
"Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0],
method);
//将 标有注解ApolloConfigChangeListener的方法设为公有的
ReflectionUtils.makeAccessible(method);
//ApolloConfigChangeListener注解上是否加上指定的namespace,如果没有的话,默认使用的namespace为application
String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys();
Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
ConfigChangeListener configChangeListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
ReflectionUtils.invokeMethod(method, bean, changeEvent);
}
}; for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace); if (interestedKeys == null) {
config.addChangeListener(configChangeListener);
} else {
config.addChangeListener(configChangeListener, interestedKeys);
}
}
}
RemoteConfigLongPollService
doLongPollingRefresh
notify(lastServiceDto, response.getBody()); //通知同步更新
调用sync()比较配置文件是否发生改变,变化就同步更新

在配置文件发生变动的时候,调用顺序就跟第一大节说的顺序一致。

4 实际使用

4.1配置多个环境列表(一个portal管理多个环境的配置)

在启动portal的时候需要添加参数来指定某个环境对应的注册中心是什么。如下:

在启动Portal的时候,当点击的是dev也签,调用的注册中心是dev_meta;

-Dapollo_profile=github,auth
-Dspring.datasource.url=jdbc:mysql://yun1:3306/ApolloPortalDB?characterEncoding=utf8
-Dspring.datasource.username=root
-Dspring.datasource.password=Blue123!
-Ddev_meta=http://localhost:8080
-Dfat_meta=http://yun2:8080
-Dserver.port=8070

****在apollo中,可以支持多个环境列表的,通过阅读源码可以知道;在portal模块启动的时候,Apollo会将PortalDB库中的ServerConfig表中的数据添加到运行变量中去,其中就有环境列表的信息,这里需要手动加上去,并且用逗号隔开,添加的值也只能是它规定的那几个值。代码如下:

  • 获取表中的数据并将它们设置到环境变量中

    public List<Env> portalSupportedEnvs() {
    String[] configurations = getArrayProperty("apollo.portal.envs", new String[]{"FAT", "UAT", "PRO"});
    List<Env> envs = Lists.newLinkedList(); for (String env : configurations) {
    envs.add(Env.fromString(env));
    } return envs;
    }
    public PortalDBPropertySource() {
    super("DBConfig", Maps.newConcurrentMap());
    } //将PortalDB.ServerConfig中的表数据全部放入到运行变量中
    @Override
    protected void refresh() {
    Iterable<ServerConfig> dbConfigs = serverConfigRepository.findAll(); for (ServerConfig config: dbConfigs) {
    String key = config.getKey();
    Object value = config.getValue(); if (this.source.isEmpty()) {
    logger.info("Load config from DB : {} = {}", key, value);
    } else if (!Objects.equals(this.source.get(key), value)) {
    logger.info("Load config from DB : {} = {}. Old value = {}", key,
    value, this.source.get(key));
    } this.source.put(key, value);
    }
    }
4.2 指定运行环境
  • 1.在默认路径 /opt/settings/server.properties中指定代码的运行时环境。在项目启动的时候,会找到classpath路径下面的 apollo-env.properties,由它来指定具体的环境与注册中心的对应关系。这样,就不需要添加-Dapollo.mata这个变量了
MetaDomainConsts
static {
Properties prop = new Properties();
prop = ResourceUtils.readConfigFile("apollo-env.properties", prop);
Properties env = System.getProperties();
domains.put(Env.LOCAL,
env.getProperty("local_meta", prop.getProperty("local.meta", DEFAULT_META_URL)));
domains.put(Env.DEV,
env.getProperty("dev_meta", prop.getProperty("dev.meta", DEFAULT_META_URL)));
domains.put(Env.FAT,
env.getProperty("fat_meta", prop.getProperty("fat.meta", DEFAULT_META_URL)));
domains.put(Env.UAT,
env.getProperty("uat_meta", prop.getProperty("uat.meta", DEFAULT_META_URL)));
domains.put(Env.LPT,
env.getProperty("lpt_meta", prop.getProperty("lpt.meta", DEFAULT_META_URL)));
domains.put(Env.PRO,
env.getProperty("pro_meta", prop.getProperty("pro.meta", DEFAULT_META_URL)));
}

Apollo配置中心源码分析的更多相关文章

  1. spring cloud config配置中心源码分析之注解@EnableConfigServer

    spring cloud config的主函数是ConfigServerApplication,其定义如下: @Configuration @EnableAutoConfiguration @Enab ...

  2. Nacos配置中心源码分析

    1.使用 compile 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config:2.2.3.RELEASE' spring: app ...

  3. 微服务之Nacos配置中心源码解析(二)

    Nacos配置中心源码解析 源码入口 ConfigFactory.createConfigService ConfigService configService = NacosFactory.crea ...

  4. nacos统一配置中心源码解析

    配置文件想必大家都很熟悉,无论什么架构 都离不开配置,虽然spring boot已经大大简化了配置,但如果服务很多 环境也好几个,管理配置起来还是很麻烦,并且每次改完配置都需要重启服务,nacos c ...

  5. SpringCloudAlibaba 微服务组件 Nacos 之配置中心源码深度解析

    大家好,这篇文章跟大家聊下 SpringCloudAlibaba 中的微服务组件 Nacos.Nacos 既能做注册中心,又能做配置中心,这篇文章主要来聊下做配置中心时 client 端的一些设计,主 ...

  6. 并发编程之 SynchronousQueue 核心源码分析

    前言 SynchronousQueue 是一个普通用户不怎么常用的队列,通常在创建无界线程池(Executors.newCachedThreadPool())的时候使用,也就是那个非常危险的线程池 ^ ...

  7. iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects

    Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...

  8. STM32时钟系统的配置寄存器和源码分析

    一.时钟系统 概述 时钟是单片机运行的基础,时钟信号推动单片机内各个部分执行相应的指令,时钟系统就是CPU的脉搏,决定cpu速率. STM32有多个时钟来源的选择,为什么 STM32 要有多个时钟源呢 ...

  9. nacos注册中心源码流程分析

    作为一个注册中心,和eureka类似,核心的功能点: 1.服务注册:nacos客户端携带自身信息向nacos服务端进行注册. 2.服务心跳:客户端定时向服务端发送心跳,告知服务端自己处于可用状态 3. ...

随机推荐

  1. win10 anaconda安装后使用报错“Original error was: DLL load failed: 找不到指定的模块”

    报错:Original error was: DLL load failed: 找不到指定的模块. 环境变量需要添加3个 然后就okay了.

  2. 在win10环境下搭建 solr 开发环境

    在win10环境下搭建 solr 开发环境 2017年05月30日 09:19:32 SegaChen0130 阅读数:1050   在win10环境下搭建 solr 开发环境 安装环境  Windo ...

  3. [转]jenkins2 插件安装

    文章来自:http://www.ciandcd.com 文中的代码来自可以从github下载: https://github.com/ciandcd Jenkins的安装包和插件在7个国家有20多个镜 ...

  4. Data Center手册(1):架构

    如图是数据中心的一个基本架构 最上层是Internet Edge,也叫Edge Router,也叫Border Router,它提供数据中心与Internet的连接. 连接多个网络供应商来提供冗余可靠 ...

  5. Java软件工程师面试题:Java运行时异常与一般异常有什么不一样?

    异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误.java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕 ...

  6. [Swift]LeetCode165. 比较版本号 | Compare Version Numbers

    Compare two version numbers version1 and version2.If version1 > version2 return 1; if version1 &l ...

  7. DMA的基本概念

    DMA允许外围设备和主内存之间直接传输 I/O 数据, DMA 依赖于系统.每一种体系结构DMA传输不同,编程接口也不同. 数据传输可以以两种方式触发:一种软件请求数据,另一种由硬件异步传输. 在第一 ...

  8. Kubernetes 笔记 04 架构是个好东西

    本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. Hi,大家好, ...

  9. HBase之CF持久化系列(续2)

    正如上篇博文所说,在本节我将为大家带来StoreFlusher.finalizeWriter..如果大家没有看过我的上篇博文<HBase之CF持久化系列(续1)>,那我希望大家还是回去看一 ...

  10. 如何在 Linux 上复制文件/文件夹到远程系统?

    从一个服务器复制文件到另一个服务器,或者从本地到远程复制是 Linux 管理员的日常任务之一. 我觉得不会有人不同意,因为无论在哪里这都是你的日常操作之一.有很多办法都能处理这个任务,我们试着加以概括 ...