apollo客户端之Config模型

apollo提供了基础数据库存储的配置管理中心,服务分client、adminService、configService、portal等模块。其中client服务在configService获取配置数据,监听配置数据,提供给具体应用的配置初始化以及配置动态更新操作

配置信息模型接口
config.png
Config
/**
 * 配置信息模型接口
 * @author Jason Song(song_s@ctrip.com)
 */
public interface Config {
  /**
   * 获取String类型配置根据配置KEY
   */
  public String getProperty(String key, String defaultValue);

  /**
   * 获取int类型配置根据配置KEY
   */
  public Integer getIntProperty(String key, Integer defaultValue);

  /**
   * 获取long类型配置根据配置KEY
   */
  public Long getLongProperty(String key, Long defaultValue);

  /**
   * 获取short类型配置根据配置KEY
   */
  public Short getShortProperty(String key, Short defaultValue);

  /**
   * 获取float类型配置根据配置KEY
   */
  public Float getFloatProperty(String key, Float defaultValue);

  /**
   * 获取double类型配置根据配置KEY
   */
  public Double getDoubleProperty(String key, Double defaultValue);

  /**
   * 获取byte类型配置根据配置KEY
   */
  public Byte getByteProperty(String key, Byte defaultValue);

  /**
   * Return the boolean property value with the given key, or {@code defaultValue} if the key
   * doesn't exist.
   */
  public Boolean getBooleanProperty(String key, Boolean defaultValue);

  /**
   * Return the array property value with the given
   */
  public String[] getArrayProperty(String key, String delimiter, String[] defaultValue);

  /**
   * Return the Date property value with the given name, or {@code defaultValue} if the name doesn't exist.
   */
  public Date getDateProperty(String key, Date defaultValue);

  /**
   * Return the Date property value with the given name, or {@code defaultValue} if the name doesn't exist.
   */
  public Date getDateProperty(String key, String format, Date defaultValue);

  /**
   * Return the Date property value with the given name, or {@code defaultValue} if the name doesn't exist.
   */
  public Date getDateProperty(String key, String format, Locale locale, Date defaultValue);

  /**
   * Return the Enum property value with the given key, or {@code defaultValue} if the key doesn't exist.
   */
  public <T extends Enum<T>> T getEnumProperty(String key, Class<T> enumType, T defaultValue);

  /**
   * Return the duration property value(in milliseconds) with the given name, or {@code
   * defaultValue} if the name doesn't exist. Please note the format should comply with the follow
   * example (case insensitive). Examples:
   * <pre>
   *    "123MS"          -- parses as "123 milliseconds"
   *    "20S"            -- parses as "20 seconds"
   *    "15M"            -- parses as "15 minutes" (where a minute is 60 seconds)
   *    "10H"            -- parses as "10 hours" (where an hour is 3600 seconds)
   *    "2D"             -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
   *    "2D3H4M5S123MS"  -- parses as "2 days, 3 hours, 4 minutes, 5 seconds and 123 milliseconds"
   * </pre>
   *
   * @param key          the property name
   * @param defaultValue the default value when name is not found or any error occurred
   * @return the parsed property value(in milliseconds)
   */
  public long getDurationProperty(String key, long defaultValue);

  /**
   * 添加配置改变监听器
   * @param listener the config change listener
   */
  public void addChangeListener(ConfigChangeListener listener);

  /**
   * 添加配置改变的监听器
   */
  public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys);

  /**
   * 获取所有配置KEY
   */
  public Set<String> getPropertyNames();
}

Config接口,配置信息超类,提供给应用获取配置信息的接口
功能如下:

  1. 提供基础类型值的配置获取
  2. 提供注册配置改变的监听器,用于配置改变回调
AbstractConfig
public abstract class AbstractConfig implements Config {
/**
   * 监听器集合
   */
  private final List<ConfigChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
  private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = Maps.newConcurrentMap();
  private final ConfigUtil m_configUtil;
  /**
   * 配置信息缓存
   */
  private volatile Cache<String, Integer> m_integerCache;
  private volatile Cache<String, Long> m_longCache;
  private volatile Cache<String, Short> m_shortCache;
  private volatile Cache<String, Float> m_floatCache;
  private volatile Cache<String, Double> m_doubleCache;
  private volatile Cache<String, Byte> m_byteCache;
  private volatile Cache<String, Boolean> m_booleanCache;
  private volatile Cache<String, Date> m_dateCache;
  private volatile Cache<String, Long> m_durationCache;
  private final Map<String, Cache<String, String[]>> m_arrayCache;
  private final List<Cache> allCaches;

  
 /**
   * 获取配置并缓存到本地缓存
   * @param key
   * @param parser
   * @param cache
   * @param defaultValue
   * @param <T>
   * @return
   */
   private <T> T getValueAndStoreToCache(String key, Function<String, T> parser, Cache<String, T> cache, T defaultValue) {
    long currentConfigVersion = m_configVersion.get();
    String value = getProperty(key, null);

    if (value != null) {
      T result = parser.apply(value);

      if (result != null) {
        synchronized (this) {
          if (m_configVersion.get() == currentConfigVersion) {
            cache.put(key, result);
          }
        }
        return result;
      }
    }

    return defaultValue;
  }
  
