本教程适用于JDK 8。本页面描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改,了解Java SE 9及以后版本中更新的语言功能的摘要。
请参阅JDK发行说明,了解所有JDK发行版本的新功能、增强功能以及已删除或已弃用选项的信息。
您是否曾经在使用IDE或其他编辑器编辑文件时,弹出一个对话框通知您其中一个打开的文件已在文件系统上更改并需要重新加载?或者,就像NetBeans IDE一样,应用程序会在不通知您的情况下悄悄更新文件。下面是使用免费编辑器jEdit的示例对话框:
要实现这种功能,称为文件更改通知,程序必须能够检测到文件系统上相关目录的变化。一种方法是轮询文件系统以查找变化,但这种方法效率低下。它不能适用于需要监视数百个打开的文件或目录的应用程序。
java.nio.file
包提供了一个文件更改通知API,称为Watch Service API。该API使您能够向监视服务注册一个目录(或多个目录)。在注册时,您告诉服务您感兴趣的事件类型:文件创建、文件删除或文件修改。当服务检测到感兴趣的事件时,它将转发给注册的进程。注册的进程有一个线程(或线程池)专门用于监视其注册的任何事件。当事件发生时,根据需要进行处理。
本节介绍以下内容:
WatchService
API相当底层,允许您进行自定义。您可以直接使用它,也可以选择在此机制之上创建一个高级API,以适应您的特定需求。
实现Watch Service所需的基本步骤如下:
closed
方法关闭服务时,监视服务将退出。当使用监视服务注册对象时,您需要指定要监视的事件类型。支持的事件类型如下:
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
方法从键中获取List
的WatchEvents
。kind
方法检索事件类型。无论键注册了哪些事件,都可能接收到OVERFLOW
事件。您可以选择处理溢出或忽略它,但应该对其进行测试。context
方法来检索它。reset
将键放回ready
状态。如果此方法返回false
,则该键不再有效,循环可以退出。这一步非常重要。如果未调用reset
,该键将不再接收任何进一步的事件。观察键具有状态。在任何给定时间,它的状态可能是以下之一:
Ready
表示键已准备好接受事件。当键被创建时,它处于准备就绪状态。Signaled
表示一个或多个事件已排队。一旦键被信号标记,它就不再处于准备就绪状态,直到调用reset
方法。Invalid
表示键不再活动。这种状态发生在以下事件之一发生时:
以下是事件处理循环的示例。该示例取自
示例,该示例监视一个目录,等待新文件出现。当有新文件可用时,通过使用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。
Watch Service API是为需要接收文件更改事件通知的应用程序设计的。它非常适用于像编辑器或IDE这样可能打开多个文件并需要确保文件与文件系统同步的应用程序。它也非常适用于监视目录的应用服务器,也许等待.jsp
或.jar
文件的到来以便部署它们。
该API不适用于索引硬盘。大多数文件系统实现都原生支持文件更改通知。Watch Service API利用了这种支持(如果可用)。然而,当文件系统不支持此机制时,Watch Service会轮询文件系统以等待事件的发生。