CodeGym /Courses /JAVA 25 SELF /Monitoring file system changes: WatchService

Monitoring file system changes: WatchService

JAVA 25 SELF
Level 40 , Lesson 4
Available

1. Introduction

WatchService is part of Java NIO (New I/O) that lets you track file system changes in real time. You can think of it as an alarm system for folders: as soon as someone adds, deletes, or modifies a file, you immediately get a notification. This capability arrived in Java 7 along with NIO.2; before that, developers had to either poll a directory manually (polling) or use third-party libraries.

There are many practical uses for WatchService: it helps automatically process new files, keep logs and make backups, sync folders with a server or the cloud, and track changes in configuration files.

Registering directories to watch

To start watching for changes, you need to:

  1. Obtain a WatchService instance.
  2. Register the desired folder and specify which events you care about.

Getting WatchService

import java.nio.file.*;

WatchService watchService = FileSystems.getDefault().newWatchService();

Registering the directory

To register, use the register method on a Path object:

Path dir = Paths.get("data/uploads");
dir.register(
    watchService,
    StandardWatchEventKinds.ENTRY_CREATE,   // file/directory creation
    StandardWatchEventKinds.ENTRY_DELETE,   // file/directory deletion
    StandardWatchEventKinds.ENTRY_MODIFY    // file/directory modification
);

Explanation:

  • ENTRY_CREATE — something was added.
  • ENTRY_DELETE — something was removed.
  • ENTRY_MODIFY — a file was modified (for example, text was appended).

Important! WatchService watches only one directory at a time (no subdirectories). If you want to watch the entire hierarchy, you need to register each subdirectory separately.

2. Handling events: the wait loop

Now that we’ve set up watching (more like a “nosy neighbor” than “Big Brother”), we can wait for events. WatchService implements an “event queue” pattern: as soon as something happens, an event is placed into the queue.

Main loop

while (true) {
    // Wait for events (blocking call)
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        // Event type: create, delete, modify
        WatchEvent.Kind<?> kind = event.kind();

        // File/directory name (Path, relative to the watched directory)
        Path filename = (Path) event.context();

        if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
            System.out.println("File/directory created: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
            System.out.println("File/directory deleted: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
            System.out.println("File/directory modified: " + filename);
        }
    }

    // Always reset the key, otherwise watching will stop!
    boolean valid = key.reset();
    if (!valid) {
        break; // Directory is unavailable; exit
    }
}

How it works

  • WatchService.take() — blocks the thread until an event appears (you can use poll() for non-blocking mode).
  • key.pollEvents() — the list of all accumulated events.
  • event.context() — the name of the changed file or directory (relative to the watched directory).
  • After processing events, be sure to call key.reset(). If the directory was deleted or became unavailable, reset() returns false — you can terminate the loop.

Complete example: watching the "data/uploads" directory

Let’s add a simple “alarm” to the uploads directory in our training application:

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("Watching directory " + dir.toAbsolutePath());

        while (true) {
            WatchKey key = watchService.take(); // wait for events

            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("Directory unavailable, monitoring stopped.");
                break;
            }
        }
    }
}

Try it: Run this code and try creating, deleting, or modifying a file in the "data/uploads" directory. The program will react immediately!

3. Limitations and specifics of WatchService

Only one directory, no subdirectories

WatchService watches only the directory you register. If it contains subdirectories, changes inside them won’t be noticed — you must register each subdirectory separately.

What to do?
If you want to watch an entire hierarchy, you’ll need to traverse all subdirectories and register them one by one. For example, when a new subdirectory is created — register it immediately.

Platform specifics

Windows: WatchService works fairly reliably, but it can occasionally coalesce several events into one (for example, when copying a large file).

Linux/macOS: The implementation is based on system mechanisms (inotify, kqueue). Sometimes events may arrive with a delay, or conversely, there may be too many of them (for example, ENTRY_MODIFY on every save).

Name-only events

WatchService reports only the name of the changed object (relative to the watched directory) and doesn’t provide full information about what changed inside the file. If you need to know exactly what changed, read the file yourself.

Event loss under heavy load

If too many changes occur in a directory over a short period (for example, bulk copying of thousands of files), the event queue may overflow and some events will be lost. For critical tasks, consider adding additional checks.

4. Practical examples

Automatic processing of new files

Suppose you’re writing a program that should automatically process new images appearing in the "photos/incoming" directory.

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("New photo: " + filename);
                // You can add processing here: copy, compression, analysis, etc.
            }
        }
    }
    key.reset();
}

Implementing a simple change logger

You can persist all events to a separate log file:

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("Watching " + 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();
            }
        }
    }
}

Watching creation of new subdirectories (and registering them)

If a new subdirectory is created in the watched directory, you can immediately register it for further monitoring:

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("Started watching new subdirectory: " + createdPath);
    }
}

5. Important nuances and common mistakes

Mistake #1: forgot to call key.reset(). If you don’t reset the key after processing events, watching the directory will stop and you won’t receive any further events. This is a classic gotcha for beginners: it looks like everything works, and then — bam! — the program goes silent.

Mistake #2: ignoring exceptions. Working with the file system is always full of surprises: the directory may be deleted, the disk detached, permissions changed. If you don’t handle exceptions (IOException, ClosedWatchServiceException), the program may terminate abnormally.

Mistake #3: watching only a single directory. Many expect that registering a directory will also watch all nested directories. It won’t! If you need to watch the entire tree, implement recursive registration.

Mistake #4: blocking the main thread. WatchService.take() blocks the thread until an event appears. If the main thread of the program should do something else, run watching in a separate thread.

Mistake #5: event loss under high load. If too many changes occur in the directory, the event queue may overflow. For critical applications, implement periodic reconciliation of the directory state (for example, compare the file list once a minute).

Mistake #6: incorrect handling of relative paths. event.context() returns the file name relative to the watched directory. If you need an absolute path, use dir.resolve((Path) event.context()).

1
Task
JAVA 25 SELF, level 40, lesson 4
Locked
Setting up a vigilant watcher for a new folder 👁️‍🗨️
Setting up a vigilant watcher for a new folder 👁️‍🗨️
1
Task
JAVA 25 SELF, level 40, lesson 4
Locked
Monitoring modifications and deletions in the critical directory 🚨
Monitoring modifications and deletions in the critical directory 🚨
1
Survey/quiz
Directory operations, level 40, lesson 4
Unavailable
Directory operations
File and directory operations
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION