Polling WatchService for file modifications
On the previous page, we explained how to
set up a WatchService to listen for file modifications. Now we are ready to
actually poll the WatchService for those modifications. Polling for modifications
happens in the following stages:
- we repeatedly poll the WatchService for the next WatchKey on which
there are modifications to report (recall that a WatchKey effectively represents
a directory being watched, though if we want to actually associate the WatchKey
with the original identity of the directory, it is up to us to do so at the point of
registering it for notifications);
- for each WatchKey returned by the WatchService, we then poll that
key for the actual pending notifications;
- after we have read through the notifications for a given WatchKey (directory),
we then "re-queue" that key if we want to go on receiving notifications for the directory
in question.
In a typical application, this whole process will take place in a loop in a separate
thread. If you are not familiar with threading in
Java, then you may wish to take the opportunity to read up on this topic. We will provide some
very simple boilerplate code below as an example of watching in a separate thread and will mention
the main threading issues you need to be aware of.
Polling WatchService
Our outer loop consists of a repeated call to poll the watchService for the next
key (directory) for which there are pending notification events. The basic loop typically looks as
follows:
public class MyWatcher {
private final WatchService watcher = ...set up as above...
private void processFileNotifications() throws InterruptedException {
while (true) {
WatchKey key = watcher.take();
// process events for this "key" (directory)
}
}
}
Note that this loop will not consume CPU while there are no pending notifications.
Instead, the take() method will block, meaning that the thread will sleep pending any
incoming notification. In other words, WatchService works very much like
a Java BlockingQueue.
The take() method and its blocking behaviour are usually what is required.
If you need them, WatchService
also provides methods allowing you to poll for events with a timeout, or poll() returning immediately
with whether or not events are available. If you use the latter functionality, it is up to
your application to ensure that it doesn't call WatchService.poll() continuously and
hence burn CPU.
From this example, you will notice that we store a reference to our WatchService
in a final variable for thread-safety: typically,
the service will be set up in one thread but then processFileNotifications()
called from a different thread.
Retrieving individual modification notifications
Whenever the watcher.take() "wakes up", we need to then poll the WatchKey
passed to us for the actual modification notifications. To this end, we call
the WatchKey's pollEvents() method. This returns a list of WatchEvent
objects which we can cycle through:
private void processFileNotifications() throws InterruptedException {
while (true) {
WatchKey key = watcher.take();
Path dir = keyPaths.get(key);
for (WatchEvent> evt : key.pollEvents()) {
WatchEvent.Kind> eventType = evt.kind();
if (eventType == OVERFLOW)
continue;
Object o = evt.context();
if (o instanceof Path) {
Path path = (Path) o;
process(dir, path, eventType);
}
}
key.reset();
}
}
private void process(Path dir, Path file, WatchEvent.Kind evtType) {
...
}
The first thing we do is query our previously-created keyPaths map to find out
which directory corresponds to this key. Notifications are sent to us in terms of
relative paths, so if we want to know the parent directory, we must record it
explicitly at the time of registering for notifications as in our example above.
We then check the event type, represented by a WatchEvent.Kind object. This could
be one of the three event types registered for— CREATION, DELETION, MODIFICATION—
or the special OVERFLOW type. An OVERFLOW event in effect means that the WatcherService
received so many notifications from the filing system that it couldn't process them all in time. It tells
our application that "you've missed some events", but what we do about this in practice will depend on our
application. Here, we simply skip passed other events returned to us: the logic being that if we know we've
missed some events and cannot keep up, we may as well skip the current queue and start polling again in
an attempt to catch up with more recent events. An alternative procedure for some applications might be
to decide not to re-queue the key (see below) after a certain number of overflows are received.
Finally, we pull out the Path indicating the file that has been modified, then
call our own process() method with the details.
Re-queueing the watch key
Notice the call to key.reset(): this effectively means "I want to go on receiving
modification notifications for this key/directory".
Putting it together: threading
A final stage of implementation will usually be to
listen for notifications in a separate
thread.
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.