Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
你是否曾经发现自己使用 IDE 或其他编辑器编辑文件时,会出现一个对话框,通知你文件系统中的某个打开文件已更改并需要重新加载?或者,与 NetBeans IDE 一样,应用程序只是在不通知你的情况下安静地更新文件。以下示例对话框显示了此通知在免费编辑器 jEdit 中的显示方式:

要实现此功能,称为 file change notification (文件变更通知),程序必须能够检测文件系统上相关目录发生了什么。一种方法是轮询文件系统以查找更改,但这种方法效率低下。它不能扩展到具有数百个要监听的打开文件或目录的应用程序。
java.nio.file 包提供了一个名为 Watch Service API 的文件更改通知 API。此 API 使你可以使用监听服务注册目录(或多个目录)。注册时,你告诉服务你感兴趣的事件类型:文件创建,文件删除或文件修改。当服务检测到感兴趣的事件时,它将被转发到注册的进程。已注册的进程有一个线程(或一个线程池),专门用于听它已注册的任何事件。当一个事件进入时,它会根据需要进行处理。
本节包括以下内容:
WatchService API 相当低级,允许你自定义它。你可以按原样使用它,也可以选择在此机制之上创建高级 API,以便它适合你的特定需求。
以下是实现监听服务所需的基本步骤:
WatchService “watcher(监听)” 文件系统。WatchKey 实例。closed 方法),监听服务退出。WatchKeys 是线程安全的,可以与 java.nio.concurrent 包一起使用。你可以将 thread pool 专用于此工作。
因为此 API 更高级,所以在继续之前尝试一下。将 示例保存到你的计算机,然后进行编译。创建将传递给示例的 WatchDirtest 目录。WatchDir 使用单个线程处理所有事件,因此它在等待事件时阻止键盘输入。在单独的窗口中或在后台运行程序,如下所示:
java WatchDir test &
在 test 目录中执行创建,删除和编辑文件。发生任何这些事件时,会向控制台输出一条消息。完成后,删除 test 目录并退出 WatchDir。或者,如果你愿意,可以手动终止该过程。
你还可以通过指定 -r 选项来监听整个文件树。指定 -r 时,WatchDir walks the file tree,使用监听服务注册每个目录。
第一步是使用 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);
}
事件处理循环中的事件顺序如下:
poll–返回排队的密钥(如果有)。如果不可用,则立即返回 null 值。poll(long, TimeUnit)–返回排队的密钥(如果有)。如果排队的密钥不能立即可用,程序将等待指定的时间。TimeUnit 参数确定指定的时间是纳秒、毫秒还是某个其他时间单位。take–返回排队的密钥。如果没有可用的排队密钥,则此方法将等待。pollEvents 方法中获取 WatchEvents 的 List。kind 方法获取事件类型。无论密钥注册了什么事件,都可以接收 OVERFLOW 事件。你可以选择处理溢出或忽略它,但你应该测试它。context 方法用于获取它。reset 将密钥重新置于 ready 状态。如果此方法返回 false,则该键不再有效,并且循环可以退出。此步骤非常 重要。如果你未能调用 reset,则此密钥将不会再收到任何事件。监听密钥具有状态。在任何给定时间,其状态可能是以下之一:
Ready 表示该密钥已准备好接受事件。首次创建时,密钥处于 ready 状态。Signaled 表示一个或多个事件已排队。一旦该密钥被发出信号(signaled),它就不再处于就绪(ready)状态,直到调用了 reset 方法。Invalid 表示密钥不再有效。发生以下事件之一时会发生此状态:
以下是事件处理循环的示例。它取自 示例,该示例监听目录,等待新文件出现。当新文件可用时,将使用 EmailprobeContentType(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> 的代码行的结果。 示例通过创建抑制未检查警告的实用程序 WatchDircast 方法来避免此错误,如下所示:
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<Path>)event;
}
如果你不熟悉 @SuppressWarnings 语法,请参阅 Annotations。
Watch Service API 专为需要通知文件更改事件的应用程序而设计。它非常适合任何应用程序,如编辑器或 IDE,可能有许多打开的文件,需要确保文件与文件系统同步。它也非常适合于监听目录的应用程序服务器,可能等待 .jsp 或 .jar 文件放入,以便部署它们。
此 API 不 设计用于索引硬盘驱动器。大多数文件系统实现都具有文件更改通知的本机支持。Watch Service API 在可用的情况下利用此支持。但是,当文件系统不支持此机制时,Watch Service 将轮询文件系统,等待事件。