    /**
   * 配置改变调用监听器触发回调方法
   * @param changeEvent
   */
  protected void fireConfigChange(final ConfigChangeEvent changeEvent) {
    for (final ConfigChangeListener listener : m_listeners) {
      // check whether the listener is interested in this change event
      if (!isConfigChangeListenerInterested(listener, changeEvent)) {
        continue;
      }
      m_executorService.submit(new Runnable() {
        @Override
        public void run() {
          String listenerName = listener.getClass().getName();
          Transaction transaction = Tracer.newTransaction("Apollo.ConfigChangeListener", listenerName);
          try {
            listener.onChange(changeEvent);
            transaction.setStatus(Transaction.SUCCESS);
          } catch (Throwable ex) {
            transaction.setStatus(ex);
            Tracer.logError(ex);
            logger.error("Failed to invoke config change listener {}", listenerName, ex);
          } finally {
            transaction.complete();
          }
        }
      });
    }
  }
  
   /**
   * 对比最新配置与本地缓存差异构建改变事件
   * @param namespace
   * @param previous
   * @param current
   * @return
   */
  List<ConfigChange> calcPropertyChanges(String namespace, Properties previous,
                                         Properties current) {
    if (previous == null) {
      previous = new Properties();
    }

    if (current == null) {
      current = new Properties();
    }

    Set<String> previousKeys = previous.stringPropertyNames();
    Set<String> currentKeys = current.stringPropertyNames();

    Set<String> commonKeys = Sets.intersection(previousKeys, currentKeys);
    Set<String> newKeys = Sets.difference(currentKeys, commonKeys);
    Set<String> removedKeys = Sets.difference(previousKeys, commonKeys);

    List<ConfigChange> changes = Lists.newArrayList();

    for (String newKey : newKeys) {
      changes.add(new ConfigChange(namespace, newKey, null, current.getProperty(newKey),
          PropertyChangeType.ADDED));
    }

    for (String removedKey : removedKeys) {
      changes.add(new ConfigChange(namespace, removedKey, previous.getProperty(removedKey), null,
          PropertyChangeType.DELETED));
    }

    for (String commonKey : commonKeys) {
      String previousValue = previous.getProperty(commonKey);
      String currentValue = current.getProperty(commonKey);
      if (Objects.equal(previousValue, currentValue)) {
        continue;
      }
      changes.add(new ConfigChange(namespace, commonKey, previousValue, currentValue,
          PropertyChangeType.MODIFIED));
    }

    return changes;
  }
 } 

AbstractConfig抽象实现类,完成了配置的通用功能
功能如下:

  1. 提供配置本地缓存功能
  2. calcPropertyChanges:对比本地缓存与最新配置构建配置改变事件
  3. 提供监听器注册以及监听器回调触发功能fireConfigChange
DefaultConfig
/**
 * 配置信息默认实现
 * @author Jason Song(song_s@ctrip.com)
 */
public class DefaultConfig extends AbstractConfig implements RepositoryChangeListener {
  private static final Logger logger = LoggerFactory.getLogger(DefaultConfig.class);
  /**
   * 命名空间
   */
  private final String m_namespace;
  /**
   * 本地文件缓存配置
   */
  private Properties m_resourceProperties;
  /**
   * 配置中心配置本地缓存
   */
  private AtomicReference<Properties> m_configProperties;
  /**
   * 配置仓库服务,用于获取远程配置中心配置
   */
  private ConfigRepository m_configRepository;
  /**
   * 流控信息
   */
  private RateLimiter m_warnLogRateLimiter;

  /**
   * 初始化,通过配置仓库加载配置中心配置信息
   */
  private void initialize() {
    try {
      m_configProperties.set(m_configRepository.getConfig());
    } catch (Throwable ex) {
      Tracer.logError(ex);
      logger.warn("Init Apollo Local Config failed - namespace: {}, reason: {}.",
          m_namespace, ExceptionUtil.getDetailMessage(ex));
    } finally {
      //注册当前实例到配置仓库服务上,用于监听配置仓库上的配置变化
      //register the change listener no matter config repository is working or not
      //so that whenever config repository is recovered, config could get changed
      m_configRepository.addChangeListener(this);
    }
  }

  /**
   * 获取配置信息
   * @param key          the property name
   * @param defaultValue the default value when key is not found or any error occurred
   * @return
   */
  @Override
  public String getProperty(String key, String defaultValue) {

    //System.getProperty方式获取配置优先级最高
    // step 1: check system properties, i.e. -Dkey=value
    String value = System.getProperty(key);

    //远程仓库方式获取配置优先级次之
    // step 2: check local cached properties file
    if (value == null && m_configProperties.get() != null) {
      value = m_configProperties.get().getProperty(key);
    }

    //System.getenv获取配置优先级第三
    /**
     * step 3: check env variable, i.e. PATH=...
     * normally system environment variables are in UPPERCASE, however there might be exceptions.
     * so the caller should provide the key in the right case
     */
    if (value == null) {
      value = System.getenv(key);
    }

    //本地配置资源获取配置优先级最低
    // step 4: check properties file from classpath
    if (value == null && m_resourceProperties != null) {
      value = (String) m_resourceProperties.get(key);
    }

    if (value == null && m_configProperties.get() == null && m_warnLogRateLimiter.tryAcquire()) {
      logger.warn("Could not load config for namespace {} from Apollo, please check whether the configs are released in Apollo! Return default value now!", m_namespace);
    }

    return value == null ? defaultValue : value;
  }

