目錄
前言
問題
遞歸監控子目錄
windows系統
跨平台解決方案
跨平台多級目錄監控方案
WatchService的其他注意事項
前言
java7+提供了WatchService類,可以用來實作對檔案增删改的監控,demo很簡單,代碼如下:
package com.test.filewatch;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
/**
* @Author:
* @Date: 2021/8/25
* @TIME: 17:53
*/
public class FileWatcherTest2 {
public static void main(String[] args) throws IOException, InterruptedException {
String watchPathStr = args[0];
Path watchPath = Paths.get(watchPathStr).toAbsolutePath();
WatchService watchService = FileSystems.getDefault().newWatchService();
watchPath.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW);
while (true) {
System.out.println("等待change event");
WatchKey watchKey = watchService.take();
List<WatchEvent<?>> watchEvents = watchKey.pollEvents();
if (watchEvents != null) {
for (WatchEvent<?> watchEvent : watchEvents) {
if (watchEvent.kind() == OVERFLOW) {
//檔案變更的事件可能丢失
System.out.println("overflow,count=" + watchEvent.count() + ",context=" + watchEvent.context());
} else if (watchEvent.kind() == ENTRY_CREATE) {
//建立檔案(夾)
System.out.println("create,count=" + watchEvent.count() + ",context=" + watchEvent.context());
} else if (watchEvent.kind() == ENTRY_DELETE) {
//删除檔案(夾)
System.out.println("delete,count=" + watchEvent.count() + ",context=" + watchEvent.context());
} else if (watchEvent.kind() == ENTRY_MODIFY) {
//更新檔案(夾)
System.out.println("modify,count=" + watchEvent.count() + ",context=" + watchEvent.context());
} else {
//理論上不會執行到這一步
System.out.println("不識别的kind類型" + watchEvent.kind().name());
}
}
}
//每次pollEvents處理完後,必須reset,否則無法繼續捕獲事件
if (!watchKey.reset())
System.out.println(watchKey.watchable() + "無法繼續監聽了....");
}
}
}
問題
上面的demo,隻能用來監控一個dir下所有子檔案(夾)的變化,但是捕獲不到子檔案内部檔案的變化情況。
遞歸監控子目錄
windows系統
在注冊WatchKey時,有兩個重載的方法可供選擇,上面的demo是一種,還有一種如下圖所示:
但是, Modifier在JavaSE API Doc中并沒有具體的實作,這說明不同的JDK版本,甚至不同的平台可能有各自特有的值。
windows平台下,可以通過下面的注冊方式實作對目錄的遞歸監控:
watchPath.register(watchService,
new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW},
ExtendedWatchEventModifier.FILE_TREE //NOTE:這個變量是windows平台特有,在linux上無法調用
);
跨平台解決方案
跨平台解決方案,在百度中也能找到,stackoverflow上也有介紹,原理就是通過root path的watch event,發現CREATE事件,且被建立的時檔案夾時,自動向WatchService注冊被建立的檔案,demo如下:
WatchKey watchKey = this.watchService.take();
Path changePath = (Path) watchKey.watchable();
LOG.debug("[{}]發生了變動", changePath);
List<WatchEvent<?>> watchEvents = watchKey.pollEvents();
if (watchEvents != null && watchEvents.size() > 0) {
for (WatchEvent<?> watchEvent : watchEvents) {
if (watchEvent.kind() == OVERFLOW) {
fileChangeEventHeapMq.produce(new WatchServiceEvent(OVERFLOW, changePath));
} else if (watchEvent.kind() == ENTRY_CREATE) {
Path createPath = changePath.resolve((Path) watchEvent.context());
boolean isDir = Files.isDirectory(createPath);
if (Files.isDirectory(createPath)) {
//如果create的是檔案夾,還需要監聽這個檔案夾
LOG.debug("file watcher build on path={}", createPath.toAbsolutePath());
createPath.register(this.watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.OVERFLOW);
}
this.fileChangeEventHeapMq.produce(new WatchServiceEvent(ENTRY_CREATE, createPath,isDir));
} else if (watchEvent.kind() == ENTRY_MODIFY) {
Path createPath = changePath.resolve((Path) watchEvent.context());
this.fileChangeEventHeapMq.produce(new WatchServiceEvent(ENTRY_MODIFY, createPath));
} else if (watchEvent.kind() == ENTRY_DELETE) {
Path createPath = changePath.resolve((Path) watchEvent.context());
this.fileChangeEventHeapMq.produce(new WatchServiceEvent(ENTRY_DELETE, createPath));
}
}
}
if (!watchKey.reset()) {
LOG.info("[{}]的監聽已經無效,可能是由于檔案被删除", changePath);
}
這就是網上大部分人的解決方案,但這個方案存在一個bug。
跨平台多級目錄監控方案
上面一節中的遞歸監控,存在一個bug,描述如下:
step.1:dirA被建立。
step.2:rootPath的CREATE event被觸發,開始向WatchService注冊dirA。
step.3:dirA下的檔案FileA被建立,此時由于dirA還沒有注冊成功,是以不會觸發任何邏輯,FileA CREATE event丢失,甚至連OVERFLOW event都不會觸發。
step.4:dirA的注冊邏輯執行結束。
step.5:dirA下的檔案FileB被建立,此時被dirA的WatchService捕獲,觸發FileB CREATE event。
從上面的描述就能看出,由于我們的代碼與File System是獨立的,無法實作注冊過程中,阻塞檔案系統的write操作,是以dirA的注冊過程持續期間,dirA下所有的變化都會丢失。
針對問題産生的原因,解決方案有很多,目前本人在做NFS的java實作,我的解決方案是:
對于NFS server端:
1、維護一個concurrent set集合,記錄所有注冊的dir名稱,同一個dir隻允許注冊一次。
2、當出現dir CREATE event時,不僅要注冊dir,同時還要周遊dir下所有的子目錄,對這些目錄也進行注冊。
3、使用生産者消費者模型,使得event的消費交給獨立的線程,線程進行event合并,當出現dir CREATE event時,自動對後續到來的event進行判斷,如果後續event的所屬path是dir的子檔案(夾),則忽略後續的event。
對于NFS client端:
1、如果需要同步的是dir,且事件類型是CREATE,則直接同步整個檔案夾的所有内容。
WatchService的其他注意事項
1、 WatchService對同一個檔案的同一次操作,可能産生多次event(例如上傳.jar檔案時,如果檔案很大,會産生N次MODIFY event) 。
2、處理OVERFLOW事件,api doc原文:A special event to indicate that events may have been lost or discarded。