1. 引言
WatchService 是 Java NIO(New IO)的一部分,可用于实时监视文件系统中的变更。你可以把它想象成文件夹的报警器:只要有人添加、删除或修改文件——你就会立即收到通知。该能力在 Java 7 中随 NIO.2 引入,在此之前,开发者要么手动轮询文件夹(polling),要么使用第三方库。
WatchService 的实际用途十分广泛:它可以自动处理新文件、记录日志并进行备份、将文件夹与服务器或云同步,以及监视配置文件的变更。
为监视注册目录
要开始监视变更,需要:
- 获取一个 WatchService 实例。
- 注册需要的文件夹并指定关心的事件类型。
获取 WatchService
import java.nio.file.*;
WatchService watchService = FileSystems.getDefault().newWatchService();
注册文件夹
注册时使用 Path 对象的 register 方法:
Path dir = Paths.get("data/uploads");
dir.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE, // 创建文件/目录
StandardWatchEventKinds.ENTRY_DELETE, // 删除文件/目录
StandardWatchEventKinds.ENTRY_MODIFY // 修改文件/目录
);
说明:
- ENTRY_CREATE — 有人添加了某些内容。
- ENTRY_DELETE — 有人删除了某些内容。
- ENTRY_MODIFY — 有人修改了文件(例如追加了文本)。
重要! WatchService 一次只监视一个文件夹(不包含其子文件夹)。如果想监视整个层级——需要为每个子文件夹分别注册。
2. 事件处理:等待循环
现在我们已经设置好了监视(更像“邻居观察者”,而不是“老大哥”),就可以等待事件了。WatchService 实现了“事件队列”模式:一旦发生了什么——事件就会被放入队列。
主循环
while (true) {
// 等待事件出现(阻塞调用)
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
// 事件类型:创建、删除、修改
WatchEvent.Kind<?> kind = event.kind();
// 文件/文件夹名(Path,相对于被监视的目录)
Path filename = (Path) event.context();
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("已创建文件/目录: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("已删除文件/目录: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("已修改文件/目录: " + filename);
}
}
// 务必重置键,否则监视会停止!
boolean valid = key.reset();
if (!valid) {
break; // 文件夹不可用,退出
}
}
工作原理?
- WatchService.take() — 阻塞线程直到有事件出现(可使用 poll() 以非阻塞方式)。
- key.pollEvents() — 已积累的所有事件列表。
- event.context() — 发生变更的文件或文件夹名(相对于被监视的目录)。
- 处理完事件后务必调用 key.reset()。如果文件夹被删除或不可用,reset() 将返回 false——可以结束循环。
完整示例:监视文件夹 "data/uploads"
让我们在教学应用里给下载文件夹加一个简单的“报警器”:
import java.nio.file.*;
import java.io.IOException;
public class WatcherDemo {
public static void main(String[] args) throws IOException, InterruptedException {
Path dir = Paths.get("data/uploads");
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY
);
System.out.println("监视文件夹 " + dir.toAbsolutePath());
while (true) {
WatchKey key = watchService.take(); // 等待事件
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path filename = (Path) event.context();
System.out.printf("[%s] %s\n", kind.name(), filename);
}
boolean valid = key.reset();
if (!valid) {
System.out.println("文件夹不可用,监视已结束。");
break;
}
}
}
}
试一试:运行这段代码,然后在 "data/uploads" 文件夹中创建、删除或修改一个文件。程序会立即响应!
3. WatchService 的限制与特点
仅监视单个文件夹,不含子文件夹
WatchService 只监视你注册的那个文件夹。如果其中包含子文件夹,子文件夹里的变更不会被捕获——必须分别为每个子文件夹注册。
如何处理?
如果你需要监视整个目录层级,就要实现对子文件夹的遍历,并逐一注册它们。比如在创建新子文件夹时——立刻把它也注册上。
不同操作系统下的特点
Windows: WatchService 工作相当稳定,但有时会把多个事件“合并”为一个(例如复制大文件时)。
Linux/macOS: 实现基于系统机制(inotify、kqueue)。有时事件可能延迟到达,或者反过来过于频繁(例如每次保存都会触发 ENTRY_MODIFY)。
仅基于名称的事件
WatchService 只会报告发生变更对象的名称(相对于被监视的文件夹),并不会告诉你文件内部究竟改了什么。如果需要知道具体差异——请自行读取文件。
高负载时的事件丢失
如果在短时间内同一文件夹发生了过多变更(例如批量拷贝成千上万个文件),事件队列可能会被填满,从而丢失部分事件。对关键任务,建议配合额外校验。
4. 实用示例
自动处理新文件
假设你要编写一个程序,自动处理出现在 "photos/incoming" 文件夹中的新图片。
Path dir = Paths.get("photos/incoming");
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path filename = (Path) event.context();
if (filename.toString().endsWith(".jpg")) {
System.out.println("新照片: " + filename);
// 这里可以添加处理:复制、压缩、分析等。
}
}
}
key.reset();
}
实现简单的变更日志器
可以把所有事件写入单独的日志文件:
import java.nio.file.*;
import java.io.*;
import java.time.LocalDateTime;
public class SimpleLogger {
public static void main(String[] args) throws IOException, InterruptedException {
Path dir = Paths.get("logs/monitored");
Files.createDirectories(dir);
Path logFile = Paths.get("logs/changes.log");
try (BufferedWriter writer = Files.newBufferedWriter(logFile, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
System.out.println("正在监视 " + dir);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
String log = String.format("%s [%s] %s\n",
LocalDateTime.now(), event.kind().name(), event.context());
writer.write(log);
writer.flush();
System.out.print(log);
}
key.reset();
}
}
}
}
监视新子目录的创建(并注册它们)
如果在被监视的文件夹中创建了新的子目录,可以立即把它注册到监视服务中:
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path createdPath = dir.resolve((Path) event.context());
if (Files.isDirectory(createdPath)) {
createdPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("已开始监视新子目录: " + createdPath);
}
}
5. 重要细节与常见错误
错误 1:忘记调用 key.reset()。 如果在处理完事件后不重置键,文件夹的监视将会停止,你将不再收到任何事件。这是新手的经典“坑”:看起来一切都在运行,然后——啪!——程序就不出声了。
错误 2:忽略异常。 与文件系统打交道总是充满意外:文件夹可能被删除,磁盘——被拔出,权限——被修改。如果不处理异常(IOException、ClosedWatchServiceException),程序可能会异常终止。
错误 3:只监视了一个文件夹。 许多人以为注册了一个文件夹后,所有嵌套的文件夹也会被监视。并非如此!如果需要监视整个结构——请实现递归注册。
错误 4:阻塞了主线程。 WatchService.take() 会阻塞线程直到出现事件。如果主线程还要做其他事情,请在单独的线程中运行监视。
错误 5:高负载下的事件丢失。 如果一个文件夹在很短时间内发生大量变更,事件队列可能会被填满。对关键应用,建议实现周期性状态校验(例如每分钟比对一次文件列表)。
错误 6:错误处理相对路径。 event.context() 返回的是相对于被监视文件夹的文件名。如果需要绝对路径——请使用 dir.resolve((Path) event.context())。
GO TO FULL VERSION