问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

Java实现监听文件变化的三种方法,推荐第三种

发布网友 发布时间:2024-09-26 15:23

我来回答

1个回答

热心网友 时间:2024-09-30 08:37

背景

在研究规则引擎时,如果规则以文件的形式存储,那么就需要监听指定的目录或文件来感知规则是否变化,进而进行加载。当然,在其他业务场景下,比如想实现配置文件的动态加载、日志文件的监听、FTP文件变动监听等都会遇到类似的场景。

本文给大家提供三种解决方案,并分析其中的利弊,建议收藏,以备不时之需。

方案一:定时任务 + File#lastModified

这个方案是最简单,最能直接想到的解决方案。通过定时任务,轮训查询文件的最后修改时间,与上一次进行对比。如果发生变化,则说明文件已经修改,进行重新加载或对应的业务逻辑处理。

在上篇文章《JDK的一个Bug,监听文件变更要小心了》中已经编写了具体的实例,并且也提出了其中的不足。

这里再把实例代码贴出来:

public class FileWatchDemo { /*** 上次更新时间*/ public static long LAST_TIME = 0L; public static void main(String[] args) throws IOException {String fileName = "/Users/zzs/temp/1.txt";// 创建文件,仅为实例,实践中由其他程序触发文件的变更createFile(fileName);// 执行2次for (int i = 0; i < 2; i++) { long timestamp = readLastModified(fileName); if (timestamp != LAST_TIME) {System.out.println("文件已被更新:" + timestamp);LAST_TIME = timestamp;// 重新加载,文件内容 } else {System.out.println("文件未更新"); }} } public static void createFile(String fileName) throws IOException {File file = new File(fileName);if (!file.exists()) { boolean result = file.createNewFile(); System.out.println("创建文件:" + result);} } public static long readLastModified(String fileName) {File file = new File(fileName);return file.lastModified(); }}

对于文件低频变动的场景,这种方案实现简单,基本上可以满足需求。不过像上篇文章中提到的那样,需要注意Java 8和Java 9中File#lastModified的Bug问题。

但该方案如果用在文件目录的变化上,缺点就有些明显了,比如:操作频繁,效率都损耗在遍历、保存状态、对比状态上了,无法充分利用OS的功能。

方案二:WatchService

在Java 7中新增了java.nio.file.WatchService,通过它可以实现文件变动的监听。WatchService是基于操作系统的文件系统监控器,可以监控系统所有文件的变化,无需遍历、无需比较,是一种基于信号收发的监控,效率高。

public class WatchServiceDemo {public static void main(String[] args) throws IOException {// 这里的监听必须是目录Path path = Paths.get("/Users/zzs/temp/");// 创建WatchService,它是对操作系统的文件监视器的封装,相对之前,不需要遍历文件目录,效率要高很多WatchService watcher = FileSystems.getDefault().newWatchService();// 注册指定目录使用的监听器,监视目录下文件的变化;// PS:Path必须是目录,不能是文件;// StandardWatchEventKinds.ENTRY_MODIFY,表示监视文件的修改事件path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);// 创建一个线程,等待目录下的文件发生变化try {while (true) {// 获取目录的变化:// take()是一个阻塞方法,会等待监视器发出的信号才返回。// 还可以使用watcher.poll()方法,非阻塞方法,会立即返回当时监视器中是否有信号。// 返回结果WatchKey,是一个单例对象,与前面的register方法返回的实例是同一个;WatchKey key = watcher.take();// 处理文件变化事件:// key.pollEvents()用于获取文件变化事件,只能获取一次,不能重复获取,类似队列的形式。for (WatchEvent<?> event : key.pollEvents()) {// event.kind():事件类型if (event.kind() == StandardWatchEventKinds.OVERFLOW) {//事件可能lost or discardedcontinue;}// 返回触发事件的文件或目录的路径(相对路径)Path fileName = (Path) event.context();System.out.println("文件更新: " + fileName);}// 每次调用WatchService的take()或poll()方法时需要通过本方法重置if (!key.reset()) {break;}}} catch (Exception e) {e.printStackTrace();}}}

上述demo展示了WatchService的基本使用方式,注解部分也说明了每个API的具体作用。

通过WatchService监听文件的类型也变得更加丰富:

ENTRY_CREATE 目标被创建

ENTRY_DELETE 目标被删除

ENTRY_MODIFY 目标被修改

OVERFLOW 一个特殊的Event,表示Event被放弃或者丢失

如果查看WatchService实现类(PollingWatchService)的源码,会发现,本质上就是开启了一个独立的线程来监控文件的变化:

PollingWatchService() {// TBD: Make the number of threads configurablescheduledExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(null, r, "FileSystemWatcher", 0, false); t.setDaemon(true); return t; }});}

也就是说,本来需要我们手动实现的部分,也由WatchService内部帮我们完成了。

如果你编写一个demo,进行验证时,会很明显的感觉到WatchService监控文件的变化并不是实时的,有时候要等几秒才监听到文件的变化。以实现类PollingWatchService为例,查看源码,可以看到如下代码:

void enable(Set<? extends Kind<?>> var1, long var2) {synchronized(this) {this.events = var1;Runnable var5 = new Runnable() {public void run() {PollingWatchKey.this.poll();}};this.poller = PollingWatchService.this.scheduledExecutor.scheduleAtFixedRate(var5, var2, var2, TimeUnit.SECONDS);}}

也就是说监听器由按照固定时间间隔的调度器来控制的,而这个时间间隔在SensitivityWatchEventModifier类中定义:

public enum SensitivityWatchEventModifier implements Modifier {HIGH(2),MEDIUM(10),LOW(30);// ...}

该类提供了3个级别的时间间隔,分别为2秒、10秒、30秒,默认值为10秒。这个时间间隔可以在path#register时进行传递:

path.register(watcher, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY},SensitivityWatchEventModifier.HIGH);

相对于方案一,实现起来简单,效率高。不足的地方也很明显,只能监听当前目录下的文件和目录,不能监视子目录,而且我们也看到监听只能算是准实时的,而且监听时间只能取API默认提供的三个值。

该API在Stack Overflow上也有人提出Java 7在Mac OS下有延迟的问题,甚至涉及到Windows和Linux系统,笔者没有进行其他操作系统的验证,如果你遇到类似的问题,可参考对应的文章,寻求解决方案:https://blog.csdn.net/claram/article/details/97919664 。

方案三:Apache Commons-IO

方案一我们自己来实现,方案二借助于JDK的API来实现,方案三便是借助于开源的框架来实现,这就是几乎每个项目都会引入的commons-io类库。

引入相应依赖:

<dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.7</version></dependency>

注意,不同的版本需要不同的JDK支持,2.7需要Java 8及以上版本。

commons-io对实现文件监听的实现位于org.apache.commons.io.monitor包下,基本使用流程如下:

自定义文件监听类并继承 FileAlterationListenerAdaptor 实现对文件与目录的创建、修改、删除事件的处理;

自定义文件监控类,通过指定目录创建一个观察者 FileAlterationObserver;

向监视器添加文件系统观察器,并添加文件监听器;

调用并执行。

第一步:创建文件监听器。根据需要在不同的方法内实现对应的业务逻辑处理。

public class FileListener extends FileAlterationListenerAdaptor {@Overridepublic void onStart(FileAlterationObserver observer) {super.onStart(observer);System.out.println("onStart");}@Overridepublic void onDirectoryCreate(File directory) {System.out.println("新建:" + directory.getAbsolutePath());}@Overridepublic void onDirectoryChange(File directory) {System.out.println("修改:" + directory.getAbsolutePath());}@Overridepublic void onDirectoryDelete(File directory) {System.out.println("删除:" + directory.getAbsolutePath());}@Overridepublic void onFileCreate(File file) {String compressedPath = file.getAbsolutePath();System.out.println("新建:" + compressedPath);if (file.canRead()) {// TODO 读取或重新加载文件内容System.out.println("文件变更,进行处理");}}@Overridepublic void onFileChange(File file) {String compressedPath = file.getAbsolutePath();System.out.println("修改:" + compressedPath);}@Overridepublic void onFileDelete(File file) {System.out.println("删除:" + file.getAbsolutePath());}@Overridepublic void onStop(FileAlterationObserver observer) {super.onStop(observer);System.out.println("onStop");}}

第二步:封装一个文件监控的工具类,核心就是创建一个观察者FileAlterationObserver,将文件路径Path和监听器FileAlterationListener进行封装,然后交给FileAlterationMonitor。

public class FileMonitor {private FileAlterationMonitor monitor;public FileMonitor(long interval) {monitor = new FileAlterationMonitor(interval);}/** * 给文件添加监听 * * @param path 文件路径 * @param listener 文件监听器 */public void monitor(String path, FileAlterationListener listener) {FileAlterationObserver observer = new FileAlterationObserver(new File(path));monitor.addObserver(observer);observer.addListener(listener);}public void stop() throws Exception {monitor.stop();}public void start() throws Exception {monitor.start();}}

第三步:调用并执行:

public class FileRunner {public static void main(String[] args) throws Exception {FileMonitor fileMonitor = new FileMonitor(1000);fileMonitor.monitor("/Users/zzs/temp/", new FileListener());fileMonitor.start();}}

执行程序,会发现每隔1秒输入一次日志。当文件发生变更时,也会打印出对应的日志:

public class WatchServiceDemo {public static void main(String[] args) throws IOException {// 这里的监听必须是目录Path path = Paths.get("/Users/zzs/temp/");// 创建WatchService,它是对操作系统的文件监视器的封装,相对之前,不需要遍历文件目录,效率要高很多WatchService watcher = FileSystems.getDefault().newWatchService();// 注册指定目录使用的监听器,监视目录下文件的变化;// PS:Path必须是目录,不能是文件;// StandardWatchEventKinds.ENTRY_MODIFY,表示监视文件的修改事件path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);// 创建一个线程,等待目录下的文件发生变化try {while (true) {// 获取目录的变化:// take()是一个阻塞方法,会等待监视器发出的信号才返回。// 还可以使用watcher.poll()方法,非阻塞方法,会立即返回当时监视器中是否有信号。// 返回结果WatchKey,是一个单例对象,与前面的register方法返回的实例是同一个;WatchKey key = watcher.take();// 处理文件变化事件:// key.pollEvents()用于获取文件变化事件,只能获取一次,不能重复获取,类似队列的形式。for (WatchEvent<?> event : key.pollEvents()) {// event.kind():事件类型if (event.kind() == StandardWatchEventKinds.OVERFLOW) {//事件可能lost or discardedcontinue;}// 返回触发事件的文件或目录的路径(相对路径)Path fileName = (Path) event.context();System.out.println("文件更新: " + fileName);}// 每次调用WatchService的take()或poll()方法时需要通过本方法重置if (!key.reset()) {break;}}} catch (Exception e) {e.printStackTrace();}}}0

当然,对应的监听时间间隔,可以通过在创建FileMonitor时进行修改。

该方案中监听器本身会启动一个线程定时处理。在每次运行时,都会先调用事件监听处理类的onStart方法,然后检查是否有变动,并调用对应事件的方法;比如,onChange文件内容改变,检查完后,再调用onStop方法,释放当前线程占用的CPU资源,等待下次间隔时间到了被再次唤醒运行。

监听器是基于文件目录为根源的,也可以可以设置过滤器,来实现对应文件变动的监听。过滤器的设置可查看FileAlterationObserver的构造方法:

public class WatchServiceDemo {public static void main(String[] args) throws IOException {// 这里的监听必须是目录Path path = Paths.get("/Users/zzs/temp/");// 创建WatchService,它是对操作系统的文件监视器的封装,相对之前,不需要遍历文件目录,效率要高很多WatchService watcher = FileSystems.getDefault().newWatchService();// 注册指定目录使用的监听器,监视目录下文件的变化;// PS:Path必须是目录,不能是文件;// StandardWatchEventKinds.ENTRY_MODIFY,表示监视文件的修改事件path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);// 创建一个线程,等待目录下的文件发生变化try {while (true) {// 获取目录的变化:// take()是一个阻塞方法,会等待监视器发出的信号才返回。// 还可以使用watcher.poll()方法,非阻塞方法,会立即返回当时监视器中是否有信号。// 返回结果WatchKey,是一个单例对象,与前面的register方法返回的实例是同一个;WatchKey key = watcher.take();// 处理文件变化事件:// key.pollEvents()用于获取文件变化事件,只能获取一次,不能重复获取,类似队列的形式。for (WatchEvent<?> event : key.pollEvents()) {// event.kind():事件类型if (event.kind() == StandardWatchEventKinds.OVERFLOW) {//事件可能lost or discardedcontinue;}// 返回触发事件的文件或目录的路径(相对路径)Path fileName = (Path) event.context();System.out.println("文件更新: " + fileName);}// 每次调用WatchService的take()或poll()方法时需要通过本方法重置if (!key.reset()) {break;}}} catch (Exception e) {e.printStackTrace();}}}1小结

至此,基于Java实现监听文件变化的三种方案便介绍完毕。经过上述分析及实例,大家已经看到,并没有完美的解决方案,根据自己的业务情况及系统的容忍度可选择最适合的方案。而且,在此基础上可以新增一些其他的辅助措施,来避免具体方案中的不足之处。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

原文:https://juejin.cn/post/7103318602748526628
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
土耳其旅游,货币在哪兑换最好,跟人民币汇率怎样? 土耳其货币兑换攻略/土耳其货币怎么兑换最划算 土耳其兑换什么 里拉什么银行能换 一个湖泊约多少升水? 2.1亿立方米水怎么 墨影侠踪手游安卓游戏如何下载 梦见一条小蛇咬我一口,然后变成一个小男孩拉屎,还沾我身上了 女人梦见手被蛇咬出血(梦见被蛇咬出血了预兆) 天津森林公园有哪些 哪个适合避暑 用银行卡交养老保险,如果卡里的钱不够本月扣除,下月补上是否能_百度知... 抖音上出现“朋友推荐”是什么意思? 香荚兰豆浸膏是什么 为什么抖音上有人会推荐我的视频? 老梦见拉屎是怎么回事 梦到老是拉屎 仓鼠宝宝怎么养 如何喂养鼠崽 新手养仓鼠幼崽的注意事项 一窝一般生几个 刚生下的仓鼠怎么处理 宝宝如何喂养 仓鼠幼崽应如何照顾 芋头烧排骨适合怎样的烹饪方式? 芋头烧排骨怎样做五味俱全? 芋头烧排骨的常见做法是什么? 宝宝4 阶段奶粉哪些品牌比较好?适合长期饮用吗? 澳洲爱他美白金版奶粉配方表包括什么呀?奶粉营养好吸收吗? 建筑公司拿什么做进项 奥粒绒黑色粘毛吗 裤子奥粒会越洗越厚吗 怎样去掉衣服上的白渍? 学生网球运动如何避免受伤 真丝衣服上面有白斑如何去除 有歌词 嘀嘀嗒 嘀嘀嗒,闹钟在跳动...男生 如何评价炉石传说选手 Kolento? 版本最强势 炉石传说K神青玉德卡组分享 炉石传说K神咆哮德 炉石传说K神卡组推荐介绍_炉石传说K神咆哮德 炉石... 炉石传说K神心火龙牧 炉石传说K神牧师卡组 一脏到底 炉石传说K神第三埋葬环牧攻略介绍_一脏到底 炉石传说K神第三... ...水电都是好的,离开一段时间后,电闸掉了下来,漏电保护器弹出(黄色小... 家里漏电触电了电闸不会跳 江铃福特旗下有几款SUV,最便宜的只要十几万 预算20万,中大型SUV推荐买哪款?江铃福特领裕怎么样? 什么是抖音推荐? 为什么抖音有人推荐我的作品? 为什么抖音有些人会推荐给我? 为什么我在抖音发作品,别人总是要我帮忙推销? 艾绒灰的功效与作用 ...对不起的却是你!” 徒弟是为___而对不起他自己。 大哥大姐,说说答案... 扇形的半径、弦长、弧长三者是否有系数关系? 三连发!新疆天业、富春染织、海优新材抢权配售指南 从大理市市区怎么到天龙八部影视城!!!急!!! 高中生完全不懂英语语法该怎么学习?