美文网首页配置中心apollo
apollo客户端之Config模型

apollo客户端之Config模型

作者: Mr_1214 | 来源:发表于2018-09-14 16:11 被阅读222次

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. 本地配置资源获取配置

相关文章

网友评论

    本文标题:apollo客户端之Config模型

    本文链接:https://www.haomeiwen.com/subject/aviogftx.html