Java 监控文件系统的变化 (WatchService,WatchKey)

Java 从 JDK1.7 开始增加了 WatchService 类,可用于监控文件系统的变化。这是一种基于信号收发的监控,因此效率是比较高的。

WatchService

WatchService 就类似一个文件监视器,我们可以使用如下方法创建一个监视器:

1
WatchService watcher = FileSystems.getDefault().newWatchService();

创建完监视器后,需要将其绑定到某个目录,并指定监控哪些变化,如:

1
2
3
4
5
6
Kind[] kinds = {
StandardWatchEventKinds.ENTRY_CREATE, // 新增
StandardWatchEventKinds.ENTRY_MODIFY, // 修改
StandardWatchEventKinds.ENTRY_DELETE // 删除
};
WatchKey key = Paths.get("D:\\A").register(watcher, kinds);

WatchService 有两个读取文件变化的方法:

  • WatchKey poll();,直接返回,没有变化则返回空。

  • WatchKey take() throws InterruptedException;,阻塞式返回,没有变化会一直等待。

WatchKey

从上面的描述可以看出,注册 WatchService 到某个目录或者调用 WatchService 的读取 (pool, take) 方法都会得到 WatchKey 对象。这个对象包含了文件变化的事件集合。

调用 List<WatchEvent<?>> watchEventList = watchKey.pollEvents(); 方法可以获取变化内容。

注意的点

  1. Paths.get(path).register 方法只会监视 path 文件下的文件变化,其子目录的变化是不会监视的。

  2. WatchKey 每次读完文件变化后,需要调用 reset() 方法才能继续读取变化。

示例

下面示例展示监视目录和其子目录下的文件变化,并打印变化信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Test
public void test() throws Exception {
String rootPath = "D:\\A";
File root = new File(rootPath);
if (!root.isDirectory()) {
throw new Exception("只监视目录变化");
}
Set<String> pathSet = new LinkedHashSet<>(); // 所有目录集合
loopDir(root, pathSet); // 递归找子目录

Kind[] kinds = {
StandardWatchEventKinds.ENTRY_CREATE, // 新增
StandardWatchEventKinds.ENTRY_MODIFY, // 修改
StandardWatchEventKinds.ENTRY_DELETE // 删除
};
WatchService watcher = FileSystems.getDefault().newWatchService();

Map<WatchKey, String> watchKeyPathMap = new HashMap<>(); // 维护 WatchKey 与目录的映射,用于找到变化文件的目录。
for (String path : pathSet) { // 遍历添加监视
WatchKey key = Paths.get(path).register(watcher, kinds);
System.out.println("监视目录:" + path);
watchKeyPathMap.put(key, path);
}
while (true) {
WatchKey watchKey = watcher.take();
List<WatchEvent<?>> watchEventList = watchKey.pollEvents();
for (WatchEvent<?> watchEvent : watchEventList) {
String msg = String.format("变化类型:%s,监视目录:%s,变化对象:%s",
watchEvent.kind().name(),
watchKeyPathMap.get(watchKey),
watchEvent.context().toString()
);
System.out.println(msg);
}

boolean b = watchKey.reset();
if (!b) {
break;
}
}

watcher.close();
}

// 递归寻找子目录
private void loopDir(File parent, Set<String> pathSet) {
if (!parent.isDirectory()) {
return;
}
pathSet.add(parent.getPath());
for (File child : parent.listFiles()) {
loopDir(child, pathSet);
}
}

// 输出:
监视目录:D:\A
监视目录:D:\A\B
监视目录:D:\A\B\D
监视目录:D:\A\C
变化类型:ENTRY_CREATE,监视目录:D:\A\B,变化对象:新建文本文档.txt
变化类型:ENTRY_MODIFY,监视目录:D:\A\B,变化对象:新建文本文档.txt
变化类型:ENTRY_MODIFY,监视目录:D:\A,变化对象:B
变化类型:ENTRY_DELETE,监视目录:D:\A\B,变化对象:新建文本文档.txt
变化类型:ENTRY_MODIFY,监视目录:D:\A,变化对象:B