成功的路上并不拥挤,因为坚持的人不多。能坚持的人不一定有成就,但要想有成就那就必须得坚持。 代码下载地址:https://github.com/f641385712/netflix-learning
热加载是一个常见概念:比如Java中的热加载类,更改某一行代码可以在不重启项目的情况下生效,这个一般在开发环境、调试环境使用得比较多,可提高效率。
热加载在配置文件修改场景下一般也有硬性需求:在不能重启项目的情况下,改动某个key的值,希望立马生效。比如某个活动开始与否是由某个值决定的,而线上需要在不停机、不重启情况打开此开关,这就需要文件热加载、热更新作为基础能力提供支撑。
在1.x版本的时候,我演示过热加载、热更新的示例,你可以移步此处查看,它是通过ReloadingStrategy
接口来实现的。
而2.x版本同样的完全推翻了这一套API,改而设计了一套全新的、耦合度更低的API方式,更加灵活的来实现Reloading
重新加载的能力,这边是接下来的主要内容。
说明:重新加载并不是文件专属,任何可以被load()进来的资源都可以被加以
Reloading
的概念。
ReloadingDetector
接口用于检测(Detector
)是否需要重载,这个接口没有定义如何执行对重新加载的检查,也就是接口不决定进行重载的条件,完全取决于具体实现。
它是实现Reloading
决定重新加载与否的最基础支持接口:
public interface ReloadingDetector {
// 检查是否满足重新加载操作的所有条件
// true:表示需要重新加载 false表示不需要
boolean isReloadingRequired();
// 通知此对象已执行重新加载操作。这方法在reloadingRequired()返回后调用
void reloadingPerformed();
}
注意:ReloadingDetector
本身并不会主动监听某一资源,只有你手动去调用isReloadingRequired()
方法才能知道是否需要重载。它的继承结构如下图:
虽然实现类有多个,但本文有且仅讨论最常用的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
负责同步:一个实例同一时刻只被一个线程访问。
直译:重载控制器。一个用于以通用方式添加对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()
方法的返回值解释:
注意:
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什么。
如果你想得到一个具有重载Reloading
能力的Builder
,你可使用它来完成文件的热加载效果。
使用本类在构建的时候,已经自动帮你注册上了ReloadingBuilderSupportListener
这个监听器(详情参见API:ReloadingFileBasedConfigurationBuilder#createReloadingController()
)
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的时候才会有最新数据呢?这是因为:
FileBasedConfigurationBuilder#initFileHandler
重新装载工作由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
可以自己实现了一个配置中心,具有实用的动态刷新的功能,有兴趣的小伙伴不妨一试哦~