监视目录更改
要实现文件更改通知,程序必须能够检测到文件系统上相关目录正在发生的事情。一种方法是轮询文件系统以查找更改,但这种方法效率低下。它不适用于需要监视数百个打开文件或目录的应用程序。
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);
}
处理事件
事件处理循环中的事件顺序如下
- 获取监视密钥。
WatchService
类提供了三种方法poll()
– 如果可用,则返回一个排队的密钥。如果不可用,则立即返回 null 值。poll(long, TimeUnit)
– 如果可用,则返回一个排队的密钥。如果排队的密钥不可立即使用,程序将等待指定的时间。TimeUnit
参数确定指定的时间是纳秒、毫秒还是其他时间单位。take()
– 返回一个排队的密钥。如果排队的密钥不可用,此方法将等待。
- 处理密钥的待处理事件。您从
pollEvents()
方法中获取List
的WatchEvent
对象。 - 使用
WatchEvent
对象的kind()
方法检索事件类型。无论密钥注册了哪些事件,都有可能收到OVERFLOW
事件。您可以选择处理溢出或忽略它,但您应该对其进行测试。 - 检索与事件关联的文件名。文件名存储为事件的上下文,因此使用
context()
方法来检索它。 - 处理完密钥的事件后,您需要通过调用此
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 日