文档

Java™ 教程-Java Tutorials 中文版
观察目录中的更改
Trail: Essential Classes
Lesson: Basic I/O
Section: File I/O (Featuring NIO.2)

观察目录中的更改

你是否曾经发现自己使用 IDE 或其他编辑器编辑文件时,会出现一个对话框,通知你文件系统中的某个打开文件已更改并需要重新加载?或者,与 NetBeans IDE 一样,应用程序只是在不通知你的情况下安静地更新文件。以下示例对话框显示了此通知在免费编辑器 jEdit 中的显示方式:

示例 jEdit 对话框说明:另一个程序在磁盘上更改了以下文件。

jEdit 对话框显示检测到修改过的文件

要实现此功能,称为 file change notification (文件变更通知),程序必须能够检测文件系统上相关目录发生了什么。一种方法是轮询文件系统以查找更改,但这种方法效率低下。它不能扩展到具有数百个要监听的打开文件或目录的应用程序。

java.nio.file 包提供了一个名为 Watch Service API 的文件更改通知 API。此 API 使你可以使用监听服务注册目录(或多个目录)。注册时,你告诉服务你感兴趣的事件类型:文件创建,文件删除或文件修改。当服务检测到感兴趣的事件时,它将被转发到注册的进程。已注册的进程有一个线程(或一个线程池),专门用于听它已注册的任何事件。当一个事件进入时,它会根据需要进行处理。

本节包括以下内容:

Watch Service 概述

WatchService API 相当低级,允许你自定义它。你可以按原样使用它,也可以选择在此机制之上创建高级 API,以便它适合你的特定需求。

以下是实现监听服务所需的基本步骤:

WatchKeys 是线程安全的,可以与 java.nio.concurrent 包一起使用。你可以将 thread pool 专用于此工作。

试一试

因为此 API 更高级,所以在继续之前尝试一下。将 WatchDir 示例保存到你的计算机,然后进行编译。创建将传递给示例的 test 目录。WatchDir 使用单个线程处理所有事件,因此它在等待事件时阻止键盘输入。在单独的窗口中或在后台运行程序,如下所示:

java WatchDir test &

test 目录中执行创建,删除和编辑文件。发生任何这些事件时,会向控制台输出一条消息。完成后,删除 test 目录并退出 WatchDir。或者,如果你愿意,可以手动终止该过程。

你还可以通过指定 -r 选项来监听整个文件树。指定 -r 时,WatchDir walks the file tree,使用监听服务注册每个目录。

创建 Watch Service 并注册事件

第一步是使用 FileSystem 中的 newWatchService 方法创建一个新的 WatchService 类,如下:

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

接下来,使用监听服务注册一个或多个对象。可以注册实现 Watchable 接口的任何对象。Path 类实现 Watchable 接口,因此每个要监听的目录都注册为 Path 对象。

与任何 Watchable 一样,Path 类实现两个 register 方法。此页面使用双参数版本 register(WatchService, WatchEvent.Kind<?>...)。(三参数版本采用 WatchEvent.Modifier,目前尚未实现。)

使用监听服务注册对象时,可以指定要监听的事件类型。支持的 StandardWatchEventKinds 事件类型如下:

以下代码段显示了如何为所有三种事件类型注册 Path 实例:

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

Path dir = ...;
try {
    WatchKey key = dir.register(watcher,
                           ENTRY_CREATE,
                           ENTRY_DELETE,
                           ENTRY_MODIFY);
} catch (IOException x) {
    System.err.println(x);
}

处理事件

