前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[享学Netflix] 七、Apache Commons Configuration2.x如何实现文件热加载/热更新?

[享学Netflix] 七、Apache Commons Configuration2.x如何实现文件热加载/热更新?

作者头像
YourBatman
发布2020-03-18 19:31:03
1.5K0
发布2020-03-18 19:31:03
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

成功的路上并不拥挤,因为坚持的人不多。能坚持的人不一定有成就,但要想有成就那就必须得坚持。 代码下载地址:https://github.com/f641385712/netflix-learning

目录
  • 前言
  • 正文
    • ReloadingDetector
      • FileHandlerReloadingDetector
      • 使用示例
    • ReloadingController
      • 使用示例
    • ReloadingFileBasedConfigurationBuilder
    • PeriodicReloadingTrigger
      • 使用示例
  • 总结
    • 声明

前言

热加载是一个常见概念:比如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();(执行重新装载内容的逻辑)

重新装载工作由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可以自己实现了一个配置中心,具有实用的动态刷新的功能,有兴趣的小伙伴不妨一试哦~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 前言
  • 正文
    • ReloadingDetector
      • FileHandlerReloadingDetector
      • 使用示例
    • ReloadingController
      • 使用示例
    • ReloadingFileBasedConfigurationBuilder
      • PeriodicReloadingTrigger
        • 使用示例
    • 总结
    相关产品与服务
    微服务引擎 TSE
    微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档