Skip to Content
Course content

207: Watching Directories with WatchService Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why am I getting multiple MODIFY events for a single file save?

This is probably the first thing that drove me crazy when I started using WatchService. You save a file in your IDE once, and your console screams that the file was modified three times in a row. It feels like a bug in Java, but it's actually how the operating system and your text editor interact.

Most modern editors don't just overwrite a file. They create a temporary file, write the changes there, delete the original, and then rename the temporary file. To the WatchService, that's a flurry of activity. Even a simple save can trigger a CREATE, a MODIFY, and then another MODIFY as the OS updates the file's metadata (like the "last modified" timestamp).

I usually handle this by implementing a simple "cooldown" or debouncing mechanism. Instead of reacting immediately, I store the filename and the timestamp of the event in a map. If another event for the same file arrives within, say, 100 milliseconds, I ignore it. Here is the conceptual approach:

Map<Path, Long> lastEventTime = new HashMap<>();
long cooldown = 100; // milliseconds

while (true) {
    WatchKey key = watcher.take();
    for (WatchEvent<Path> event : key.pollEvents()) {
        Path fileName = event.context();
        long now = System.currentTimeMillis();
        
        if (now - lastEventTime.getOrDefault(fileName, 0L) > cooldown) {
            System.out.println("Actually processing change for: " + fileName);
            lastEventTime.put(fileName, now);
            // Do the heavy lifting here
        }
    }
    key.reset();
}

What happens when I see an OVERFLOW event?

If you're monitoring a directory where thousands of files are being dumped in a matter of seconds—maybe a log directory or a temporary build folder—you'll eventually encounter StandardWatchEventKinds.OVERFLOW. This is the WatchService basically admitting, "I can't keep up; I've dropped some events."

You can't "recover" the missed events because they're gone from the buffer. When I hit an overflow, the only safe move is to trigger a full manual scan of the directory. I usually write a method that lists all files in the watched folder and compares them against my internal state to find out what changed while the service was overwhelmed. Don't try to guess what you missed; just re-sync everything.

How do I make the watcher recursive?

Here is the frustrating part: WatchService is not recursive. If you register /my-project, you'll hear about files added to that folder, but you'll be completely deaf to anything happening in /my-project/src/main/java. I've seen a lot of developers assume this works out of the box, only to spend hours wondering why their subfolders are ignored.

To fix this, you have to manually register every single subdirectory. The cleanest way to do this is using Files.walkFileTree. You walk the tree, find every directory, and call register() on each one. Just keep in mind that if a new directory is created while the program is running, you need to register that new directory on the fly, or it will also be ignored.

Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, 
                    StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
        return FileVisitResult.CONTINUE;
    }
});

I'll give you a warning: be careful with this on massive file systems. Every registered directory consumes OS resources. If you try to watch a root drive with 100,000 folders, you might hit an OS limit or tank your performance.




📋 Practical Task

Exercise: Building a Hot-Reloading Config Monitor

Create a program that monitors a specific folder named /config for changes to a file named settings.properties. The program should:

  • Print "Reloading configuration..." whenever the file is modified.
  • Implement a 200ms cooldown (debounce) mechanism to ensure that a single save operation doesn't trigger the "Reloading" message multiple times.
  • Gracefully handle the OVERFLOW event by printing a warning that a manual resync is required.
  • Run in a separate thread so that the main application remains responsive.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.