1. 概述

本文我们将学习 Java NIO.2 文件系统中的 WatchService 接口。它是Java 7中和 FileVisitor 一起引入的NIO接口,了解的人较少。

使用 WatchService 前,需要import正确的包:

import java.nio.file.*;

2. 为什么使用 WatchService

一个常见的使用场景就是我们的IDE。

您可能已经注意到,IDE 总是检测源代码文件中发生的更改,当文件被外部修改时,一般 IDE 会弹出对话框通知您,是否选择从重新加载文件。

类似的,一些应用框架支持热加载功能,监听代码修改时能自动更新应用程序。

其实这些功能都是利用 文件更改通知 实现的。

最简单粗暴的办法是我们可以轮询检查文件或目录是否被修改。 但是,此解决方案不可扩展,尤其是当文件和目录达到成百上千时。

Java 7 NIO.2 中,引用了 WatchService,提供了一种更优雅的方式监控文件修改。

3. Watchservice 是如何工作的?

首先创建 WatchService 实例:

WatchService watchService = FileSystems.getDefault().newWatchService();

定义我们需要监听的路径:

Path path = Paths.get("pathToDir");

然后我们需要向 watch service 注册要监听的路径。其中涉及到2个重要的class,StandardWatchEventKinds和WatchKey

WatchKey watchKey = path.register(
  watchService, StandardWatchEventKinds...);

3.1. StandardWatchEventKinds

此类定义我们需要监听的事件类型:

  • StandardWatchEventKinds.ENTRY_CREATE – 在监控目录中创建新条目时触发。 这可能是由于创建新文件或重命名现有文件所致。
  • StandardWatchEventKinds.ENTRY_MODIFY – 当监控目录中的现有条目被修改时触发。 所有文件编辑都会触发此事件。 在某些平台上,即使更改文件属性也会触发它。
  • StandardWatchEventKinds.ENTRY_DELETE – 当在监控目录中删除、移动或重命名条目时触发。
  • StandardWatchEventKinds.OVERFLOW – 一个特殊事件,表示事件可能已丢失或丢弃,我们不需要太关注

3.2. WatchKey

Watch Service 没有提供事件回调通知,我们需要通过 poll方法来轮询:

WatchKey watchKey = watchService.poll();

此方法调用后立即返回,如果没有注册的事件发生则返回 null。可设置timeout参数,当没有事件发生时会等待超时,而不是立即返回null

WatchKey watchKey = watchService.poll(long timeout, TimeUnit units);

使用 take() 方法会一直阻塞,直到有事件发生。

WatchKey watchKey = watchService.take();

需要重点说明的是:WatchKey实例 由 poll 和 take 返回后,我们需要reset watchKey,否者无法捕获更多事件

watchKey.reset();

这意味着每次通过 poll 操作返回 WatchKey 实例时,它会从 watch service 队列中被移除。reset方法 调用将其放回队列中等待更多事件。

所以通常我们需要在一个循环中不断检查被监控目录的变化并进行相应处理:

WatchKey key;
while ((key = watchService.take()) != null) {
    for (WatchEvent<?> event : key.pollEvents()) {
        //process
    }
    key.reset();
}

我们创建一个 WatchKey 来存储 poll 操作的返回值。while 循环会阻塞,直到条件语句返回 WatchKey 或 null。

当获得 WatchKey 时,while 循环会执行其中的代码。我们使用 WatchKey.pollEvents 返回发生的事件列表,然后使用 for each 循环逐个处理事件。

在处理完所有事件之后,我们必须调用 reset 来重新将 WatchKey 入队。

4. 文件夹监听例子

介绍完 WatchService 原理和基本用法之后,我来看一个完整的示例。

出于可移植性的原因,我们将监听用户主目录中的活动,这在所有现代操作系统上都应该可用。

public class DirectoryWatcherExample {

    public static void main(String[] args) {
        WatchService watchService
          = FileSystems.getDefault().newWatchService();

        Path path = Paths.get(System.getProperty("user.home"));

        path.register(
          watchService, 
            StandardWatchEventKinds.ENTRY_CREATE, 
              StandardWatchEventKinds.ENTRY_DELETE, 
                StandardWatchEventKinds.ENTRY_MODIFY);

        WatchKey key;
        while ((key = watchService.take()) != null) {
            for (WatchEvent<?> event : key.pollEvents()) {
                System.out.println(
                  "Event kind:" + event.kind() 
                    + ". File affected: " + event.context() + ".");
            }
            key.reset();
        }
    }
}

当您进入用户主目录并执行任何文件操作,比如创建文件或目录、修改文件内容,甚至删除文件,都会在控制台上记录下来。

例如,假设您进入用户主目录,在空白处右键单击,选择 新建 -> 文件 创建一个新文件,然后将其命名为 testFile。然后添加一些内容并保存。控制台上的输出将如下所示:

Event kind:ENTRY_CREATE. File affected: New Text Document.txt.
Event kind:ENTRY_DELETE. File affected: New Text Document.txt.
Event kind:ENTRY_CREATE. File affected: testFile.txt.
Event kind:ENTRY_MODIFY. File affected: testFile.txt.
Event kind:ENTRY_MODIFY. File affected: testFile.txt.

5. 总结

本文中,我们学习了 Java 7 NIO.2 文件系统 API 中较少使用的功能,特别是 WatchService 接口。

我们还演示了构建一个目录监视应用程序的步骤,以展示其功能。

最后,本文中使用的示例的完整源代码可在 Github 项目 中找到。