Java WatchService: Native Filesystem Change Monitoring
WatchService, part of Java NIO.2 (introduced in Java 7), lets you monitor directories for file system events (creation, modification, and deletion) using native OS mechanisms (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows). It replaces slow, polling-based file monitors with an event-driven, low-overhead approach.
Contents
Basic: Watching a Single Directory
A WatchServiceFileSystems.getDefault().newWatchService()PathENTRY_CREATEENTRY_MODIFYENTRY_DELETEpoll()poll(timeout, unit)take()WatchService
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
// Create a WatchService from the default FileSystem
WatchService watchService = FileSystems.getDefault().newWatchService();
Path dir = Path.of("/tmp/watched");
// Register the directory and the event types to watch
WatchKey key = dir.register(watchService,
ENTRY_CREATE, // a new file/directory was created
ENTRY_DELETE, // a file/directory was deleted
ENTRY_MODIFY); // a file was modified
System.out.println("Watching: " + dir);
// Poll for events (non-blocking)
WatchKey polled = watchService.poll(); // returns null immediately if no events
// Poll with timeout
WatchKey polledTimeout = watchService.poll(1, TimeUnit.SECONDS);
// Block until an event occurs (no timeout)
WatchKey blocking = watchService.take(); // blocks until event
// Always close the WatchService when done
watchService.close();
Event Types and WatchKey
WatchKeyregister()take()poll()WatchEventpollEvents()reset()OVERFLOW
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
// The three standard event kinds:
// ENTRY_CREATE — file or directory created in the watched directory
// ENTRY_DELETE — file or directory deleted from the watched directory
// ENTRY_MODIFY — file in the watched directory was modified
// OVERFLOW — events may have been lost (system-level event queue overflow)
// WatchEvent carries the kind and context (the relative path of the changed file)
WatchService ws = FileSystems.getDefault().newWatchService();
Path dir = Path.of("/var/log/app");
dir.register(ws, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
WatchKey key = ws.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) {
System.out.println("Events may have been lost!");
continue;
}
// The context is the relative path of the affected file
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path filename = pathEvent.context(); // relative: "app.log"
Path fullPath = dir.resolve(filename); // absolute: "/var/log/app/app.log"
if (kind == ENTRY_CREATE) System.out.println("Created: " + fullPath);
if (kind == ENTRY_DELETE) System.out.println("Deleted: " + fullPath);
if (kind == ENTRY_MODIFY) System.out.println("Modified: " + fullPath);
}
// IMPORTANT: reset the key after processing — otherwise it won't receive more events
boolean valid = key.reset();
if (!valid) {
System.out.println("Directory no longer accessible");
}
key.reset()WatchKeyEvent Processing Loop
The standard watch loop calls take()poll(timeout)WatchKeykey.pollEvents()event.context()key.reset()false
class DirectoryWatcher implements Runnable, AutoCloseable {
private final WatchService watchService;
private final Path directory;
private volatile boolean running = true;
DirectoryWatcher(Path directory) throws IOException {
this.directory = directory;
this.watchService = FileSystems.getDefault().newWatchService();
directory.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
}
@Override
public void run() {
while (running) {
WatchKey key;
try {
key = watchService.poll(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
if (key == null) continue; // timeout — check running flag and loop
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == OVERFLOW) continue;
@SuppressWarnings("unchecked")
Path file = directory.resolve(((WatchEvent<Path>) event).context());
handleEvent(event.kind(), file);
}
if (!key.reset()) {
System.out.println("Watch key invalid — stopping");
break;
}
}
}
private void handleEvent(WatchEvent.Kind<?> kind, Path file) {
if (kind == ENTRY_CREATE) System.out.println("Created: " + file);
else if (kind == ENTRY_DELETE) System.out.println("Deleted: " + file);
else if (kind == ENTRY_MODIFY) System.out.println("Modified: " + file);
}
public void stop() { running = false; }
@Override
public void close() throws IOException {
running = false;
watchService.close();
}
}
// Usage
try (DirectoryWatcher watcher = new DirectoryWatcher(Path.of("/tmp/watched"))) {
Thread.ofVirtual().start(watcher);
Thread.sleep(60_000); // watch for 60 seconds
} // close() called automatically, stops the watcher
Recursive Directory Watching
WatchService watches a single directory, not its subdirectories. For recursive watching, register each subdirectory individually and handle new directories as they're created:
class RecursiveWatcher {
private final WatchService watchService;
private final Map<WatchKey, Path> keyToPath = new HashMap<>();
RecursiveWatcher() throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
}
// Register a directory and all its subdirectories
void registerAll(Path root) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
keyToPath.put(key, dir);
System.out.println("Registered: " + dir);
}
void processEvents() throws IOException, InterruptedException {
while (true) {
WatchKey key = watchService.take();
Path parent = keyToPath.get(key);
if (parent == null) continue;
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == OVERFLOW) continue;
@SuppressWarnings("unchecked")
Path child = parent.resolve(((WatchEvent<Path>) event).context());
// If a new directory was created, register it too
if (event.kind() == ENTRY_CREATE && Files.isDirectory(child)) {
registerAll(child); // recursively register new subdirectory
}
System.out.println(event.kind().name() + ": " + child);
}
if (!key.reset()) {
keyToPath.remove(key);
if (keyToPath.isEmpty()) break;
}
}
}
}
Practical: Config File Reloader
A common real-world use case is hot-reloading a configuration file without restarting the application. Since WatchService
// Auto-reload configuration when the file changes
class HotReloadConfig implements AutoCloseable {
private final Path configFile;
private final WatchService watchService;
private volatile Properties props = new Properties();
private final Thread watchThread;
HotReloadConfig(Path configFile) throws IOException {
this.configFile = configFile;
this.watchService = FileSystems.getDefault().newWatchService();
// Watch the parent directory (WatchService works on directories, not files)
configFile.getParent().register(watchService, ENTRY_MODIFY);
loadConfig();
// Start background watcher using virtual thread
this.watchThread = Thread.ofVirtual().start(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == OVERFLOW) continue;
@SuppressWarnings("unchecked")
Path changed = configFile.getParent()
.resolve(((WatchEvent<Path>) event).context());
if (changed.equals(configFile)) {
System.out.println("Config changed — reloading...");
loadConfig();
}
}
key.reset();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
System.err.println("Config reload error: " + e.getMessage());
}
});
}
private void loadConfig() throws IOException {
Properties p = new Properties();
try (var reader = Files.newBufferedReader(configFile)) {
p.load(reader);
}
this.props = p; // atomic replace (volatile write)
System.out.println("Config loaded: " + p.size() + " properties");
}
public String get(String key) { return props.getProperty(key); }
@Override
public void close() throws IOException {
watchThread.interrupt();
watchService.close();
}
}