事件处理循环中的事件顺序如下:

  1. 获取监听密钥。提供了三种方法:
    • poll–返回排队的密钥(如果有)。如果不可用,则立即返回 null 值。
    • poll(long, TimeUnit)–返回排队的密钥(如果有)。如果排队的密钥不能立即可用,程序将等待指定的时间。TimeUnit 参数确定指定的时间是纳秒、毫秒还是某个其他时间单位。
    • take–返回排队的密钥。如果没有可用的排队密钥,则此方法将等待。
  2. 处理密钥的挂起事件。从 pollEvents 方法中获取 WatchEventsList
  3. 使用 kind 方法获取事件类型。无论密钥注册了什么事件,都可以接收 OVERFLOW 事件。你可以选择处理溢出或忽略它,但你应该测试它。
  4. 获取与事件关联的文件名。文件名存储为事件的上下文,因此 context 方法用于获取它。
  5. 处理完密钥事件后,需要通过调用 reset 将密钥重新置于 ready 状态。如果此方法返回 false,则该键不再有效,并且循环可以退出。此步骤非常 重要。如果你未能调用 reset,则此密钥将不会再收到任何事件。

监听密钥具有状态。在任何给定时间,其状态可能是以下之一:

以下是事件处理循环的示例。它取自 Email 示例,该示例监听目录,等待新文件出现。当新文件可用时,将使用 probeContentType(Path) 方法检查它是否为 text/plain 文件。目的是将 text/plain 文件通过电子邮件发送到别名,但该实现细节留给读者。

Watch 服务 API 特有的方法以粗体显示:

for (;;) {

    // wait for key to be signaled
    WatchKey key;
    try {
        key = watcher.take();
    } catch (InterruptedException x) {
        return;
    }

    for (WatchEvent<?> event: key.pollEvents()) {
        WatchEvent.Kind<?> kind = event.kind();

        // This key is registered only
        // for ENTRY_CREATE events,
        // but an OVERFLOW event can
        // occur regardless if events
        // are lost or discarded.
        if (kind == OVERFLOW) {
            continue;
        }

        // The filename is the
        // context of the event.
        WatchEvent<Path> ev = (WatchEvent<Path>)event;
        Path filename = ev.context();

        // Verify that the new
        //  file is a text file.
        try {
            // Resolve the filename against the directory.
            // If the filename is "test" and the directory is "foo",
            // the resolved name is "test/foo".
            Path child = dir.resolve(filename);
            if (!Files.probeContentType(child).equals("text/plain")) {
                System.err.format("New file '%s'" +
                    " is not a plain text file.%n", filename);
                continue;
            }
        } catch (IOException x) {
            System.err.println(x);
            continue;
        }

        // Email the file to the
        //  specified email alias.
        System.out.format("Emailing file %s%n", filename);
        //Details left to reader....
    }

    // Reset the key -- this step is critical if you want to
    // receive further watch events.  If the key is no longer valid,
    // the directory is inaccessible so exit the loop.
    boolean valid = key.reset();
    if (!valid) {
        break;
    }
}

获取文件名

从事件上下文中获取文件名。Email 示例使用以下代码获取文件名:

WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path filename = ev.context();

编译 Email 示例时,它会生成以下错误:

Note: Email.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

此错误是将 WatchEvent<T> 强制转换为 WatchEvent<Path> 的代码行的结果。WatchDir 示例通过创建抑制未检查警告的实用程序 cast 方法来避免此错误,如下所示:

@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
    return (WatchEvent<Path>)event;
}

如果你不熟悉 @SuppressWarnings 语法,请参阅 Annotations

何时使用和不使用此 API

Watch Service API 专为需要通知文件更改事件的应用程序而设计。它非常适合任何应用程序,如编辑器或 IDE,可能有许多打开的文件,需要确保文件与文件系统同步。它也非常适合于监听目录的应用程序服务器,可能等待 .jsp.jar 文件放入,以便部署它们。

此 API 设计用于索引硬盘驱动器。大多数文件系统实现都具有文件更改通知的本机支持。Watch Service API 在可用的情况下利用此支持。但是,当文件系统不支持此机制时,Watch Service 将轮询文件系统,等待事件。


Previous page: Finding Files
Next page: Other Useful Methods