Java NIO WatchService的多级目录监控问题

目录

前言

 问题

递归监控子目录

windows系统

跨平台解决方案

跨平台多级目录监控方案

WatchService的其他注意事项


前言

        java7+提供了WatchService类,可以用来实现对文件增删改的监控,demo很简单,代码如下:

package com.test.filewatch;

import java.io.IOException;
import java.nio.file.*;
import java.util.List;

import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

/**
 * @Author: 
 * @Date: 2021/8/25
 * @TIME: 17:53
 */
public class FileWatcherTest2 {
    public static void main(String[] args) throws IOException, InterruptedException {
        String watchPathStr = args[0];
        Path watchPath = Paths.get(watchPathStr).toAbsolutePath();
        WatchService watchService = FileSystems.getDefault().newWatchService();
        watchPath.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW);

        while (true) {
            System.out.println("等待change event");
            WatchKey watchKey = watchService.take();
            List<WatchEvent<?>> watchEvents = watchKey.pollEvents();
            if (watchEvents != null) {
                for (WatchEvent<?> watchEvent : watchEvents) {
                    if (watchEvent.kind() == OVERFLOW) {
                        //文件变更的事件可能丢失
                        System.out.println("overflow,count=" + watchEvent.count() + ",context=" + watchEvent.context());
                    } else if (watchEvent.kind() == ENTRY_CREATE) {
                        //创建文件(夹)
                        System.out.println("create,count=" + watchEvent.count() + ",context=" + watchEvent.context());
                    } else if (watchEvent.kind() == ENTRY_DELETE) {
                        //删除文件(夹)
                        System.out.println("delete,count=" + watchEvent.count() + ",context=" + watchEvent.context());
                    } else if (watchEvent.kind() == ENTRY_MODIFY) {
                        //更新文件(夹)
                        System.out.println("modify,count=" + watchEvent.count() + ",context=" + watchEvent.context());
                    } else {
                        //理论上不会执行到这一步
                        System.out.println("不识别的kind类型" + watchEvent.kind().name());
                    }
                }
            }
            //每次pollEvents处理完后,必须reset,否则无法继续捕获事件
            if (!watchKey.reset())
                System.out.println(watchKey.watchable() + "无法继续监听了....");
        }
    }
}

 问题

        上面的demo,只能用来监控一个dir下所有子文件(夹)的变化,但是捕获不到子文件内部文件的变化情况

递归监控子目录

windows系统

        在注册WatchKey时,有两个重载的方法可供选择,上面的demo是一种,还有一种如下图所示:

        但是, Modifier在JavaSE API Doc中并没有具体的实现,这说明不同的JDK版本,甚至不同的平台可能有各自特有的值。

        windows平台下,可以通过下面的注册方式实现对目录的递归监控:

watchPath.register(watchService, 
                new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW},
                ExtendedWatchEventModifier.FILE_TREE //NOTE:这个变量是windows平台特有,在linux上无法调用
        );

跨平台解决方案

        跨平台解决方案,在百度中也能找到,stackoverflow上也有介绍,原理就是通过root path的watch event,发现CREATE事件,且被创建的时文件夹时,自动向WatchService注册被创建的文件,demo如下:

WatchKey watchKey = this.watchService.take();
        Path changePath = (Path) watchKey.watchable();
        LOG.debug("[{}]发生了变动", changePath);
        List<WatchEvent<?>> watchEvents = watchKey.pollEvents();
        if (watchEvents != null && watchEvents.size() > 0) {
            for (WatchEvent<?> watchEvent : watchEvents) {
                if (watchEvent.kind() == OVERFLOW) {
                    fileChangeEventHeapMq.produce(new WatchServiceEvent(OVERFLOW, changePath));
                } else if (watchEvent.kind() == ENTRY_CREATE) {
                    Path createPath = changePath.resolve((Path) watchEvent.context());
                    boolean isDir = Files.isDirectory(createPath);
                    if (Files.isDirectory(createPath)) {
                        //如果create的是文件夹,还需要监听这个文件夹
                        LOG.debug("file watcher build on path={}", createPath.toAbsolutePath());
                        createPath.register(this.watchService,
                                StandardWatchEventKinds.ENTRY_CREATE,
                                StandardWatchEventKinds.ENTRY_MODIFY,
                                StandardWatchEventKinds.ENTRY_DELETE,
                                StandardWatchEventKinds.OVERFLOW);
                    }
                    this.fileChangeEventHeapMq.produce(new WatchServiceEvent(ENTRY_CREATE, createPath,isDir));
                } else if (watchEvent.kind() == ENTRY_MODIFY) {
                    Path createPath = changePath.resolve((Path) watchEvent.context());
                    this.fileChangeEventHeapMq.produce(new WatchServiceEvent(ENTRY_MODIFY, createPath));
                } else if (watchEvent.kind() == ENTRY_DELETE) {
                    Path createPath = changePath.resolve((Path) watchEvent.context());
                    this.fileChangeEventHeapMq.produce(new WatchServiceEvent(ENTRY_DELETE, createPath));
                }
            }
        }
        if (!watchKey.reset()) {
            LOG.info("[{}]的监听已经无效,可能是由于文件被删除", changePath);
        }

        这就是网上大部分人的解决方案,但这个方案存在一个bug

跨平台多级目录监控方案

        上面一节中的递归监控,存在一个bug,描述如下:

        step.1:dirA被创建。

        step.2:rootPath的CREATE event被触发,开始向WatchService注册dirA。

        step.3:dirA下的文件FileA被创建,此时由于dirA还没有注册成功,因此不会触发任何逻辑,FileA CREATE event丢失,甚至连OVERFLOW event都不会触发。

        step.4:dirA的注册逻辑执行结束。

        step.5:dirA下的文件FileB被创建,此时被dirA的WatchService捕获,触发FileB CREATE event。

        从上面的描述就能看出,由于我们的代码与File System是独立的,无法实现注册过程中,阻塞文件系统的write操作,因此dirA的注册过程持续期间,dirA下所有的变化都会丢失。

        针对问题产生的原因,解决方案有很多,目前本人在做NFS的java实现,我的解决方案是:

        对于NFS server端: 

        1、维护一个concurrent set集合,记录所有注册的dir名称,同一个dir只允许注册一次。

        2、当出现dir CREATE event时,不仅要注册dir,同时还要遍历dir下所有的子目录,对这些目录也进行注册。

        3、使用生产者消费者模型,使得event的消费交给独立的线程,线程进行event合并,当出现dir CREATE event时,自动对后续到来的event进行判断,如果后续event的所属path是dir的子文件(夹),则忽略后续的event。

        

        对于NFS client端:

        1、如果需要同步的是dir,且事件类型是CREATE,则直接同步整个文件夹的所有内容。

WatchService的其他注意事项

       1、 WatchService对同一个文件的同一次操作,可能产生多次event(例如上传.jar文件时,如果文件很大,会产生N次MODIFY event) 。

        2、处理OVERFLOW事件,api doc原文:A special event to indicate that events may have been lost or discarded。 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值