系列中的上一篇
当前教程
监视目录更改
这是本系列的最后一篇!

系列中的上一篇: 遍历文件树

监视目录更改

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

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

 

监视服务概述

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

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

  • 为文件系统创建一个WatchService“监视器”。
  • 对于要监视的每个目录,将其注册到监视器。注册目录时,您指定要接收通知的事件类型。您将为注册的每个目录接收一个WatchKey 实例。
  • 实现一个无限循环来等待传入事件。当事件发生时,密钥将被发出信号并放入监视器的队列中。
  • 从监视器的队列中检索密钥。您可以从密钥中获取文件名。
  • 检索密钥的每个待处理事件(可能有多个事件)并根据需要进行处理。
  • 重置密钥,并继续等待事件。
  • 关闭服务:当线程退出或关闭(通过调用其close() 方法)时,监视服务退出。

WatchKeys 是线程安全的,可以与java.nio.concurrent 包一起使用。您可以为此工作专门分配一个线程池。

 

试一试

由于此 API 比较高级,请在继续之前尝试一下。保存并查看本节末尾的WatchDir 示例。将其保存到您的计算机,并进行编译。创建一个将传递给示例的测试目录。WatchDir 使用单个线程来处理所有事件,因此它在等待事件时会阻塞键盘输入。要么在单独的窗口中运行程序,要么在后台运行,如下所示

$ java WatchDir test &

在测试目录中玩创建、删除和编辑文件。当这些事件中的任何一个发生时,一条消息将打印到控制台。完成后,删除测试目录,WatchDir 将退出。或者,如果您愿意,也可以手动终止进程。

您还可以通过指定-r 选项来监视整个文件树。当您指定-r 时,WatchDir 会遍历文件树,将每个目录注册到监视服务。

 

创建监视服务并注册事件

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

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

接下来,将一个或多个对象注册到监视服务。任何实现Watchable 接口的对象都可以注册。Path 类实现了Watchable 接口,因此要监视的每个目录都将作为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);
}

 

处理事件

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

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

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

  • Ready 表示密钥已准备好接受事件。首次创建时,密钥处于 ready 状态。
  • Signaled 表示一个或多个事件已排队。一旦密钥发出信号,它将不再处于 ready 状态,直到调用reset() 方法。
  • Invalid 表示密钥不再活动。当发生以下事件之一时,将发生此状态
    • 进程通过使用cancel() 方法显式取消密钥。
    • 目录变得不可访问。
    • 监视服务已关闭。

这是一个事件处理循环的示例。它监视一个目录,等待新文件出现。当一个新文件可用时,它将使用 probeContentType(Path) 方法检查它是否是文本/普通文件。

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;
    }
}

 

获取文件名

文件名从事件上下文获取。前面的示例使用以下代码获取文件名

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

编译此示例时,会生成以下错误

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

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

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

如果您不熟悉 @SuppressWarnings 语法,请参阅关于 注解 的部分。

 

何时使用和不使用此 API

Watch Service API 旨在用于需要接收文件更改事件通知的应用程序。它非常适合任何应用程序,例如编辑器或 IDE,这些应用程序可能具有许多打开的文件,并且需要确保文件与文件系统同步。它也适合于监视目录的应用程序服务器,例如等待 .jsp.jar 文件下降以进行部署。

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

 

WatchDir 示例

import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.LinkOption.*;
import java.nio.file.attribute.*;
import java.io.*;
import java.util.*;

/**
 * Example to watch a directory (or tree) for changes to files.
 */

public class WatchDir {

    private final WatchService watcher;
    private final Map<WatchKey,Path> keys;
    private final boolean recursive;
    private boolean trace = false;

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

    /**
     * Register the given directory with the WatchService
     */
    private void register(Path dir) throws IOException {
        WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        if (trace) {
            Path prev = keys.get(key);
            if (prev == null) {
                System.out.format("register: %s\n", dir);
            } else {
                if (!dir.equals(prev)) {
                    System.out.format("update: %s -> %s\n", prev, dir);
                }
            }
        }
        keys.put(key, dir);
    }

    /**
     * Register the given directory, and all its sub-directories, with the
     * WatchService.
     */
    private void registerAll(final Path start) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException
            {
                register(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * Creates a WatchService and registers the given directory
     */
    WatchDir(Path dir, boolean recursive) throws IOException {
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<WatchKey,Path>();
        this.recursive = recursive;

        if (recursive) {
            System.out.format("Scanning %s ...\n", dir);
            registerAll(dir);
            System.out.println("Done.");
        } else {
            register(dir);
        }

        // enable trace after initial registration
        this.trace = true;
    }

    /**
     * Process all events for keys queued to the watcher
     */
    void processEvents() {
        for (;;) {

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

            Path dir = keys.get(key);
            if (dir == null) {
                System.err.println("WatchKey not recognized!!");
                continue;
            }

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

                // TBD - provide example of how OVERFLOW event is handled
                if (kind == OVERFLOW) {
                    continue;
                }

                // Context for directory entry event is the file name of entry
                WatchEvent<Path> ev = cast(event);
                Path name = ev.context();
                Path child = dir.resolve(name);

                // print out event
                System.out.format("%s: %s\n", event.kind().name(), child);

                // if directory is created, and watching recursively, then
                // register it and its sub-directories
                if (recursive && (kind == ENTRY_CREATE)) {
                    try {
                        if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
                            registerAll(child);
                        }
                    } catch (IOException x) {
                        // ignore to keep sample readbale
                    }
                }
            }

            // reset key and remove from set if directory no longer accessible
            boolean valid = key.reset();
            if (!valid) {
                keys.remove(key);

                // all directories are inaccessible
                if (keys.isEmpty()) {
                    break;
                }
            }
        }
    }

    static void usage() {
        System.err.println("usage: java WatchDir [-r] dir");
        System.exit(-1);
    }

    public static void main(String[] args) throws IOException {
        // parse arguments
        if (args.length == 0 || args.length > 2)
            usage();
        boolean recursive = false;
        int dirArg = 0;
        if (args[0].equals("-r")) {
            if (args.length < 2)
                usage();
            recursive = true;
            dirArg++;
        }

        // register directory and process its events
        Path dir = Paths.get(args[dirArg]);
        new WatchDir(dir, recursive).processEvents();
    }
}

上次更新: 2023 年 1 月 25 日


系列中的上一篇
当前教程
监视目录更改
这是本系列的最后一篇!

系列中的上一篇: 遍历文件树