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);
}
}
// 一定要重設 key,否則監控會停止!
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()。 如果在處理完事件後沒有重設 key,目錄監控就會停止,你也不會再收到任何事件。這是新手常見的「陷阱」:看起來一切都正常,結果——砰!——程式就沉默了。
錯誤 #2:忽略例外。 檔案系統充滿各種意外:目錄可能被刪除、磁碟可能被拔除、權限可能被變更。如果不處理例外(如 IOException、ClosedWatchServiceException),程式可能會非預期終止。
錯誤 #3:只監控單一目錄。 很多人以為註冊一個目錄後,所有子目錄也會被監控。不是這樣!如果需要監控整個結構——請實作遞迴式註冊。
錯誤 #4:阻塞主執行緒。 WatchService.take() 會阻塞執行緒直到事件出現。如果主執行緒還需要做其他事,請在獨立的執行緒中啟動監控。
錯誤 #5:高負載下遺失事件。 如果目錄在短時間內發生太多變更,事件佇列可能會被塞滿。對於關鍵應用,建議實作定期比對目錄狀態(例如每分鐘比對一次檔案清單)。
錯誤 #6:錯誤處理相對路徑。 event.context() 回傳的是相對於被監控目錄的檔名。如果需要絕對路徑——請使用 dir.resolve((Path) event.context())。
GO TO FULL VERSION