Skip to content

WatchService 文件监听

你做过热重载功能吗?当源代码改变时,程序自动重新加载。

这背后的原理就是文件系统监听——操作系统会告诉我们「某个目录下的文件变了」。

JDK 7 的 WatchService 就是干这个的。

基本用法

三步走:

java
// 1. 创建 WatchService
WatchService ws = FileSystems.getDefault().newWatchService();

// 2. 注册要监听的目录
Path dir = Path.of("watched_dir");
dir.register(ws,
    StandardWatchEventKinds.ENTRY_CREATE,  // 文件创建
    StandardWatchEventKinds.ENTRY_MODIFY,  // 文件修改
    StandardWatchEventKinds.ENTRY_DELETE);  // 文件删除

// 3. 阻塞等待事件
while (true) {
    WatchKey key = ws.take(); // 阻塞直到有事件
    for (WatchEvent<?> event : key.pollEvents()) {
        Path changed = (Path) event.context();
        System.out.println(event.kind() + ": " + changed);
    }
    key.reset(); // 必须重置,继续监听
}

监听类型

事件类型说明
ENTRY_CREATE文件被创建(或移动进)
ENTRY_MODIFY文件被修改
ENTRY_DELETE文件被删除(或移动出)
OVERFLOW事件溢出(系统事件太多,可能丢失)

注意:不同操作系统对 ENTRY_MODIFY 的敏感度不同。Windows 通常一次修改只触发一次,但 Linux 可能会触发多次。

完整示例:热重载配置

java
public class ConfigWatcher {
    private final WatchService watcher;
    private final Map<WatchKey, Path> keys = new HashMap<>();

    public ConfigWatcher(Path dir) throws IOException {
        this.watcher = FileSystems.getDefault().newWatchService();
        registerAll(dir);
    }

    private void registerAll(Path dir) throws IOException {
        // 递归注册所有子目录
        Files.walk(dir)
            .filter(Files::isDirectory)
            .forEach(p -> {
                try {
                    WatchKey key = p.register(watcher,
                        StandardWatchEventKinds.ENTRY_CREATE,
                        StandardWatchEventKinds.ENTRY_MODIFY,
                        StandardWatchEventKinds.ENTRY_DELETE);
                    keys.put(key, p);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
    }

    public void processEvents() {
        while (true) {
            WatchKey key;
            try {
                key = watcher.take();
            } catch (InterruptedException e) {
                return;
            }

            Path dir = keys.get(key);
            if (dir == null) continue;

            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();

                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    continue;
                }

                @SuppressWarnings("unchecked")
                WatchEvent<Path> ev = (WatchEvent<Path>) event;
                Path filename = ev.context();
                Path child = dir.resolve(filename);

                System.out.println(kind + ": " + child);

                // 如果是子目录被创建,注册它
                if (kind == StandardWatchEventKinds.ENTRY_CREATE &&
                    Files.isDirectory(child)) {
                    registerAll(child);
                }

                // 如果是配置文件被修改,热重载
                if (kind == StandardWatchEventKinds.ENTRY_MODIFY &&
                    filename.toString().endsWith(".properties")) {
                    reloadConfig(child);
                }
            }

            key.reset();
        }
    }

    private void reloadConfig(Path configFile) {
        System.out.println("热重载: " + configFile);
        // 重新加载配置...
    }
}

使用场景

  • 热重载:配置文件、代码修改后自动重载
  • 文件同步:监听源目录,同步到目标目录
  • 自动化构建:检测文件变化,自动触发构建
  • 内容索引:检测文件变化,更新搜索索引

注意事项

  1. OVERFLOW 处理:事件太多时会触发这个事件,可能丢失部分事件。需要根据场景决定如何处理(忽略、重启监听等)。

  2. 必须调用 reset():不重置的话,这个 key 就不会再响应后续事件了。

  3. 只支持目录:WatchService 监听的是目录变化,不是单个文件。被监听目录下的文件变化会触发事件。

  4. 跨平台差异:macOS 有时需要额外配置。


WatchService 让你的程序拥有了「耳朵」,听得到文件系统的变化。

下一节,我们来总结 NIO.2 的全部内容。

基于 VitePress 构建