CodeGym /课程 /JAVA 25 SELF /文件系统变更监视:WatchService

文件系统变更监视:WatchService

JAVA 25 SELF
第 40 级 , 课程 4
可用

1. 引言

WatchService 是 Java NIO(New IO)的一部分,可用于实时监视文件系统中的变更。你可以把它想象成文件夹的报警器:只要有人添加、删除或修改文件——你就会立即收到通知。该能力在 Java 7 中随 NIO.2 引入,在此之前,开发者要么手动轮询文件夹(polling),要么使用第三方库。

WatchService 的实际用途十分广泛:它可以自动处理新文件、记录日志并进行备份、将文件夹与服务器或云同步,以及监视配置文件的变更。

为监视注册目录

要开始监视变更,需要:

  1. 获取一个 WatchService 实例。
  2. 注册需要的文件夹并指定关心的事件类型。

获取 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:忽略异常。 与文件系统打交道总是充满意外:文件夹可能被删除,磁盘——被拔出,权限——被修改。如果不处理异常(IOExceptionClosedWatchServiceException),程序可能会异常终止。

错误 3:只监视了一个文件夹。 许多人以为注册了一个文件夹后,所有嵌套的文件夹也会被监视。并非如此!如果需要监视整个结构——请实现递归注册。

错误 4:阻塞了主线程。 WatchService.take() 会阻塞线程直到出现事件。如果主线程还要做其他事情,请在单独的线程中运行监视。

错误 5:高负载下的事件丢失。 如果一个文件夹在很短时间内发生大量变更,事件队列可能会被填满。对关键应用,建议实现周期性状态校验(例如每分钟比对一次文件列表)。

错误 6:错误处理相对路径。 event.context() 返回的是相对于被监视文件夹的文件名。如果需要绝对路径——请使用 dir.resolve((Path) event.context())

1
调查/小测验
目录操作第 40 级,课程 4
不可用
目录操作
文件和目录的操作
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION