文档

Java™ 教程
隐藏目录
监视目录变化
教程:基本Java类
课程:基本输入输出
章节:文件输入输出(使用NIO.2)

监视目录的变化

您是否曾经在使用IDE或其他编辑器编辑文件时,弹出一个对话框通知您其中一个打开的文件已在文件系统上更改并需要重新加载?或者,就像NetBeans IDE一样,应用程序会在不通知您的情况下悄悄更新文件。下面是使用免费编辑器jEdit的示例对话框:

jEdit示例对话框:以下文件已被其他程序更改。

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

要实现这种功能,称为文件更改通知,程序必须能够检测到文件系统上相关目录的变化。一种方法是轮询文件系统以查找变化,但这种方法效率低下。它不能适用于需要监视数百个打开的文件或目录的应用程序。

java.nio.file包提供了一个文件更改通知API,称为Watch Service API。该API使您能够向监视服务注册一个目录(或多个目录)。在注册时,您告诉服务您感兴趣的事件类型:文件创建、文件删除或文件修改。当服务检测到感兴趣的事件时,它将转发给注册的进程。注册的进程有一个线程(或线程池)专门用于监视其注册的任何事件。当事件发生时,根据需要进行处理。

本节介绍以下内容:

Watch Service概述

WatchService API相当底层,允许您进行自定义。您可以直接使用它,也可以选择在此机制之上创建一个高级API,以适应您的特定需求。

实现Watch Service所需的基本步骤如下:

当使用监视服务注册对象时,您需要指定要监视的事件类型。支持的事件类型如下:

以下代码片段显示了如何为所有三个事件类型注册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方法从键中获取ListWatchEvents
  3. 使用kind方法检索事件类型。无论键注册了哪些事件,都可能接收到OVERFLOW事件。您可以选择处理溢出或忽略它,但应该对其进行测试。
  4. 检索与事件关联的文件名。文件名存储为事件的上下文,因此使用context方法来检索它。
  5. 处理完键的事件后,需要通过调用reset将键放回ready状态。如果此方法返回false,则该键不再有效,循环可以退出。这一步非常重要。如果未调用reset,该键将不再接收任何进一步的事件。

观察键具有状态。在任何给定时间,它的状态可能是以下之一:

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

以下是专用于观察服务API的方法(以粗体显示):

for (;;) {

    // 等待键被标记
    WatchKey key;
    try {
        key = watcher.take();
    } catch (InterruptedException x) {
        return;
    }

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

        // 这个键仅注册为
        // ENTRY_CREATE事件,
        // 但无论是否丢失或丢弃事件,
        // 都可能发生OVERFLOW事件。
        if (kind == OVERFLOW) {
            continue;
        }

        // 文件名是事件的上下文。
        WatchEvent<Path> ev = (WatchEvent<Path>)event;
        Path filename = ev.context();

        // 验证新文件是否为文本文件。
        try {
            // 将文件名解析为目录的路径。
            // 如果文件名是"test",目录是"foo",
            // 解析后的名称是"test/foo"。
            Path child = dir.resolve(filename);
            if (!Files.probeContentType(child).equals("text/plain")) {
                System.err.format("新文件'%s'" +
                    "不是纯文本文件。%n", filename);
                continue;
            }
        } catch (IOException x) {
            System.err.println(x);
            continue;
        }

        // 将文件发送到指定的电子邮件别名。
        System.out.format("正在发送文件 %s%n", filename);
        // 具体细节留给读者....
    }

    // 重置键 -- 如果要接收更多的观察事件,这一步很重要。
    // 如果键无效,目录不可访问,因此退出循环。
    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会轮询文件系统以等待事件的发生。


上一页: 查找文件
下一页: 其他有用的方法