Have you ever wanted to update some configuration of your web application without needing to restart it? Here is one way of doing it. Java's WatchService API allows you to monitor directoy for any changes. Without further ado, here's the example code.
package net.codesamples.crowsnest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Component
public class EnvironmentConfigurationWatcher {
private static final Logger log = LoggerFactory.getLogger(EnvironmentConfigurationWatcher.class);
List environmentList = new ArrayList<>();
private final String environmentsConfigFile = "environments.json";
@Value("${watch.directory}")
private String watchDirectory;
@EventListener(ApplicationReadyEvent.class)
public void startWatcher() {
readConfig();
new Thread(() -> {
final Path path = FileSystems.getDefault().getPath(watchDirectory);
try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
// final WatchKey watchKey =
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
while (true) {
final WatchKey wk = watchService.take();
for (WatchEvent event : wk.pollEvents()) {
//we only register "ENTRY_MODIFY" so the context is always a Path.
final Path changed = (Path) event.context();
if (changed.endsWith(environmentsConfigFile)) {
readConfig();
}
}
// reset the key
boolean valid = wk.reset();
if (!valid) {
log.info("Key has been unregistered");
}
}
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
private void readConfig() {
ObjectMapper mapper = new ObjectMapper();
File envConfigFile = new File(watchDirectory + "/" + environmentsConfigFile);
try (InputStream is = new FileInputStream(envConfigFile); ) {
Environment[] environments = mapper.readValue(is, Environment[].class);
environmentList.clear();
environmentList.addAll(Arrays.asList(environments));
log.info("Envs updated, {}", environmentList);
} catch (Exception e) {
log.error("Unable to read envs config file, {}", e.getMessage());
}
}
public List getEnvironmentList() {
return environmentList;
}
}
The above code is part of my Crowsnest repository in GitHub. Crowsnest is a simple monitoring app. It just checks whether sites are online or offline. For this project, I didn't want to restart the web app everytime I update the environments to monitor as specified in the environments.json file.
To solve that requirement, I utilized Java's WatchService API. Starting on line 37, I specify the directory where the JSON file is located. Then create a new WatchService. Next is to register it to the Path we want to watch. I then specified the kind of event I want to monitor, ENTRY_MODIFY, this mean I'll be notified when a directory entry is modified.
All good so far? Right, take() is next. Which means this method waits until there is a key in the queue. So this is blocking. Once a key is available, I get a list of events from pollEvents(). The file name is stored as the context of the event, hence the call to context(). If it is the environment configuration file that has changed the trigger a read and rebuild environment list.
After polling the events, I invoke a reset so the key can go back to a ready state. This is crucial. Fail to invoke a reset and the key will not receive any further events.
Demonstration
On startup, you'll see something like below. I'm using IntelliJ IDEA 2023.3.4 (Community Edition). You can use any IDE you like. You can even go command line!
Now, let's edit the environments.json file. You should see some log output like below. Notice that I changed Developmental to Development.
Java WatchService
Pretty cool, huh?. We no longer need to restart our web app! Thanks WatchService API. Thank you for reading and happy coding.


No comments:
Post a Comment