[享学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。它提供了以下功能:
- 动态、类型的属性
- 高吞吐量和线程安全的配置操作
- 允许获取
Configuration Source
配置源的属性更改的轮询框架 - 配置改变时的回调机制Callback
- 一个
JMX MBean
,可以通过JConsole
访问它来检查和调用属性上的操作(查询和修改等) - 组合配置(复合配置,和Spring的属性源很像)
- 动态配置
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; }
它有两个实现类:JDBCConfigurationSource
和URLConfigurationSource
。前者基于数据库使用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); } }
PollResult
是WatchedUpdateResult
的子类,里面维护着这几个属性: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
自带的热加载机制貌似更加优秀些~)