Java可以如何实现文件变动的监听的示例
应用中使用logback作为日志输出组件的话,大部分会去配置`logback.xml`这个文件,而且生产环境下,直接去修改logback.xml文件中的日志级别,不用重启应用就可以生效那么,这个功能是怎么实现的呢?
应用中使用logback作为日志输出组件的话,大部分会去配置logback.xml这个文件,而且生产环境下,直接去修改logback.xml文件中的日志级别,不用重启应用就可以生效
那么,这个功能是怎么实现的呢?
I.问题描述及分析
针对上面的这个问题,首先抛出一个实际的case,在我的个人网站Z+中,所有的小工具都是通过配置文件来动态新增和隐藏的,因为只有一台服务器,所以配置文件就简化的直接放在了服务器的某个目录下
现在的问题时,我需要在这个文件的内容发生变动时,应用可以感知这种变动,并重新加载文件内容,更新应用内部缓存
一个最容易想到的方法,就是轮询,判断文件是否发生修改,如果修改了,则重新加载,并刷新内存,所以主要需要关心的问题如下:
- 如何轮询?
- 如何判断文件是否修改?
- 配置异常,会不会导致服务不可用?(即容错,这个与本次主题关联不大,但又比较重要...)
II.设计与实现
问题抽象出来之后,对应的解决方案就比较清晰了
- 如何轮询?--》定时器Timer,ScheduledExecutorService都可以实现
- 如何判断文件修改?--》根据java.io.File#lastModified获取文件的上次修改时间,比对即可
那么一个很简单的实现就比较容易了:
publicclassFileUpTest{ privatelonglastTime; @Test publicvoidtestFileUpdate(){ Filefile=newFile("/tmp/alarmConfig"); //首先文件的最近一次修改时间戳 lastTime=file.lastModified(); //定时任务,每秒来判断一下文件是否发生变动,即判断lastModified是否改变 ScheduledExecutorServicescheduledExecutorService=Executors.newScheduledThreadPool(1); scheduledExecutorService.scheduleAtFixedRate(newRunnable(){ @Override publicvoidrun(){ if(file.lastModified()>lastTime){ System.out.println("fileupdate!time:"+file.lastModified()); lastTime=file.lastModified(); } } },0,1,TimeUnit.SECONDS); try{ Thread.sleep(1000*60); }catch(InterruptedExceptione){ e.printStackTrace(); } } }
上面这个属于一个非常简单,非常基础的实现了,基本上也可以满足我们的需求,那么这个实现有什么问题呢?
定时任务的执行中,如果出现了异常会怎样?
对上面的代码稍作修改
publicclassFileUpTest{ privatelonglastTime; privatevoidttt(){ thrownewNullPointerException(); } @Test publicvoidtestFileUpdate(){ Filefile=newFile("/tmp/alarmConfig"); lastTime=file.lastModified(); ScheduledExecutorServicescheduledExecutorService=Executors.newScheduledThreadPool(1); scheduledExecutorService.scheduleAtFixedRate(newRunnable(){ @Override publicvoidrun(){ if(file.lastModified()>lastTime){ System.out.println("fileupdate!time:"+file.lastModified()); lastTime=file.lastModified(); ttt(); } } },0,1,TimeUnit.SECONDS); try{ Thread.sleep(1000*60*10); }catch(InterruptedExceptione){ e.printStackTrace(); } } }
实际测试,发现只有首次修改的时候,触发了上面的代码,但是再次修改则没有效果了,即当抛出异常之后,定时任务将不再继续执行了,这个问题的主要原因是因为ScheduledExecutorService的原因了
直接查看ScheduledExecutorService的源码注释说明
Ifanyexecutionofthetaskencountersanexception,subsequentexecutionsaresuppressed.Otherwise,thetaskwillonlyterminateviacancellationorterminationoftheexecutor.即如果定时任务执行过程中遇到发生异常,则后面的任务将不再执行。
所以,使用这种姿势的时候,得确保自己的任务不会抛出异常,否则后面就没法玩了
对应的解决方法也比较简单,整个catch一下就好
III.进阶版
前面是一个基础的实现版本了,当然在java圈,基本上很多常见的需求,都是可以找到对应的开源工具来使用的,当然这个也不例外,而且应该还是大家比较属性的apache系列
首先maven依赖
commons-io commons-io 2.6
主要是借助这个工具中的FileAlterationObserver,FileAlterationListener,FileAlterationMonitor三个类来实现相关的需求场景了,当然使用也算是很简单了,以至于都不太清楚可以再怎么去说明了,直接看下面从我的一个开源项目quick-alarm中拷贝出来的代码
publicclassPropertiesConfListenerHelper{ publicstaticbooleanregisterConfChangeListener(Filefile,Function>func){ try{ //轮询间隔5秒 longinterval=TimeUnit.SECONDS.toMillis(5); //因为监听是以目录为单位进行的,所以这里直接获取文件的根目录 Filedir=file.getParentFile(); //创建一个文件观察器用于过滤 FileAlterationObserverobserver=newFileAlterationObserver(dir, FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.nameFileFilter(file.getName()))); //设置文件变化监听器 observer.addListener(newMyFileListener(func)); FileAlterationMonitormonitor=newFileAlterationMonitor(interval,observer); monitor.start(); returntrue; }catch(Exceptione){ log.error("registerpropertieschangelistenererror!e:{}",e); returnfalse; } } staticfinalclassMyFileListenerextendsFileAlterationListenerAdaptor{ privateFunction >func; publicMyFileListener(Function >func){ this.func=func; } @Override publicvoidonFileChange(Filefile){ Map ans=func.apply(file);//如果加载失败,打印一条日志 log.warn("PropertiesConfigchanged!reloadans:{}",ans); } } }
针对上面的实现,简单说明几点:
- 这个文件监听,是以目录为根源,然后可以设置过滤器,来实现对应文件变动的监听
- 如上面registerConfChangeListener方法,传入的file是具体的配置文件,因此构建参数的时候,捞出了目录,捞出了文件名作为过滤
- 第二参数是jdk8语法,其中为具体的读取配置文件内容,并映射为对应的实体对象
一个问题,如果func方法执行时,也抛出了异常,会怎样?
实际测试表现结果和上面一样,抛出异常之后,依然跪,所以依然得注意,不要跑异常
那么简单来看一下上面的实现逻辑,直接扣出核心模块
publicvoidrun(){ while(true){ if(this.running){ Iteratorvar1=this.observers.iterator(); while(var1.hasNext()){ FileAlterationObserverobserver=(FileAlterationObserver)var1.next(); observer.checkAndNotify(); } if(this.running){ try{ Thread.sleep(this.interval); }catch(InterruptedExceptionvar3){ ; } continue; } } return; } }
从上面基本上一目了然,整个的实现逻辑了,和我们的第一种定时任务的方法不太一样,这儿直接使用线程,死循环,内部采用sleep的方式来来暂停,因此出现异常时,相当于直接抛出去了,这个线程就跪了
补充JDK版本
jdk1.7,提供了一个WatchService,也可以用来实现文件变动的监听,之前也没有接触过,才知道有这个东西,然后搜了一下使用相关,发现也挺简单的,看到有博文说明是基于事件驱动式的,效率更高,下面也给出一个简单的示例demo
@Test publicvoidtestFileUpWather()throwsIOException{ //说明,这里的监听也必须是目录 Pathpath=Paths.get("/tmp"); WatchServicewatcher=FileSystems.getDefault().newWatchService(); path.register(watcher,ENTRY_MODIFY); newThread(()->{ try{ while(true){ WatchKeykey=watcher.take(); for(WatchEvent>event:key.pollEvents()){ if(event.kind()==OVERFLOW){ //事件可能lostordiscarded continue; } PathfileName=(Path)event.context(); System.out.println("文件更新:"+fileName); } if(!key.reset()){//重设WatchKey break; } } }catch(Exceptione){ e.printStackTrace(); } }).start(); try{ Thread.sleep(1000*60*10); }catch(InterruptedExceptione){ e.printStackTrace(); } }
IV.小结
使用Java来实现配置文件变动的监听,主要涉及到的就是两个点
- 如何轮询:定时器(Timer,ScheduledExecutorService),线程死循环+sleep
- 文件修改:File#lastModified
整体来说,这个实现还是比较简单的,无论是自定义实现,还是依赖commos-io来做,都没太大的技术成本,但是需要注意的一点是:
- 千万不要在定时任务or文件变动的回调方法中抛出异常!!!
为了避免上面这个情况,一个可以做的实现是借助EventBus的异步消息通知来实现,当文件变动之后,发送一个消息即可,然后在具体的重新加载文件内容的方法上,添加一个@Subscribe注解即可,这样既实现了解耦,也避免了异常导致的服务异常(如果对这个实现有兴趣的可以评论说明)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。