Contents
- Basic: Watching a Single Directory
- Event Types and WatchKey
- Event Processing Loop
- Recursive Directory Watching
- Practical: Config File Reloader
A WatchService is obtained from FileSystems.getDefault().newWatchService(). A directory Path is then registered with the service, specifying which event kinds to watch: ENTRY_CREATE, ENTRY_MODIFY, or ENTRY_DELETE. To retrieve events, call poll() for a non-blocking check, poll(timeout, unit) to wait up to a duration, or take() to block until an event arrives. The WatchService itself must be closed when no longer needed.
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();
WatchKey is returned by register() and also by take() or poll() when events arrive. It holds the list of pending WatchEvent objects retrieved via pollEvents(). After processing all events, reset() must be called on the key to put it back into the ready state — if you forget, the key is cancelled and no further events for that directory will be delivered. The OVERFLOW event kind indicates that events may have been lost because the OS event queue was full; always handle it explicitly.
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");
}
Always call key.reset() after processing each WatchKey's events. If you don't reset the key, it will be in the SIGNALLED state and never queued again — you'll miss all subsequent events for that directory.
The standard watch loop calls take() (blocking) or poll(timeout) (non-blocking, better for graceful shutdown) to wait for the next WatchKey. Once a key arrives, iterate key.pollEvents() to process all pending events; get the affected path with event.context(), which is relative to the watched directory, then resolve it against the directory path to get the absolute path. Always call key.reset() at the end of each iteration; if it returns false, the watched directory is no longer accessible and the loop should break.
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
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;
}
}
}
}
A common real-world use case is hot-reloading a configuration file without restarting the application. Since WatchService watches directories rather than individual files, register the parent directory and filter events by comparing the event's context path to the config file name. When a match is found, reload the configuration atomically. Run the event loop in a daemon thread so it does not prevent JVM shutdown when the main application exits.
// 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();
}
}
WatchService uses native OS filesystem notifications, so it's much more efficient than polling — no wasted CPU scanning directories repeatedly. On Linux it uses inotify, on macOS kqueue, and on Windows ReadDirectoryChangesW.