天天看點

Java NIO WatchService的多級目錄監控問題前言遞歸監控子目錄跨平台多級目錄監控方案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是一種,還有一種如下圖所示:

Java NIO WatchService的多級目錄監控問題前言遞歸監控子目錄跨平台多級目錄監控方案WatchService的其他注意事項

        但是, 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。