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 更高级,所以在继续之前尝试一下。将
示例保存到你的计算机,然后进行编译。创建将传递给示例的 WatchDir
test
目录。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
表示密钥不再有效。发生以下事件之一时会发生此状态:
以下是事件处理循环的示例。它取自
示例,该示例监听目录,等待新文件出现。当新文件可用时,将使用 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。
Watch Service API 专为需要通知文件更改事件的应用程序而设计。它非常适合任何应用程序,如编辑器或 IDE,可能有许多打开的文件,需要确保文件与文件系统同步。它也非常适合于监听目录的应用程序服务器,可能等待 .jsp
或 .jar
文件放入,以便部署它们。
此 API 不 设计用于索引硬盘驱动器。大多数文件系统实现都具有文件更改通知的本机支持。Watch Service API 在可用的情况下利用此支持。但是,当文件系统不支持此机制时,Watch Service 将轮询文件系统,等待事件。