[享学Netflix] 七、Apache Commons Configuration2.x如何实现文件热加载/热更新?
- 2020 年 3 月 18 日
- 筆記
成功的路上并不拥挤,因为坚持的人不多。能坚持的人不一定有成就,但要想有成就那就必须得坚持。 代码下载地址:https://github.com/f641385712/netflix-learning
目录
前言
热加载是一个常见概念:比如Java中的热加载类,更改某一行代码可以在不重启项目的情况下生效,这个一般在开发环境、调试环境使用得比较多,可提高效率。
热加载在配置文件修改场景下一般也有硬性需求:在不能重启项目的情况下,改动某个key的值,希望立马生效。比如某个活动开始与否是由某个值决定的,而线上需要在不停机、不重启情况打开此开关,这就需要文件热加载、热更新作为基础能力提供支撑。
正文
在1.x版本的时候,我演示过热加载、热更新的示例,你可以移步此处查看,它是通过ReloadingStrategy
接口来实现的。
而2.x版本同样的完全推翻了这一套API,改而设计了一套全新的、耦合度更低的API方式,更加灵活的来实现Reloading
重新加载的能力,这边是接下来的主要内容。
说明:重新加载并不是文件专属,任何可以被load()进来的资源都可以被加以
Reloading
的概念。
ReloadingDetector
ReloadingDetector
接口用于检测(Detector
)是否需要重载,这个接口没有定义如何执行对重新加载的检查,也就是接口不决定进行重载的条件,完全取决于具体实现。 它是实现Reloading
决定重新加载与否的最基础支持接口:
public interface ReloadingDetector { // 检查是否满足重新加载操作的所有条件 // true:表示需要重新加载 false表示不需要 boolean isReloadingRequired(); // 通知此对象已执行重新加载操作。这方法在reloadingRequired()返回后调用 void reloadingPerformed(); }
注意:ReloadingDetector
本身并不会主动监听某一资源,只有你手动去调用isReloadingRequired()
方法才能知道是否需要重载。它的继承结构如下图:

虽然实现类有多个,但本文有且仅讨论最常用的FileHandlerReloadingDetector
即可。
FileHandlerReloadingDetector
一个特殊的实现ReloadingDetector
,它监控由FileHandler
关联的文件,这是我们平时最常用的方式。
public class FileHandlerReloadingDetector implements ReloadingDetector { // 你的文件也可以是一个Jar文件 private static final String JAR_PROTOCOL = "jar"; // 默认刷新的间歇时间。避免你一直读取一直IO,因为IO是很耗性能的 private static final int DEFAULT_REFRESH_DELAY = 5000; // 重要。关联的File private final FileHandler fileHandler; // 一般都是DEFAULT_REFRESH_DELAY 这个值 private final long refreshDelay; // 配置文件最后修改的时间戳 private long lastModified; // 最后一次check文件的时间戳 private long lastChecked; // 构造器们:FileHandler是必须的,否则关联不到文件嘛~~ public FileHandlerReloadingDetector(final FileHandler handler, final long refreshDelay) { fileHandler = handler != null ? handler : new FileHandler(); this.refreshDelay = refreshDelay; } ... // 省略其它构造器和get方法 // =============接口方法============= @Override public boolean isReloadingRequired() { final long now = System.currentTimeMillis(); // 这个判断是避免你频繁不断的访问文件,浪费IO 默认会给你延迟5秒 if (now >= lastChecked + getRefreshDelay()) { lastChecked = now; // 文件最后修改时间戳:file.lastModified() // 这个值只有你文件真的被修改过了,才会 >0 final long modified = getLastModificationDate(); if (modified > 0) { // 逻辑描述:首次进来lastModified=0,那就初始化一下它 但是最终是返回false哦 // 所以:初始化动作updateLastModified()一般建议初始化的时候就调用一次 if (lastModified == 0) { // initialization updateLastModified(modified); // 最后一次check文件和文件实际的lastModified不一样,那就证明可以重新加载喽 } else { if (modified != lastModified) { return true; } } } } return false; } // 更新lastModified值为:当前文件实际的lastModified // 初始化的时候本方法会被调用 @Override public void reloadingPerformed() { updateLastModified(getLastModificationDate()); } // 此方法效果同上,它不是接口方法 public void refresh() { updateLastModified(getLastModificationDate()); } ... // 省略File文件操作file.lastModified()等等 }
流程描述:
- 这个类的实例在构造时传递一个
FileHandler
、delay时间(非必须)。 - 每次调用
isReloadingRequired()
方法时,它都会进行检查FileHandler
是否指向有效位置(所以请务必关联上文件) - 然后得到文件的最后修改时间
lastModified
,并与最后存储时间lastChecked
比较,如果发生了更改,则应执行重新加载操作。
另外,因为文件I/O
资源一般比较昂贵,所以可以配置刷新延迟时间(毫秒)。这是两次检查之间的最小间隔。如果短时间内调用isReloadingRequired()
方法,它不执行检查,直接返回false,从而降低IO损耗,提高整体表现。它失去的是数据同步不能完全及时,但是这一般情况下是可以接受的~
reloadingPerformed()
方法用来通知说重新加载确实发生了,此方法可用于重置 内部状态(lastModified的值),以便能够检测到下一次重新加载的条件。
使用示例
@Test public void fun1() throws InterruptedException { // 关联上1.properties这个文件 Map<String, Object> map = new HashMap<>(); map.put("fileName", "1.properties"); // 因fileHandler此例不需要FileBased,所以先用null吧 FileHandler fileHandler = new FileHandler(null, FileHandler.fromMap(map)); // 构建一个detector实例 ReloadingDetector detector = new FileHandlerReloadingDetector(fileHandler); while (true) { if (detector.isReloadingRequired()) { System.out.println("====文件被修改了====程序退出。。。"); break; } else { TimeUnit.SECONDS.sleep(10); System.out.println("文件没有修改。。。"); } } }
启动程序,然后修改文件内容保存,并且重新编译,控制台打印如下:
文件没有修改。。。 文件没有修改。。。 ====文件被修改了====程序退出。。。
这就是直接使用ReloadingDetector
接口的案例,能够告诉你文件是否已经改变过,让你来决定如何处理,一切都是手动的。 实际上,该接口最终是配合着ReloadingController
来使用,当与ReloadingController
一起使用时,实现不必是线程安全的,由控制器ReloadingController
负责同步:一个实例同一时刻只被一个线程访问。
ReloadingController
直译:重载控制器。一个用于以通用方式添加对Reloading
重载操作的支持的类。
public interface ReloadingControllerSupport { ReloadingController getReloadingController(); }
// 它是一个EventSource,所以可以向上注册监听器来监听它。它发送的时间类型是:`ReloadingEvent` public class ReloadingController implements EventSource { // 委托给它来去确定,是否需要重新加载 private final ReloadingDetector detector; // 注册在ReloadingController 身上的监听器们 private final EventListenerList listeners; // 只有是false的时候,才会让你继续去重载 private boolean reloadingState; // 唯一构造器 public ReloadingController(final ReloadingDetector detect) { ... } ... // 省略注册、取消注册监听器的方法 // 同步方法:控制状态值,来确保并发重载的问题 public synchronized boolean isInReloadingState() { return reloadingState; } // 重置:reloadingState置为false,表示你下次还可以重载喽 // reloadingPerformed()表示,重载成功过 public synchronized void resetReloadingState() { if (isInReloadingState()) { // 如果重载过,reset才生效 getDetector().reloadingPerformed(); reloadingState = false; } } // 最重要方法:也是个同步方法 public boolean checkForReloading(final Object data) { boolean sendEvent = false; synchronized (this) { if (isInReloadingState()) { return true; } // 委托给`ReloadingDetector`去判断,是否能够执行重载 if (getDetector().isReloadingRequired()) { sendEvent = true; // 只有需要重载,才去发送重载事件 reloadingState = true; // 值设置为true,只有reset()后才能再次重载 } } // 发送ReloadingEvent事件,数据是就是外部传进来的data if (sendEvent) { listeners.fire(new ReloadingEvent(this, data)); return true; } return false; } }
此控制器代码逻辑简单,某个资源是否需要执行重载,是需要主动调用checkForReloading()
方法来判断,而这个动作是委托给ReloadingDetector
去完成的。 checkForReloading()
方法的返回值解释:
- true:重载事件发送成功(也就是说重载逻辑成功执行了)
- false:没有发送重载事件
注意:
ReloadingController
它并不关联具体的文件,因为它只关心ReloadingDetector
接口,而具体监控的啥东西取决于实现本身
该类实现了执行重新加载检查的通用协议(基于外部触发器)并相应地作出反应,通过事件重新加载是松散耦合的。
【判断】一个文件是否需要重新加载这个操作实际上是委托给了ReloadingDetector
这个接口去完成,当这个检测器(detector)发现了变化就将这一消息发送给已经注册好的监听器。
说明:从源码处可以看到,
ReloadingController
它负责处理了同步问题,而相关的处理程序并不给与保证(比如监听器实现)~
使用示例
@Test public void fun2() throws InterruptedException { // 关联上1.properties这个文件 Map<String, Object> map = new HashMap<>(); map.put("fileName", "1.properties"); // 因fileHandler此例不需要FileBased,所以先用null吧 FileHandler fileHandler = new FileHandler(null, FileHandler.fromMap(map)); // 使用控制器ReloadingController 代理掉ReloadingDetector来使用,更好用 ReloadingController reloadingController = new ReloadingController(new FileHandlerReloadingDetector(fileHandler)); reloadingController.addEventListener(ReloadingEvent.ANY, event -> { ReloadingController currController = event.getController(); Object data = event.getData(); currController.resetReloadingState(); // 需要手动充值一下,否则下次文件改变就不会发送此事件啦 System.out.println((reloadingController == currController) + " data:" + data); }); while (true) { if (reloadingController.checkForReloading("自定义数据")) { System.out.println("====文件被修改了====触发重载事件,然后程序退出。。。"); break; } else { TimeUnit.SECONDS.sleep(20); System.out.println("文件没有修改。。。"); } } }
修改文件,保存并且重新编译后,控制台输出:
文件没有修改。。。 true data:自定义数据 ====文件被修改了====触发重载事件,然后程序退出。。。
这里面check过程其实还是手动挡,需要手动去reloadingController.checkForReloading()
来判断,在绝对要do什么。
ReloadingFileBasedConfigurationBuilder
如果你想得到一个具有重载Reloading
能力的Builder
,你可使用它来完成文件的热加载效果。 使用本类在构建的时候,已经自动帮你注册上了ReloadingBuilderSupportListener
这个监听器(详情参见API:ReloadingFileBasedConfigurationBuilder#createReloadingController()
)
PeriodicReloadingTrigger
Periodic:周期的。
从上可知,要想感知到文件的变化触发Reloading
操作,不管你是用ReloadingController
还是ReloadingDetector
都需要你手动去调用方法check
,相对麻烦。 针对此情况,Commons Configuration
提供了基于Timer的方案:PeriodicReloadingTrigger
来帮助你实现“自动监听逻辑”。
public class PeriodicReloadingTrigger { // 定时Scheduled,你可以自己传进来,默认使用的是守护线程,命名为:ReloadingTrigger // 默认使用的是:Executors.newScheduledThreadPool(1, factory); // factory = new BasicThreadFactory.Builder().namingPattern("ReloadingTrigger-%s").daemon(true).build(); private final ScheduledExecutorService executorService; private final ReloadingController controller; private final Object controllerParam; // 定时器多久执行一次 private final long period; private final TimeUnit timeUnit; // 正在执行的task任务,避免重复执行 private ScheduledFuture<?> triggerTask; ... // 省略构造器为各个属性赋值 public synchronized void start() { // triggerTask != null; 也就是没有正在运行的任务的时候,可以start if (!isRunning()) { // commond:调用controller.checkForReloading(controllerParam)方法而已 triggerTask = getExecutorService().scheduleAtFixedRate(createTriggerTaskCommand(), period, period,timeUnit); } } // 停止任务 public synchronized void stop() { if (isRunning()) { triggerTask.cancel(false); triggerTask = null; } } public synchronized boolean isRunning() { return triggerTask != null; } // 比stop狠,因为他是关闭线程池getExecutorService().shutdown(); // 当然shutdownExecutor = true的时候才管,否则效果同stop public void shutdown(final boolean shutdownExecutor) { ... } public void shutdown() { shutdown(true); } }
当我们不需要自动监控了的时候,请调用shutdown()
方法来释放资源。
使用示例
@Test public void fun22() { // 关联上1.properties这个文件 Map<String, Object> map = new HashMap<>(); map.put("fileName", "1.properties"); // 因fileHandler此例不需要FileBased,所以先用null吧 FileHandler fileHandler = new FileHandler(null, FileHandler.fromMap(map)); // 使用控制器ReloadingController 代理掉ReloadingDetector来使用,更好用 ReloadingController reloadingController = new ReloadingController(new FileHandlerReloadingDetector(fileHandler)); reloadingController.addEventListener(ReloadingEvent.ANY, event -> { ReloadingController currController = event.getController(); Object data = event.getData(); currController.resetReloadingState(); // 需要手动充值一下,否则下次文件改变就不会发送此事件啦 System.out.println((reloadingController == currController) + " data:" + data); }); // 准备定时器:用于监控文件的的变化:3秒看一次 注意一定要start()才能生效哦 new PeriodicReloadingTrigger(reloadingController, "自定义数据", 3, TimeUnit.SECONDS).start(); // hold住主线程 while (true) { } }
代码进化一下:结合使用ReloadingFileBasedConfigurationBuilder
以及内置的监听器ReloadingBuilderSupportListener
来实现文件热热加载、热更新的功能:
@Test public void fun3() throws ConfigurationException, InterruptedException { // 准备Builder,并且持有期引用,方便获取到重载后的内容 // 已自动帮绑定好`ReloadingBuilderSupportListener`监听器:因此具有重复一直检测的能力 ReloadingFileBasedConfigurationBuilder builder = new ReloadingFileBasedConfigurationBuilder(PropertiesConfiguration.class); builder.configure(new PropertiesBuilderParametersImpl().setFileName("reload.properties")); // 准备定时器:用于监控文件的的变化:3秒看一次 注意一定要start()才能生效哦 new PeriodicReloadingTrigger(builder.getReloadingController(), "自定义数据", 3, TimeUnit.SECONDS).start(); // 查看文件变化 10秒钟去获取一次 while (true) { Configuration configuration = (Configuration) builder.getConfiguration(); System.out.println("====config hashCode:" + configuration.hashCode()); ConfigurationUtils.dump(configuration, System.out); System.out.println(); TimeUnit.SECONDS.sleep(8); } }
运行程序控制台打印:
====config hashCode:226710952 name=YourBatman ====config hashCode:226710952 name=YourBatman ====config hashCode:1509563803 name=YourBatman-change ====config hashCode:684874119 name=YourBatman-change2222
getConfiguration()
并不是每次都生成新实例,而是在每次发生重载后就会生成一个新的实例。
请注意:ReloadingBuilderSupportListener
它每次监听到事件会builder.resetResult()
重置Result,所以每次你需要重新getConfiguration()
才能得到最新结果(因为只有重重新创建时才会去),而原来的那个Configuration
实例就是老数据,这方便你做日志管理和前后对比。
说明:为何需要get的时候才会有最新数据呢?这是因为:
- createResult()
- initResultInstance() 初始化Configuration实例
FileBasedConfigurationBuilder#initFileHandler
- initEncoding(handler),handler.locate();,handler.load();(执行重新装载内容的逻辑)
- initResultInstance() 初始化Configuration实例
重新装载工作由FileBasedConfigurationBuilder
通过FileHandler
完成,自然作为子类的ReloadingBuilderSupportListener
就更可以咯。
多多使用内置工具,最后给个升级版代码,供以参考:
@Test public void fun4() throws ConfigurationException, InterruptedException { ReloadingFileBasedConfigurationBuilder<PropertiesConfiguration> builder = new ReloadingFileBasedConfigurationBuilder<>(PropertiesConfiguration.class) .configure(new Parameters().properties() .setEncoding("UTF-8") .setFileName("reload.properties") .setListDelimiterHandler(new DefaultListDelimiterHandler(',')) .setReloadingRefreshDelay(2000L) .setThrowExceptionOnMissing(true)); new PeriodicReloadingTrigger(builder.getReloadingController(), "自定义数据", 3, TimeUnit.SECONDS).start(); // 查看文件变化 10秒钟去获取一次 while (true) { Configuration configuration = (Configuration) builder.getConfiguration(); System.out.println("====config hashCode:" + configuration.hashCode()); ConfigurationUtils.dump(configuration, System.out); System.out.println(); TimeUnit.SECONDS.sleep(8); } }
总结
关于如何使用Commons Configuration2.x
实现文件的热加载就介绍到这了,因为它比较实用,相信对你工作上也能有所帮助。
说明:基于Apache Commons Configuration2.x
可以自己实现了一个配置中心,具有实用的动态刷新的功能,有兴趣的小伙伴不妨一试哦~