背??景在研究规则引擎时,如果规则是以文件的形式存储的,需要监控指定的目录或文件,感知规则是否发生变化,然后加载。当然,在其他业务场景中,比如配置文件的动态加载、日志文件的监控、FTP文件变化的监控等,也会遇到类似的场景。本文为大家提供了三种解决方案,并分析了它们的优缺点。建议保存它们以备不时之需。方案一:定时任务+File#lastModified这种方案是最简单最直接可以想到的方案。通过定时任务,轮训查询文件的最后修改时间,并与上次进行比较。如果有变化,说明文件被修改,重新加载或者进行相应的业务逻辑处理。在此处粘贴示例代码: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的具体功能。WatchService监听的文件类型也变得更加丰富了:ENTRY_CREATE目标被创建ENTRY_DELETE目标被删除ENTRY_MODIFY目标被修改OVERFLOW一个特殊的Event,表示该Event被放弃或者丢失如果看WatchService实现类的源码(PollingWatchService),你会发现,本质上,它启动了一个独立的线程来监听文件变化:PollingWatchService(){//TBD:MakethenumberofthreadsconfigurablescheduledExecutor=Executors.newSingleThreadScheduledExecutor(newThreadFactory(){@OverridepublicThreadnewThread(Runnabler){Threadt=newThread(null,r,"FileSystemWatcher",0,false);t.setDaemon(true);returnt;}});也就是说,需要我们手动实现的部分,也由WatchServiceDone帮我们实现了。如果你写一个demo,在验证的时候,你会明显感觉到WatchService监控文件变化不是实时的,有时候监控文件变化需要几秒。以实现类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);与方案一相比,实现起来简单高效。缺点也很明显。它只能监控当前目录下的文件和目录,不能监控子目录。我们也看到监控只能算准实时,监控时间默认只能取API提供的三个值。该API也被Java7在StackOverflow上提出,在MacOS下存在延迟问题,甚至涉及到Windows和Linux系统。方案三:ApacheCommons-IO方案一自己实现,方案二借助JDKAPI实现,方案三借助开源框架实现,也就是几乎每个人都用的commons-io类库项目介绍。引入相应的依赖:
