监视目录以进行更改

您是否曾经发现自己使用 IDE 或其他编辑器编辑文件,并且出现一个对话框来通知您文件系统中一个打开的文件已更改,需要重新加载?也许像 NetBeans IDE 一样,该应用程序只是在不通知您的情况下静默更新文件。以下示例对话框显示了使用免费编辑器jEdit时此通知的外观:

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

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

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

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

本节包括以下内容:

手表服务概述

WatchService API 的级别较低,您可以对其进行自定义。您可以按原样使用它,也可以选择在此机制之上创建一个高级 API,使其适合您的特定需求。

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

  • 为文件系统创建一个WatchService“监视程序”。

  • 对于要监视的每个目录,请向观察者注册。注册目录时,可以指定要通知的事件的类型。您为注册的每个目录收到一个WatchKey实例。

  • 实现无限循环以 await 传入事件。当事件发生时,该密钥会发出 signal,并放入观察者的队列中。

  • 从观察者的队列中检索密钥。您可以从密钥中获取文件名。

  • 检索密钥的每个未决事件(可能有多个事件)并根据需要进行处理。

  • 重置密钥,然后 continueawait 事件。

  • 关闭服务:当线程退出或关闭时(通过调用其closed方法),watch 服务退出。

WatchKeys是线程安全的,可以与java.nio.concurrent包一起使用。您可以为此投入thread pool

试用

由于此 API 更高级,请在 continue 之前try一下。将WatchDir示例保存到您的计算机上并进行编译。创建一个test目录,该目录将传递给示例。 WatchDir使用单个线程来处理所有事件,因此它在 await 事件时会阻止键盘 Importing。在单独的窗口中或在后台运行程序,如下所示:

java WatchDir test &

播放在test目录中创建,删除和编辑文件的过程。这些事件中的任何一个发生时,都会在控制台上显示一条消息。完成后,删除test目录,然后WatchDir退出。或者,如果您愿意,可以手动终止该过程。

您还可以通过指定-r选项来监视整个文件树。当您指定-rWatchDir 走文件树时,请在监视服务中注册每个目录。

创建监视服务并注册事件

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

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

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

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

在监视服务中注册对象时,可以指定要监视的事件类型。受支持的StandardWatchEventKinds事件类型如下:

  • ENTRY_CREATE –创建目录条目。

  • ENTRY_DELETE –删除目录条目。

  • ENTRY_MODIFY –修改目录条目。

  • OVERFLOW –表示事件可能已丢失或丢弃。您不必注册OVERFLOW事件即可接收它。

以下代码段显示了如何为所有三种事件类型注册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);
}

Processing Events

事件处理循环中事件的 Sequences 如下:

  • 获取监视键。提供了三种方法:

  • poll –返回排队的密钥(如果有)。如果不可用,则立即返回null值。

  • poll(long, TimeUnit) –返回一个排队的密钥(如果有)。如果排队的密钥不是立即可用的,则程序将 await 直到指定的时间。 TimeUnit参数确定指定的时间是纳秒,毫秒还是其他时间单位。

  • take –返回排队的钥匙。如果没有可用的排队密钥,则此方法 await。

  • 处理密钥的未决事件。您可以从pollEvents方法中获取WatchEventsList

  • 通过使用kind方法检索事件的类型。无论密钥已注册了什么事件,都可能会收到OVERFLOW事件。您可以选择处理溢出还是忽略它,但是您应该对其进行测试。

  • 检索与事件关联的文件名。文件名存储为事件的上下文,因此使用context方法检索它。

  • 在处理完密钥事件之后,您需要通过调用reset将密钥重新设置为ready状态。如果此方法返回false,则密钥不再有效,并且循环可以退出。这一步非常重要。如果您无法调用reset,那么此键将不会再接收任何事件。

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

  • Ready表示密钥已准备好接受事件。首次创建时,密钥处于就绪状态。

  • Signaled表示一个或多个事件已排队。密钥发出 signal 后,在调用reset方法之前,它将不再处于就绪状态。

  • Invalid表示该键不再有效。当发生以下事件之一时,会发生此状态:

  • 该过程通过使用cancel方法显式取消密钥。

    • 该目录不可访问。

    • 监视服务是closed

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

监视服务 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示例通过创建 Utilcast方法来避免未检查的警告,从而避免了该错误,如下所示:

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

如果您不熟悉@SuppressWarnings语法,请参见Annotations

何时使用和不使用此 API

Watch Service API 专为需要通知文件更改事件的应用程序而设计。它非常适合任何可能具有许多打开文件且需要确保文件与文件系统同步的应用程序,例如编辑器或 IDE。它也非常适合监视目录的应用程序服务器,该目录可能 await.jsp.jar文件删除以进行部署。

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