要相信:你遇到的问题,肯定不止你一个人遇到过。 代码下载地址: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
配置源的属性更改的轮询框架JMX MBean
,可以通过JConsole
访问它来检查和调用属性上的操作(查询和修改等)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
为了更好的理解Netflix Archaius
,有些基础API你不得不知。
配置源的定义接口,通过轮询对配置进行动态更改。
public interface PolledConfigurationSource {
// 轮询配置源以获得最新内容。
// initial:如果是首次轮询,填true
// checkPoint:检查点。如果返回的内容是底层的,确定起始点。如果木有填写null
// PoolResult:返回配置的内容。可以是full的,也可能是增量的
public PollResult poll(boolean initial, Object checkPoint) throws Exception;
}
它有两个实现类:JDBCConfigurationSource
和URLConfigurationSource
。前者基于数据库使用DataSource
去查询实现(每次返回全量结果),后者基于一组url的轮询配置源,本文只关注后者。
基于一组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喽。
此抽象类负责调度配置源的定期轮询并应用将结果轮询到配置。子类应该在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的,还是用第三方框架的,这个是子类来定的事。
这是它的唯一内置子类。它使用的是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
自带的热加载机制貌似更加优秀些~)