­

[享学Netflix] 九、Archaius配置管理库:初体验及基础API详解

  • 2020 年 3 月 18 日
  • 筆記

要相信:你遇到的问题,肯定不止你一个人遇到过。 代码下载地址:https://github.com/f641385712/netflix-learning

目录

前言

Netflix是一家互联网流媒体播放商,是美国视频巨头,随着Netflix转型为一家云计算公司,它也开始积极参与开源项目,并且提供了众多好用的开源产品,比如耳熟能详的就有:

  • Netflix/zuul:网关
  • Netflix/hystrix:断路器
  • Netflix/ribbon:客户端负载均衡器
  • Netflix/archaius:配置管理
  • Netflix/feign:Http客户端
  • Netflix/eureka:配置中心
  • Netflix/conductor:服务编排、任务调度
  • Netflix/hollow:处理内存数据集的Java库

由于Spring Cloud主流技术栈都是构建在Netflix OSS(Open Source)基础上,因此本专栏主要学习些Netflix OSS组件的基础知识,为云源生相关专题做好知识储备。 另需说明:本系列讲述的是源生的Netflix OSS套件,并不是已经集成好的spring-cloud-starter-netflix-archaius

本篇作为Netflix OSS系列第一篇文章,那就先介绍Archaius,因为他是基础。

说明:Netflix Archaius是对Apache Commons Configuration库的扩展,而关于Commons Configuration的介绍前面花了8篇文章详细描述,请移步了解


正文

源码地址:https://github.com/Netflix/archaius 名称由来:这个项目的代号来自一种濒临灭绝的变色龙Archaius。因为变色龙以根据环境和环境改变颜色(一种属性)而闻名。这个和该项目的愿望是契合的:使用动态属性更改来影响基于特定上下文的运行时行为。

总之:Netflix Archaius是一个功能强大的配置管理库。它是一个可用于从许多不同来源收集配置属性的框架,Archaius包含一组由Netflix使用的配置管理api。它提供了以下功能:

  1. 动态、类型的属性
  2. 高吞吐量和线程安全的配置操作
  3. 允许获取Configuration Source配置源的属性更改的轮询框架
  4. 配置改变时的回调机制Callback
  5. 一个JMX MBean,可以通过JConsole访问它来检查和调用属性上的操作(查询和修改等)
  6. 组合配置(复合配置,和Spring的属性源很像)
  7. 动态配置

Archaius允许属性在运行时动态更改,使系统无需重新启动应用程序即可获得这些变化。


Netflix Archaius

<dependency>      <groupId>com.netflix.archaius</groupId>      <artifactId>archaius-core</artifactId>      <version>0.7.7</version>  </dependency>

依赖项:

一旦我们添加了所需的依赖项,我们就能够访问框架管理的属性,并且开箱即用。


快速示例

下面以一个最简单示例,开启Netflix Archaius的使用。

@Test  public void fun1(){      DynamicStringProperty nameProperty = DynamicPropertyFactory.getInstance().getStringProperty("name", "defaultName");      System.out.println(nameProperty.get());  }

默认情况下,它动态管理应用程序类路径中名为config.properties的文件中定义的所有属性。

因此我在classpath下新建一个文件config.properties

name=YourBatman

运行以上程序,控制台输出:

YourBatman

基础API

为了更好的理解Netflix Archaius,有些基础API你不得不知。

PolledConfigurationSource

配置源的定义接口,通过轮询对配置进行动态更改。

