如何监视文件夹及其子文件夹的变化

18

我想要监视一个特定的文件夹以便获取其中文件的修改类型,包括添加、编辑或删除,并且需要监视该文件夹及其子文件夹中所有文件的变化。我正在使用WatchService实现,但它只能监视单个路径,无法处理子文件夹。

这是我的解决方案:

try {
        WatchService watchService = pathToWatch.getFileSystem().newWatchService();
        pathToWatch.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

        // loop forever to watch directory
        while (true) {
            WatchKey watchKey;
            watchKey = watchService.take(); // This call is blocking until events are present

            // Create the list of path files
            ArrayList<String> filesLog = new ArrayList<String>();
            if(pathToWatch.toFile().exists()) {
                File fList[] = pathToWatch.toFile().listFiles();
                for (int i = 0; i < fList.length; i++) { 
                    filesLog.add(fList[i].getName());
                }
            }

            // Poll for file system events on the WatchKey
            for (final WatchEvent<?> event : watchKey.pollEvents()) {
                printEvent(event);
            }

            // Save the log
            saveLog(filesLog);

            if(!watchKey.reset()) {
                System.out.println("Path deleted");
                watchKey.cancel();
                watchService.close();
                break;
            }
        }

    } catch (InterruptedException ex) {
        System.out.println("Directory Watcher Thread interrupted");
        return;
    } catch (IOException ex) {
        ex.printStackTrace();  // Loggin framework
        return;
    }

就像我之前说的,我只是为所选路径中的文件获取日志,而我想监视所有文件夹和子文件夹中的文件,类似于:

示例1:

FileA (Created)
FileB
FileC
FolderA FileE
FolderA FolderB FileF

例子2:

FileA
FileB (Modified)
FileC
FolderA FileE
FolderA FolderB FileF

有更好的解决方案吗?


1
这似乎是Oracle实现此功能的方式:https://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java - Erk
3个回答

25
WatchService只会监视您注册的Path,不会递归遍历这些路径。

如果将/Root作为已注册路径,则只监视该路径。

/Root
    /Folder1
    /Folder2
        /Folder3

如果Folder3发生更改,它不会被捕获。

您可以使用递归自行注册目录路径:

private void registerRecursive(final Path root) throws IOException {
    // register all subfolders
    Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
            return FileVisitResult.CONTINUE;
        }
    });
}

现在 WatchService 将会通知所有子文件夹中的所有更改,即你传递的 Path 参数的根目录。


1
@Pacerier 等等,不要创建那么多的观察者。使用单个 Watcher 注册多个 Path - Sotirios Delimanolis
2
你如何监视不存在的子目录?换句话说,如果我正在观察一个文件夹,如果我将一个文件夹放入其中,这算是修改还是创建? - MG Developer
@java_developer 我得试一下,但我认为你正在观察的文件夹中会有一个ENTRY_CREATE来表示新添加的文件夹,以及一个ENTRY_UPDATE来表示你正在观察的文件夹的最后修改日期。 - Sotirios Delimanolis
1
@SotiriosDelimanolis 我现在(2018年)正在尝试实现这个功能,但发现有一些边角情况会导致事件丢失。具体来说,当您进行多目录创建时,即mkdir -p a/b/c(或在Windows上使用带有命令扩展的mkdir a\b\c),Java代码无法看到第二个和第三个子目录的创建,因为它们发生得太快了。当您获得第一个目录的事件并设法注册它时,其他子目录已经存在且不可见。 - Jim Garrison
1
变化更糟糕了。当在Windows 10中删除树(例如使用 rm -rf)时,您可能会先收到父目录的删除事件,再收到子目录的删除事件。太疯狂了。这个API在Windows上似乎并没有真正工作。 - Jim Garrison
显示剩余5条评论

14

按照Sotirios所说的,进行递归注册将起作用。这实际上注册了当前存在的每个目录/子目录。

或者,您可以导入并使用*com.sun.nio.file.ExtendedWatchEventModifier.FILE_TREE*,如下所示:

dir.register(watcher, standardEventsArray, ExtendedWatchEventModifier.FILE_TREE);

这将监视整个子树的更改并考虑添加的目录和子目录。

否则,您将不得不监视任何新的目录/子目录并进行注册。由于每个注册的目录都有一个句柄在监视它,因此在删除目录层次结构的部分时可能会出现问题,因为最低级别的子目录需要首先被删除。


24
请注意,此修饰符仅适用于 Windows 平台。如果在其他平台上指定,Path.register() 将抛出 UnsupportedOperationException 异常。 - jamp
2
这在Linux上不起作用,正如之前的评论所说。 - Avamander

5

我使用Java 8的流和lambda实现了类似的功能。

递归文件夹的发现是通过Consumer @FunctionalInterface实现的:

    final Map<WatchKey, Path> keys = new HashMap<>();

    Consumer<Path> register = p -> {
        if (!p.toFile().exists() || !p.toFile().isDirectory()) {
            throw new RuntimeException("folder " + p + " does not exist or is not a directory");
        }
        try {
            Files.walkFileTree(p, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    LOG.info("registering " + dir + " in watcher service");
                    WatchKey watchKey = dir.register(watcher, new WatchEvent.Kind[]{ENTRY_CREATE}, SensitivityWatchEventModifier.HIGH);
                    keys.put(watchKey, dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            throw new RuntimeException("Error registering path " + p);
        }
    };

每次创建新文件夹时都会调用上述代码,以便在后续动态添加文件夹。 完整解决方案和更多详细信息在此处

如果我必须猜测,我会说这个踩的原因与这个旧版本有关:http://stackoverflow.com/revisions/37658255/1,但是谁投票以及为什么投票无法确定。 - Flexo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接