背景在研究规则引擎的时候,如果规则是以文件的形式存储的,那么需要监控指定的目录或者文件来感知规则是否发生变化,然后加载它们。当然,在其他业务场景中,比如配置文件的动态加载、日志文件的监控、FTP文件变化的监控等,也会遇到类似的场景。本文为大家提供了三种解决方案,并分析了它们的优缺点。建议保存它们以备不时之需。方案一:定时任务+File#lastModified这种方案是最简单最直接可以想到的方案。通过定时任务,轮训查询文件的最后修改时间,并与上次进行比较。如果有变化,说明文件被修改,重新加载或者进行相应的业务逻辑处理。具体的例子在上一篇《JDK的一个Bug,监听文件变更要小心了》已经写过了,不足之处也提出来了。在此处粘贴示例代码:publicclassFileWatchDemo{/***最后更新时间*/publicstaticlongLAST_TIME=0L;publicstaticvoidmain(String[]args)throwsIOException{StringfileName="/Users/zzs/temp/1.txt";//创建一个文件,只是一个例子。实际上,其他程序会触发文件更改createFile(fileName);//执行2次for(inti=0;i<2;i++){longtimestamp=readLastModified(fileName);if(timestamp!=LAST_TIME){System.out.println("文件已更新:"+timestamp);LAST_TIME=时间戳;//重新加载,文件内容}else{System.out.println("文件未更新");}}}publicstaticvoidcreateFile(StringfileName)抛出IOException{Filefile=newFile(fileName);如果(!file.exists()){布尔结果=file.createNewFile();System.out.println("创建文件:"+result);}}publicstaticlongreadLastModified(StringfileName){Filefile=newFile(fileName);返回文件.lastModified();}}对于文件变化频率较低的场景,该方案实现简单,基本可以满足需求。但是在上一篇文章中提到,需要注意Java8和Java9中File#lastModified的bug。但是如果使用这种方案来改变文件目录,弊端就很明显了。例如,操作频繁,在遍历、保存状态、比较状态时效率低下,无法充分发挥OS的功能。方案二:WatchService在Java7中增加了java.nio.file.WatchService,通过它可以实现对文件变化的监听。WatchService是一个基于操作系统的文件系统监视器。无需遍历、比较,即可监控系统中所有文件的变化。它是一种基于信号发送和接收的高效率监控。publicclassWatchServiceDemo{publicstaticvoidmain(String[]args)throwsIOException{//这里的监听必须是一个目录Pathpath=Paths.get("/Users/zzs/temp/");//创建WatchService,是对操作系统的文件监听器的封装,相比之前,不需要遍历文件目录,效率高很多。WatchService观察者=FileSystems.getDefault().newWatchService();//注册指定目录使用的监听器,监听目录下的文件变化;//PS:Path必须是目录,不能是文件;//StandardWatchEventKinds.ENTRY_MODIFY,表示监视文件的修改事件path.register(watcher,StandardWatchEventKinds.ENTRY_MODIFY);//创建一个线程来等待目录中的文件发生变化try{while(true){//获取目录中的变化://take()是一个阻塞方法,它在返回之前等待来自监视器的信号。//也可以使用watcher.poll()方法,非阻塞方法,会立即返回此时monitor是否有信号。//返回结果WatchKey是一个单例对象,与前面register方法返回的实例相同;WatchKeykey=watcher.take();//处理文件变化事件://key.pollEvents()用于获取文件变化事件只能获取一次,不能重复获取,类似队列的形式。for(WatchEvent>event:key.pollEvents()){//event.kind():事件类型if(event.kind()==StandardWatchEventKinds.OVERFLOW){//事件可能丢失或丢弃continue;}//返回触发事件的文件或目录的路径(相对路径)PathfileName=(Path)event.context();System.out.println("文件更新:"+fileName);}//每次调用WatchService()或poll()方法都需要通过这个方法重置if(!key.reset()){break;}}}catch(Exceptione){e.printStackTrace();}}}上面的demo展示了WatchService的基本用法,注解部分也说明了各个API的具体作用。以实现类PollingWatchService为例,查看源码,可以看到如下代码:Runnablevar5=newRunnable(){publicvoidrun(){PollingWatchKey.this.poll();}};this.poller=PollingWatchService.this.scheduledExecutor.scheduleAtFixedRate(var5,var2,var2,TimeUnit.SECONDS);}}也就是说,监听器是被一个调度器控制在一个固定的时间间隔,而这个时间间隔是在SensitivityWatchEventModifier类中定义的:publicenumSensitivityWatchEventModifierimplementsModifier{HIGH(2),MEDIUM(10),LOW(30);//...}该类提供了3级时间间隔,分别为2秒、10秒、30秒,默认值为10秒。这个时间间隔可以在path#register中传递:path.register(watcher,newWatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY},SensitivityWatchEventModifier.HIGH);与方案一相比,实现起来简单高效。公共类FileListener扩展FileAlterationListenerAdaptor{@OverridepublicvoidonStart(FileAlterationObserverobserver){super.onStart(observer);System.out.println("开始");}@OverridepublicvoidonDirectoryCreate(Filedirectory){System.out.println("New:"+directory.getAbsolutePath());}@OverridepublicvoidonDirectoryChange(Filedirectory){System.out.println("Modify:"+directory.getAbsolutePath());}@OverridepublicvoidonDirectoryDe??lete(Filedirectory){System.out.println("删除:"+directory.getAbsolutePath());}@OverridepublicvoidonFileCreate(Filefile){StringcompressedPath=file.getAbsolutePath();System.out.println("新建:"+compressedPath);if(file.canRead()){//TODO读取或重新加载文件内容System.out.println("filechanged,process");}}@OverridepublicvoidonFileChange(Filefile){StringcompressedPath=file.getAbsolutePath();System.out.println("修改:"+compressedPath);}@OverridepublicvoidonFileDelete(Filefile){System.out.println("删除:"+file.getAbsolutePath());}@OverridepublicvoidonStop(FileAlterationObserverobserver){super.onStop(observer);System.out.println("停止");}}第二步:封装一个文件监听工具类,核心是创建一个观察者FileAlterationObserver,封装文件路径Path和监听器FileAlterationListener,然后交给FileAlterationMonitor公共类FileMonitor{私有FileAlterationMonitor监视器;公共FileMonitor(长间隔){monitor=newFileAlterationMonitor(间隔);}/***添加监听器到文件**@parampath文件路径*@paramlistener文件监听器*/publicvoidmonitor(Stringpath,FileAlterationListenerlistener){FileAlterationObserverobserver=newFileAlterationObserver(newFile(path));monitor.addObserver(观察者);observer.addListener(侦听器);}publicvoidstop()throwsException{monitor.stop();}publicvoidstart()throwsException{monitor.start();}}第三步:调用并执行:publicclassFileRunner{publicstaticvoidmain(String[]args)throwsException{FileMonitorfileMonitor=newFileMonitor(1000);fileMonitor.monitor("/Users/zzs/temp/",newFileListener());文件监控器.start();}}执行程序,你会发现每1秒就进入一次日志。当文件发生变化时,也会打印相应的日志:onStartmodification:/Users/zzs/temp/1.txtStoponStartonStop当然可以在创建FileMonitor时修改相应的监控间隔。在这个方案中,监听器本身会启动一个线程进行定时处理。每次运行时,会先调用事件监听处理类的onStart方法,然后检查是否有变化,调用相应的事件方法;例如onChange文件内容发生变化,检查后调用onStop方法释放当前线程占用的CPU资源,等待下一个interval被唤醒再次运行。监听器是以文件目录为根,也可以设置过滤器监听相应的文件变化。Filter的设置可以在FileAlterationObserver的构造方法中找到:publicFileAlterationObserver(StringdirectoryName,FileFilterfileFilter,IOCasecaseSensitivity){this(newFile(directoryName),fileFilter,caseSensitivity);}总结到此为止,三种基于文件变化的监控方法关于Java程序的介绍。经过上面的分析和例子,你已经看到没有完美的解决方案,你可以根据自己的业务情况和系统的容忍度来选择最合适的方案。此外,在此基础上,还可以增加一些其他的辅助措施,避免具体方案的不足。博主简介:《SpringBoot技术内幕》技术书籍作者,热爱研究技术,撰写技术文章。公众号:《程序新视界》,博主的公众号,欢迎关注~技术交流:请联系博主微信号:zhuan2quan