public interface PolledConfigurationSource {  	// 轮询配置源以获得最新内容。  	// initial:如果是首次轮询,填true  	// checkPoint:检查点。如果返回的内容是底层的,确定起始点。如果木有填写null  	// PoolResult:返回配置的内容。可以是full的,也可能是增量的      public PollResult poll(boolean initial, Object checkPoint) throws Exception;  }

它有两个实现类:JDBCConfigurationSourceURLConfigurationSource。前者基于数据库使用DataSource去查询实现(每次返回全量结果),后者基于一组url的轮询配置源,本文只关注后者。


URLConfigurationSource

基于一组url的轮询配置源。对于每一次轮询,它总是返回所有文件中定义的属性的完整联合。如果一个属性定在多个文件里了,那么后者覆盖前者。

public class URLConfigurationSource implements PolledConfigurationSource {  	private final URL[] configUrls;    	// 你还可以通过系统属性,外部化定义URLS  	public static final String CONFIG_URL = "archaius.configurationSource.additionalUrls";  	// 默认会在类路径下**轮询此文件名**  	// 此文件名可通过下面的系统属性DEFAULT_CONFIG_FILE_PROPERTY更改  	public static final String DEFAULT_CONFIG_FILE_NAME = "config.properties";  	public static final String DEFAULT_CONFIG_FILE_PROPERTY = "archaius.configurationSource.defaultFileName";      	public static final String DEFAULT_CONFIG_FILE_FROM_CLASSPATH = System.getProperty(DEFAULT_CONFIG_FILE_PROPERTY, DEFAULT_CONFIG_FILE_NAME);    	// 字符串通过new URL转换为URL对象      public URLConfigurationSource(String... urls) {         configUrls = createUrls(urls);      }      public URLConfigurationSource(URL... urls) {          configUrls = urls;      }        // 以上是自己指定URL,那么若你是用空构造器  	// 1、从classpath里去找名为config.properties的文件  	// 2、在找你自己自定义配置的CONFIG_URL  	// 3、注意:相同key后者覆盖前者哦。所以config.properties优先级最低  	public URLConfigurationSource() { ... }    	// 返回结果很简单:把每个文件属性都加载进来  	// 合并后,完整输出PollResult.createFull(map);      @Override      public PollResult poll(boolean initial, Object checkPoint) throws IOException {          Map<String, Object> map = new HashMap<String, Object>();          for (URL url: configUrls) {             ...          }          return PollResult.createFull(map);      }  }

PollResultWatchedUpdateResult的子类,里面维护着这几个属性:protected final Map<String, Object> complete, added, changed, deleted;,清晰的记录着每次的各种变化~

PolledConfigurationSource的主要作用是提供配置,并且提供对轮询的能力,但是什么时候轮询,频次多少,就看poll()方法是如何被调用的,这就引申到下面的Scheduled喽。


AbstractPollingScheduler

此抽象类负责调度配置的定期轮询并应用将结果轮询到配置。子类应该在schedule(Runnable)stop()中提供特定的调度逻辑。

public abstract class AbstractPollingScheduler {    	// 是否忽略配置源里的删除  	// true:忽略。也就是源头删除了,但是内存里不做变更  	// 一般不建议改为true,保持false即可  	private volatile boolean ignoreDeletesFromSource;  	// 轮询时监听器,也就是执行poll时候的监听器  	private List<PollListener> listeners = new CopyOnWriteArrayList<PollListener>();  	...  	//DynamicPropertyUpdater 的作用是:几个Configuration和WatchedUpdateResult  	// 看看哪些需要新增、哪些需要删除、哪些需要update等等  	private DynamicPropertyUpdater propertyUpdater = new DynamicPropertyUpdater();    	// 唯一构造器      public AbstractPollingScheduler() {          this.ignoreDeletesFromSource = false;      }    	// 初始化源,并且将其结果放在Config里面(应用)      protected synchronized void initialLoad(final PolledConfigurationSource source, final Configuration config) {          PollResult result = null;          try {    			// 请注意:这个EventType是Netflix自己的哟,和Apache事件无关  			// 触发所有的PollListener执行              fireEvent(EventType.POLL_BEGIN, null, null);              result = source.poll(true, null); // 执行初始化              checkPoint = result.getCheckPoint();    				// 把result的结果,应用到config里面去  				// 借助propertyUpdater来完成                  populateProperties(result, config);    			// 发送成功事件。数据有result              fireEvent(EventType.POLL_SUCCESS, result, null);          } catch (Exception e) {          	// 发送失败事件,没有数据但是有异常              fireEvent(EventType.POLL_FAILURE, null, e);              throw new RuntimeException("Unable to load Properties source from " + source, e);          }      }    	// 方法执行逻辑完全同上,只不过这里是以Runnable的形式返回  	// 延迟执行  	protected Runnable getPollingRunnable(final PolledConfigurationSource source, final Configuration config) { ... }  	... // 省略属性访问、注册事件等方法    	// 一般只有结果是递增的时候才有用      protected Object getNextCheckPoint(Object lastCheckpoint) {          return lastCheckpoint;      }  }

以上是AbstractPollingScheduler提供的一些“工具方法”,也是最为核心的方法。下面应该看看它的public API:

AbstractPollingScheduler:    	// 开始,启动轮询  	// 请注意:因为要启动schedule,所以这里使用的Runable模式      public void startPolling(final PolledConfigurationSource source, final Configuration config) {          initialLoad(source, config);          Runnable r = getPollingRunnable(source, config);          schedule(r);      }    	// 教你启动了,但停止需子类自行实现  	protected abstract void schedule(Runnable pollingRunnable);  	public abstract void stop();

轮询是通过schedule去实现,至于schedule()如何实现,是用JDK的,还是用第三方框架的,这个是子类来定的事。


FixedDelayPollingScheduler

这是它的唯一内置子类。它使用的是java.util.concurrent.ScheduledExecutorService来实现轮询。

public class FixedDelayPollingScheduler extends AbstractPollingScheduler {      private ScheduledExecutorService executor;      private int initialDelayMillis = 30000; // 初始化后30s启动轮询      private int delayMillis = 60000; // 默认1分钟轮询一次    	// 这两个值均可通过系统属性方式设置,调整  	// 当然你也可以通过构造器显示指定  	// 构造器显示指定的优先级大于系统属性哦~~~~      public static final String INITIAL_DELAY_PROPERTY = "archaius.fixedDelayPollingScheduler.initialDelayMills";      public static final String DELAY_PROPERTY = "archaius.fixedDelayPollingScheduler.delayMills";    	... // 省略构造器为属性赋值    	// 请注意:每调用一次schedule()方法,均生成了一个新的executor实例哟  	// 所以schedule()不要同一实例调用多次啦      @Override      protected synchronized void schedule(Runnable runnable) {          executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {              @Override              public Thread newThread(Runnable r) {                  Thread t = new Thread(r, "pollingConfigurationSource");                  t.setDaemon(true); // 守护线程执行Runnable                  return t;              }          });          // Timer定时器方式,周期性执行任务          executor.scheduleWithFixedDelay(runnable, initialDelayMillis, delayMillis, TimeUnit.MILLISECONDS);      }        @Override      public void stop() {          if (executor != null) {              executor.shutdown();              executor = null;          }      }  }

schedule()方法为同一Runnable任务可以执行多次,但还好该方法不是public的,不会造成乱调用的情况。


使用示例
@Test  public void fun3() throws InterruptedException {      PropertiesConfiguration config = new PropertiesConfiguration();        // 开启轮询,只有文件内容有变化就实时同步      FixedDelayPollingScheduler scheduler = new FixedDelayPollingScheduler(3000, 5000, false);      scheduler.startPolling(new URLConfigurationSource(), config);        while (true) {          ConfigurationUtils.dump(config, System.out);          System.out.println();          TimeUnit.SECONDS.sleep(10);      }  }

运行程序,控制台输出:

name=YourBatman  name=YourBatman-changed  name=YourBatman

加上监听器,打印线程名称,来个增强版:

@Test  public void fun3() throws InterruptedException {      PropertiesConfiguration config = new PropertiesConfiguration();        // 开启轮询,只有文件内容有变化就实时同步      FixedDelayPollingScheduler scheduler = new FixedDelayPollingScheduler(3000, 5000, false);      scheduler.startPolling(new URLConfigurationSource(), config);      scheduler.addPollListener((eventType, result, exception) -> {          if (eventType == PollListener.EventType.POLL_SUCCESS) {              System.out.println("线程名称:" + Thread.currentThread().getName());                System.out.println("新增属性们:" + result.getAdded());              System.out.println("删除属性们:" + result.getDeleted());              System.out.println("修改属性们:" + result.getChanged());              System.out.println("完成属性们:" + result.getComplete());          }      });        while (true) {          ConfigurationUtils.dump(config, System.out);          System.out.println();          TimeUnit.SECONDS.sleep(10);      }  }

控制台情况:

name=YourBatman  线程名称:pollingConfigurationSource  新增属性们:null  删除属性们:null  修改属性们:null  完成属性们:{name=YourBatman}  线程名称:pollingConfigurationSource  新增属性们:null  删除属性们:null  修改属性们:null  完成属性们:{name=YourBatman}    // 从此处发生了改变  线程名称:pollingConfigurationSource  新增属性们:null  删除属性们:null  修改属性们:null  完成属性们:{name=YourBatman-changed, age=18}  name=YourBatman-changed  age=18  ...

由此可见:每次轮询都会执行源的poll方法,所以都会发送相应事件哦。 另外,关于protected final Map<String, Object> complete, added, changed, deleted;这些属性,是需要你自己通过构造WatchedUpdateResult时区分出来的,如果你仅仅是PollResult.createFull(Map<String, Object> complete),那它仅仅只有设置complete值哦。

说明:PollResult#createIncremental()是区分出所有(哪些新增、哪些删除),不过实现起来稍微麻烦点,需要你自行实现~


总结

关于Netflix Archaius的初体验以及基础API的解释就到这了,了解了内置支持的配置源以及轮询机制,并且辅以案例进行了讲解,应该知道Netflix Archaius的动态配置是如何实现的了。

对于Netflix Archaius实现配置动态化和Apache Commons Configuration实现热加载,前者很明显简单很多,并且设计上更加的产品化一点,推荐使用。

说明:Netflix Archaius的动态配置属性并不依赖于Apache Commons Configuration的热加载机制,而是自己实现的轮询策略。(当然喽,这很可能和它依赖的是Commons Configuration1.x有关,若是2.x使用Commons Configuration自带的热加载机制貌似更加优秀些~)