Daniel Mitterdorfer

Watching File Changes with the JDK 7 WatchService

Have you ever needed to watch the file system for changes in a Java application? Java 7 ships with the WatchService API that is suited exactly for this use case as you might know. My journey with the WatchService API began a few months ago when I stumbled across this code I have originally written a few years ago:

//TODO: Update to JDK 7 and replace with native watcher
FileSystemManager fsManager = VFS.getManager();
watchedDirectory = fsManager.resolveFile(queueDirectory.getAbsolutePath());

DefaultFileMonitor fileMonitor = new DefaultFileMonitor(this);
fileMonitor.setRecursive(false);
fileMonitor.addFile(watchedDirectory);

In the snippet above I setup a Apache Commons VFS DefaultFileMonitor which checks for modification within the queueDirectory. The code is part of a Dropbox-based data synchronization I use for myself. Although there is nothing wrong with Apache Commons VFS, I use only a small portion of a quite large library and wanted to reduce the application's footprint by switching to native JDK facilities. Furthermore, Apache Commons VFS relies on polling whereas the Java 7 WatchService can use native kernel facilities if they are available on the platform. As Java 7 is available for quite some time, I decided to finally have a look at the WatchService API. I was not amused.

The Pain

As you can see above, the DefaultFileMonitor in Apache Commons VFS has a very straightforward API: You register for a directory to watch and receive change events afterwards. However, a look at the Javadoc of WatchService shows that Oracle has chosen a totally different approach. The code snippet below demonstrates how to watch for changes in a directory with the WatchService API. I simplified the example to its bare essentials: the code contains no precondition checks and no exception handling. Do not use this code for anything else than playing around with the API, I have warned you:

import java.io.File;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;

public class BasicPathWatcher {
    public static void main(String[] args) throws Exception {
        //the path to watch; we assume no recursive watching (would be even more complicated)
        Path pathToWatch = Paths.get(args[0]);
        WatchService ws = pathToWatch.getFileSystem().newWatchService(); // 1.
        // we do not care about the returned WatchKey in this simple example
        pathToWatch.register(ws, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); //2.
        boolean valid = true;
        while(valid) {
            WatchKey key = ws.take(); //3.
            for (WatchEvent <?> e: key.pollEvents()) { //4.
                WatchEvent.Kind<?> kind = e.kind();
                if (kind != OVERFLOW) {
                    WatchEvent<Path> event = (WatchEvent<Path>)e;
                    Path filename = event.context();
                    Path child = pathToWatch.resolve(filename);
                    System.out.println("Got event '" + kind + "' for path '" + child + "'");
                }
            }
            valid = key.reset(); //5.
        }
    }
}

Here is what the code does:

  1. Get the right WatchService. Many code examples get this step wrong by always obtaining the WatchService from the "default" file system. If the provided path is not on the "default" file system, the wrong WatchService is obtained. The correct way is to use Path#getFileSystem()#newWatchService() as shown in the example.
  2. Register a Path at the WatchService. You'll then get a WatchKey, which is a handle for events that you'll receive later on.
  3. Next, call WatchService#take() in a loop to get notified on file system changes. This call blocks until an event occurs.
  4. The call returns a WatchKey, which you then have to poll for events.
  5. Now you can handle the events and finally reset the WatchKey (or you won't get any further events). If you want to watch recursively and a directory has been created, you have to register the new directory again with the watch service.

Even for the simplest case, these are a lot of details that you have to get right in order to use the API correctly. A more robust implementation is as least twice as long.

There is a reason why the API is designed this way. Oracle engineer Alan Bateman gave an introductory talk on the WatchService API at Devoxx 2011 where he also described the API design (starts at around 45 minutes into the video).

This part of the API is deliberately a low-level interface

Alan Bateman at Devoxx 2011

In the video, he describes that the key decision was to design the interface very low-level to allow for many use-cases such as server applications or graphical applications.

Others must experience this pain too...

Although the API is hard to use, I still wanted to integrate it albeit not directly in the application. So I searched for libraries that abstract all the gory details. To my surprise I couldn't find a single one. These are the file watching libraries in Java I am aware of:

  • jpathwatch: It closely resembles the JDK 7 watch service API but requires only JDK 5. So I would "just" get backwards compatibility from jpathwatch but are still stuck with the same API.
  • jnotify: It has quite a simple API but relies on a native library.
  • Apache Commons VFS: Commons VFS was the API I began with. The API is simple but it is based on polling which I'd rather avoid for efficiency reasons.

There also exist a few Scala libraries but nobody seems to provide an abstraction library in plain Java.

Conclusion

I refuse to use such a complex low-level API directly in my application. Since Oracle designed the WatchService API as a low level API I would have expected that someone has written a library by now that abstracts the details. It's time to do something about that ...

Questions or comments?

Just ping me on Twitter