你应该思考:我们不去做的事,哪些是因为克制?哪些是因为懒惰?
代码下载地址:https://github.com/f641385712/netflix-learning
上篇文章 概要性的介绍了Apache Commons Configuration
,并且了解了它的核心API以及使用。但它带给我们的功能还不仅于此,比如本文要讲到的事件监听机制和热更新。
事件-监听机制:能在“修改”(增删改)属性值的时候发送对应事件,让感兴趣的监听者执行其对应的逻辑。
热更新这个词各位开发者肯定不陌生,在实际生产中经常会遇到有类似的需求场景。ReloadingStrategy
表示重载策略,Commons Configuration
通过它来帮助我们实现热更新、热加载的效果,关键时刻不用再重启应用而掉链子。
作为一个有经验的开发人员,对事件-监听机制(同义词:观察者模式)应该是不陌生的,比如Java源生的就有
java.util.EventObject
java.util.Observer
/ java.util.EventListener
java.util.Observable
除了Java源生的,还有大家更为熟悉的Spring里的事件监听实现:
org.springframework.context.ApplicationEvent
org.springframework.context.ApplicationListener
org.springframework.context.event.ApplicationEventMulticaster
从AbstractConfiguration
派生的所有配置类均允许注册事件侦听器,只要更改配置数据,便会通知事件侦听器,这样使用者就可以根据需要,定制自己关心的事件来实现自我的逻辑。
几乎所有的Configuration
子类均直接/间接的从AbstractConfiguration
派生出来,而对于上文描述的常见的几个子类全部派生自AbstractConfiguration
,因此都具有事件-监听的能力。
Apache Commons Configuration
有它自己的事件-监听相关API:
org.apache.commons.configuration.event.ConfigurationEvent
/ ConfigurationErrorEvent
ConfigurationListener
/ ConfigurationErrorListener
EventSource
作为基类,用于收集、管理监听“自己”的xxxListener
监听器们。
用于报告配置对象更新的事件类,它继承自Java标注事件对象:java.util.EventObject
,代表着一个事件。
public class ConfigurationEvent extends EventObject {
// 事件类型
private int type;
// 属性名
private String propertyName;
// 属性值
private Object propertyValue;
// update之前标志位 默认是false
// 标记此事件是在源配置更新之前还是之后生成的。
private boolean beforeUpdate;
// 请注意:父类还有个最重要的属性:事件对象 它是不能为null的
protected transient Object source;
... // 省略构造器,以及所有get方法
}
该事件Event可以存储如下几部分信息:
错误事件类型,用于报告同时发生的错误的事件类处理配置属性,它继承自ConfigurationEvent
。
public class ConfigurationErrorEvent extends ConfigurationEvent {
// 导致发送此事件的错误原因
private Throwable cause;
... // 省略构造器及getCause()方法
}
配置观察者的简单事件监听接口。
public interface ConfigurationListener {
void configurationChanged(ConfigurationEvent event);
}
对配置触发的更新事件感兴趣的对象必须实现ConfigurationListener
接口。
不解释。
public interface ConfigurationErrorListener {
void configurationError(ConfigurationErrorEvent event);
}
用于生成配置事件的对象的基类。这个类实现了管理一组事件监听器的功能,可以在事件发生时通知(类似于Spring的事件广播器)。
操作可以并发地添加和删除事件监听器用于导致事件的配置,操作是同步的线程安全的(不同于Spring,它只能同步触发监听者逻辑)。
public class EventSource {
// 注册的监听器们
private Collection<ConfigurationListener> listeners;
private Collection<ConfigurationErrorListener> errorListeners;
private final Object lockDetailEventsCount = new Object();
// 详细事件的计数器
private int detailEvents;
// 唯一构造器:完成集合的初始化 变为线程安全的集合CopyOnWriteArrayList
public EventSource() {
initListeners();
}
private void initListeners() {
listeners = new CopyOnWriteArrayList<ConfigurationListener>();
errorListeners = new CopyOnWriteArrayList<ConfigurationErrorListener>();
}
public void addConfigurationListener(ConfigurationListener l) { ... }
public boolean removeConfigurationListener(ConfigurationListener l) { ... }
// 返回的是个视图
public Collection<ConfigurationListener> getConfigurationListeners() { ... }
public void clearConfigurationListeners() {
listeners.clear();
}
... // 省略error的增删改查方法
// 确定是否应生成详细事件。如果启用,一些方法可以生成多个更新事件。
// 注意这个方法记录调用次数,例如如果setDetailEvents(false)}被调用三次
// 您将必须经常调用该方法以启用细节。
public void setDetailEvents(boolean enable) { ... detailEvents++; ... }
private boolean checkDetailEvents(int limit) {
synchronized (lockDetailEventsCount) {
return detailEvents > limit;
}
}
// 创建事件对象,并将其交付给所有已注册的事件。也就是让所有关心它的监听器执行
// 注意:这里执行的正常事件(非错误事件)
protected void fireEvent(int type, String propName, Object propValue, boolean before) {
if (checkDetailEvents(-1)) {
Iterator<ConfigurationListener> it = listeners.iterator();
if (it.hasNext()) { // 这句相当于判空,下面while才是一个个执行
ConfigurationEvent event = createEvent(type, propName, propValue, before);
// 把监听器一个个的执行掉
while (it.hasNext()) {
it.next().configurationChanged(event);
}
}
}
}
protected void fireError(int type, String propName, Object propValue, Throwable ex) { ... }
// 子类可复写构造一个ConfigurationEvent 实例的方法
// 比如根据type、propName来判断等等逻辑
protected ConfigurationEvent createEvent(int type, String propName, Object propValue, boolean before) {
return new ConfigurationEvent(this, type, propName, propValue, before);
}
...
}
EventSource
它提供了对监听器的维护,以及当对应事件到达时会触发注册上来的监听者们。如果你希望被监听,那就继承自此类吧。
AbstractConfiguration
就继承自此类,因此它的派生子类们均可以被监听。同时此类也是EventSource
的唯一继承子类。
它是org.apache.commons.configuration.Configuration
的通用抽象实现,并且继承了EventSource
,从而允许其派生出的所有的子类均可被监听。
public abstract class AbstractConfiguration extends EventSource implements Configuration {
// 它定义了很多事件类型哦,并且还都是public可访问的
public static final int EVENT_ADD_PROPERTY = 1;
public static final int EVENT_CLEAR_PROPERTY = 2;
public static final int EVENT_SET_PROPERTY = 3;
public static final int EVENT_CLEAR = 4;
public static final int EVENT_READ_PROPERTY = 5;
// 可以使用${}引用其它属性值
protected static final String START_TOKEN = "${";
protected static final String END_TOKEN = "}";
// List请用逗号分隔
private static char defaultListDelimiter = ',';
... // 生路基本属性的赋值、get方法
// 下面是对Configuration的改变方法,需要发出事件
// 动作前、后都发出了对应的事件
public void addProperty(String key, Object value) {
fireEvent(EVENT_ADD_PROPERTY, key, value, true);
addPropertyValues(key, value, isDelimiterParsingDisabled() ? DISABLED_DELIMITER : getListDelimiter());
fireEvent(EVENT_ADD_PROPERTY, key, value, false);
}
// 抽象方法:直接添加 -> 不触发事件的发送
protected abstract void addPropertyDirect(String key, Object value);
// 同样的,前后均发送事件
public void setProperty(String key, Object value) {
fireEvent(EVENT_SET_PROPERTY, key, value, true);
setDetailEvents(false);
try {
clearProperty(key);
addProperty(key, value);
} finally {
setDetailEvents(true);
}
fireEvent(EVENT_SET_PROPERTY, key, value, false);
}
... // 其它的类同
}
AbstractConfiguration
主要是对接口Configuration
大部分方法的通用实现,并且在一些修改方法的前后均发送了事件,感兴趣的监听器也就会被触发。
另外你会发现fireError()
方法并没有被触发过,这是因为AbstractConfiguration
实现里并不会产生异常,但子类实现就不一定了,比如:
DatabaseConfiguration:
public Object getProperty(String key) {
...
try {
...
} catch (SQLException e) {
fireError(EVENT_READ_PROPERTY, key, null, e);
}
...
}
JNDIConfiguration:
public Object getProperty(String key) {
...
try {
...
} catch (NamingException e)
fireError(EVENT_READ_PROPERTY, key, null, e);
return null;
}
...
}
事件-监听机制使用起来其实并没有什么难度,它的难度在于使用过多的话理解上会稍微困难点,这点你应该在理解Spring Boot
原理上深有体会。
@Test
public void fun4() throws ConfigurationException {
PropertiesConfiguration configuration = new PropertiesConfiguration("1.properties");
// 注册一个监听器
configuration.addConfigurationListener(event -> {
Object source = event.getSource(); // 事件源
int type = event.getType();
if(!event.isBeforeUpdate()){ // 只关心update后的事件,否则会执行两次哦,请务必注意
System.out.println("事件源:" + source.getClass());
System.out.println("事件type类型:" + type);
// 处理你自己的逻辑
}
});
// 增加一个属性,会同步触发监听器去执行
configuration.addProperty("common.addition","additionOne");
System.out.println(configuration.getString("common.addition"));
}
运行程序,控制台打印:
事件源:class org.apache.commons.configuration.PropertiesConfiguration
事件type类型:1
additionOne
决定是否重新加载配置的策略接口。
public interface ReloadingStrategy {
// 设置configuration被这个策略所管理的
void setConfiguration(FileConfiguration configuration);
// 初始化此策略
void init();
// 评估策略:是否需要重新加载配置
boolean reloadingRequired();
// 通知策略文件已被重新加载(通知)
void reloadingPerformed();
}
该策略接口的实现类如下:
这里面InvariantReloadingStrategy
相当于啥都不做:不做Reloading(所以它是默认策略,占位的意思)。所以下面仅只讲述大名鼎鼎的FileChangedReloadingStrategy
,获取它才是我们编程中唯一可能会使用的实现类。
一种重新加载策略,每次更改基础文件时都将重新加载配置。
注意:此重新加载策略不会主动监视配置文件,而是在访问属性时由其关联的配置触发。然后检查配置文件的最后修改日期,如果更改了,则重新加载。
说明:并不是实时的,有delay延迟,并且是你再次访问的时候再去检查是否有变化
为了避免在连续属性查找时永久访问磁盘,可以指定刷新延迟。这将导致在此延迟期间只检查一次配置文件的最后修改日期。此刷新延迟的默认值为5秒。此策略仅适用于File文件Configuration配置实例。
public class FileChangedReloadingStrategy implements ReloadingStrategy {
private static final String JAR_PROTOCOL = "jar";
// 默认最多5秒钟才会去访问一次磁盘,这就是最大延迟
private static final int DEFAULT_REFRESH_DELAY = 5000;
// 它的实例有形如:PropertiesConfiguration、XMLConfiguration等都是它的子类
protected FileConfiguration configuration;
// 最后一次修改的时间戳
protected long lastModified;
// minimum delay
protected long refreshDelay = DEFAULT_REFRESH_DELAY;
// 标志位:是否正在正在重新加载中
private boolean reloading;
...
@Override
public void setConfiguration(FileConfiguration configuration) {
this.configuration = configuration;
}
// 初始化的时候,就touch一下lastModified时间戳
@Override
public void init() {
updateLastModified();
}
@Override
public void reloadingPerformed() {
updateLastModified();
}
// 给lastModified重新赋值
protected void updateLastModified() {
// 拿到这个配置关联到的文件,如:configuration.getFile() / configuration.getURL()
File file = getFile();
if (file != null) {
lastModified = file.lastModified();
}
reloading = false;
}
@Override
public boolean reloadingRequired() {
// 如果没有正在重新加载中,那就进来处理
if (!reloading) {
long now = System.currentTimeMillis();
// 继续判断:两次间隔超过了refreshDelay,才继续执行
if (now > lastChecked + refreshDelay) {
lastChecked = now;
// file.lastModified() > lastModified 证明文件已经改变过啦
if (hasChanged()) {
if (logger.isDebugEnabled())
logger.debug("File change detected: " + getName());
reloading = true;
}
}
}
return reloading;
}
...
}
本类做的事情其实非常的少,主要还是strategy.reloadingRequired()
这个方法提供一个标志位,在访问的时候是否需要重新加载:
AbstractFileConfiguration:
protected ReloadingStrategy strategy;
public void reload() {
reload(false);
}
public boolean reload(boolean checkReload) {
...
if (strategy.reloadingRequired()) {
// 发送事件
refresh();
strategy.reloadingPerformed();
}
}
// 刷新 --> 重要方法
public void refresh() throws ConfigurationException {
... // 发送before事件,事件类型是:EVENT_RELOAD
try {
clear(); // 先情况,再加载
load(); // 重新从文件里加载最新内容
} finally { ... }
clear();
load();
... // 发送after事件,事件类型是:EVENT_RELOAD
}
这段代码解释到了,重新加载数据的逻辑:strategy.reloadingRequired()
为true时,先clear()
本实例的数据,再从文件内重新load()
进该实例。对外暴露的方法是:reload()
方法,它的调用处均为读方法:
AbstractFileConfiguration:
@Override
public Object getProperty(String key) {
...
reload();
return super.getProperty(key);
}
@Override
public boolean isEmpty() {
reload();
return super.isEmpty();
}
@Override
public boolean containsKey(String key) {
reload();
return super.containsKey(key);
}
@Override
public Iterator<String> getKeys() {
reload();
...
}
生产环境,有些配置是需要热加载的,不用重启服务器进行更新。
下面就通过FileChangedReloadingStrategy
来实现热更新,并且注册一个 ConfigurationListener
监听RELOAD
事件类型来告知被重新加载了(热更新成功)。
@Test
public void fun5() throws ConfigurationException {
PropertiesConfiguration configuration = new PropertiesConfiguration("1.properties");
// 监听到配置文件被重新加载了就输出一条日志喽~
configuration.addConfigurationListener(event -> {
// 只监听到重新加载事件
if (event.getType() == PropertiesConfiguration.EVENT_RELOAD) {
System.out.println("配置文件重载...");
configuration.getKeys().forEachRemaining(k -> {
System.out.println("/t " + k + "-->" + configuration.getString(k));
});
}
});
// 使用文件改变重载策略:让改变文件能热加载
FileChangedReloadingStrategy reloadingStrategy = new FileChangedReloadingStrategy();
reloadingStrategy.setRefreshDelay(3000L); // 设置最小事件间隔,单位是毫秒
configuration.setReloadingStrategy(reloadingStrategy);
// 使用另外一个线程模拟去get
otherThreadGet(configuration);
// hold住main线程,不让程序终止
while (true) {}
}
private void otherThreadGet(PropertiesConfiguration configuration) {
new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
configuration.getString("commmon.name");
}
}).start();
}
运行程序,改变1.properties文件并保存且重新编译,控制打印:
配置文件重载...
/t common.name-->YourBatman
/t common.age-->18
/t common.addr-->China
/t common.count-->4
/t common.out-->1
/t common.fullname-->YourBatman-Bryant
/t java.version-->1.8.123
配置文件重载...
// 注意:common.name这一行是被我动态删除掉了,所以此处木有
/t common.age-->18
/t common.addr-->China
/t common.count-->4
/t common.fullname-->YourBatman-Bryant
/t java.version-->1.8.123
因此案例在本地模拟起来实属不易,有几个坑在此做出说明,相信对你一定有帮助,一定要看哦,否则你自己运行可能出不来效果:
PeriodicReloadingTrigger
通过ScheduledExecutorService
的方式解决此问题关于Apache Commons Configuration
的时间监听机制,以及读者们非常关心,且觉得非常实用的热更新实现就介绍完了,是不是越发觉得这个配置库还是蛮不错的,别犹豫了,用它去管理你的配置文件们吧~
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。