  /**
   * 配置仓库中的配置改变回调函数
   * @param namespace the namespace of this repository change
   * @param newProperties the properties after change
   */
  @Override
  public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
    if (newProperties.equals(m_configProperties.get())) {
      return;
    }
    Properties newConfigProperties = new Properties();
    newConfigProperties.putAll(newProperties);

    Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties);

    //check double checked result
    if (actualChanges.isEmpty()) {
      return;
    }

    //调用配置改变回调方法,通知配置改变监听器
    this.fireConfigChange(new ConfigChangeEvent(m_namespace, actualChanges));

    Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);
  }

  private Map<String, ConfigChange> updateAndCalcConfigChanges(Properties newConfigProperties) {
    List<ConfigChange> configChanges =
        calcPropertyChanges(m_namespace, m_configProperties.get(), newConfigProperties);

    ImmutableMap.Builder<String, ConfigChange> actualChanges =
        new ImmutableMap.Builder<>();

    /** === Double check since DefaultConfig has multiple config sources ==== **/

    //1. use getProperty to update configChanges's old value
    for (ConfigChange change : configChanges) {
      change.setOldValue(this.getProperty(change.getPropertyName(), change.getOldValue()));
    }

    //2. update m_configProperties
    m_configProperties.set(newConfigProperties);
    clearConfigCache();

    //3. use getProperty to update configChange's new value and calc the final changes
    for (ConfigChange change : configChanges) {
      change.setNewValue(this.getProperty(change.getPropertyName(), change.getNewValue()));
      switch (change.getChangeType()) {
        case ADDED:
          if (Objects.equals(change.getOldValue(), change.getNewValue())) {
            break;
          }
          if (change.getOldValue() != null) {
            change.setChangeType(PropertyChangeType.MODIFIED);
          }
          actualChanges.put(change.getPropertyName(), change);
          break;
        case MODIFIED:
          if (!Objects.equals(change.getOldValue(), change.getNewValue())) {
            actualChanges.put(change.getPropertyName(), change);
          }
          break;
        case DELETED:
          if (Objects.equals(change.getOldValue(), change.getNewValue())) {
            break;
          }
          if (change.getNewValue() != null) {
            change.setChangeType(PropertyChangeType.MODIFIED);
          }
          actualChanges.put(change.getPropertyName(), change);
          break;
        default:
          //do nothing
          break;
      }
    }
    return actualChanges.build();
  }

  /**
   * 加载本地资源配置信息根据命名空间;META-INF/config/{namespace}.properties
   * @param namespace
   * @return
   */
  private Properties loadFromResource(String namespace) {
    String name = String.format("META-INF/config/%s.properties", namespace);
    InputStream in = ClassLoaderUtil.getLoader().getResourceAsStream(name);
    Properties properties = null;

    if (in != null) {
      properties = new Properties();

      try {
        properties.load(in);
      } catch (IOException ex) {
        Tracer.logError(ex);
        logger.error("Load resource config for namespace {} failed", namespace, ex);
      } finally {
        try {
          in.close();
        } catch (IOException ex) {
          // ignore
        }
      }
    }

    return properties;
  }
}

DefaultConfig类为客户端提供的默认配置获取类,此类主要完成以下功能

  1. 初始化从配置仓库拉取远程的配置信息
  2. 注册当前实例(配置仓库的监听器)到配置仓库,用于监听配置仓库的改变
  3. 提供配置仓库改变的回调函数onRepositoryChange
  4. 根据KEY查找配置值信息
  5. 加载当前配置命名空间的本地资源(loadFromResource)

其中获取配置有以下几种方式,按优先级由高到低如下:

  1. 通过System.getProperty方式获取配置
  2. 远程仓库方式获取配置
  3. System.getenv获取配置
  4. 本地配置资源获取配置
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,968评论 3 119
  • 春思-李白 燕草如碧丝,秦桑低绿枝。 当君怀归日,是妾断肠时。 春风不相识,何事入罗帏。 春歌 秦地罗敷女,采桑绿...
    走在边缘阅读 220评论 0 1
  • 多态存在的三个必要条件一、要有继承;二、要有重写;三、父类引用指向子类对象。
    zealotabc阅读 750评论 0 0
  • 人生做了很多事,也想做过很多事,但总是半途而废了,许多事不知道为什么没有坚持下去,也许是想法变了,也许是情况变了,...
    天下非空阅读 174评论 0 1
  • 今天看了一个知乎问答,买车后和买车前的差别。高票回答有这些,我觉得还挺有道理的。 第一是生活半径扩大了。第二是跟女...
    xll2068阅读 175评论 0 2