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){
Mapans=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注解即可,这样既实现了解耦,也避免了异常导致的服务异常(如果对这个实现有兴趣的可以评论说明)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。