[享学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();(执行重新装载内容的逻辑)

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