Thursday, March 26, 2026

Run App as SystemD Service Example

Here we go. You got an old web app that start/stop/restart using initd scripts. This was well and good in the CentOS days. After some OS updates here and there, your web app now runs on Rocky 9.7. Trouble is on VM reboots, your web app does not start up automatically and you have 10 apps on the box. From time to time, CI/CD breaks because your web app process goes on zombie mode and you'll have to manually kill it.

What can you do? Move it from an initd to a systemd service. As to the why the Linux folks replaced or moved from initd to systemd, I'll let you google that around. Here are the steps to make your app run as a systemd service.

Create the SystemD Service File


[jpllosa@rocky-balboa ~]$ cat /etc/os-release
NAME="Rocky Linux"
VERSION="9.7 (Blue Onyx)"

...snipped...

[jpllosa@rocky-balboa systemd-stuff]$ cat my-web-app.service
[Unit]
Description=My Web App service
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
User=codesamples
ExecStart=/usr/bin/java -jar /opt/codesamples/my-web-app/my-web-app.jar --spring.config.location=/etc/codesamples/my-web-app/my-web-app.properties

[Install]
WantedBy=multi-user.target
[jpllosa@rocky-balboa systemd-stuff]$

What do all the mumbo jumbo above mean?

All .service file start with a Unit section which provides basic information about the service. The Description must not exceed 80 characters and must describe the service. The After tells systemd to only activate this service after this prerequisite. In this example, the service is activated after network connectivity is established. We have disabled any kind of rate limiting by setting StartLimitIntervalSec to 0. This rate limiting works in conjunction with Restart.

The Service section provides instructions on how to control the service. The simple Type means to start the service without any special considerations. The ExecStart clearly shows the command to run to start the service. The full path to the command and arguments are declared. User specifies the user under which the service should run. RestartSec configures the time in seconds to sleep before restarting the service. Restart is set to always, as the word says, the service will be restarted no matter what (e.g. process is terminated by a signal, non-zero exit code, termination due to out of memory, etc). There is a default timeout for starting, stopping and aborting of units, as well as a default time to sleep between automatic restarts of units. If the restart times out after several tries, the system will not try to restart the service. Therefore, we have set above StartLimitIntervalSec to 0, so it will try to restart the service to infinity and beyond.

WantedBy is used to create symlinks in .wants/ directories when the service is enabled. In this case, a symlink will be created under multi-user.target.wants. This indicates that the service should be started as part of the multi-user system.

Copy the above service file to where systemd service file reside. As seen below.


[jpllosa@rocky-balboa system]$ pwd
/etc/systemd/system
[jpllosa@rocky-balboa system]$ ls -ali
total 24
 67738686 drwxr-xr-x. 10 root root 4096 Mar 17 13:27 .
   863730 drwxr-xr-x.  6 root root 4096 Feb 10 13:45 ..
 67824144 drwxr-xr-x.  2 root root   65 Feb 13 13:43 basic.target.wants
 67750396 lrwxrwxrwx.  1 root root   37 Feb 10 12:27 ctrl-alt-del.target -> /usr/lib/systemd/system/reboot.target
 67824045 lrwxrwxrwx.  1 root root   41 Feb 10 12:28 dbus-org.fedoraproject.FirewallD1.service -> /usr/lib/systemd/system/firewalld.service
 67823100 lrwxrwxrwx.  1 root root   57 Feb 10 12:28 dbus-org.freedesktop.nm-dispatcher.service -> /usr/lib/systemd/system/NetworkManager-dispatcher.service
 67750469 lrwxrwxrwx.  1 root root   43 Feb 10 12:27 dbus.service -> /usr/lib/systemd/system/dbus-broker.service
 67750393 lrwxrwxrwx.  1 root root   41 Feb 10 12:29 default.target -> /usr/lib/systemd/system/multi-user.target
 67736707 -rw-r--r--.  1 root root  394 Mar 17 13:27 my-web-app.service
   905723 drwxr-xr-x.  2 root root   32 Feb 10 12:27 getty.target.wants
 68554611 drwxr-xr-x.  2 root root   56 Feb 13 13:43 graphical.target.wants
 34039637 drwxr-xr-x.  2 root root 4096 Mar 17 13:32 multi-user.target.wants
 67823101 drwxr-xr-x.  2 root root   48 Feb 10 12:28 network-online.target.wants
 67750466 drwxr-xr-x.  2 root root   71 Feb 10 12:28 sockets.target.wants
101399263 drwxr-xr-x.  2 root root 4096 Feb 10 12:28 sysinit.target.wants
   956462 drwxr-xr-x.  2 root root   56 Feb 10 12:28 timers.target.wants

Starting and Enabling the Web App

Now that the service is ready. Let's start up the web app.


sudo systemctl start web-app

If there are no error, you can then enable the web app so it will start at boot. You don't need to worry about reboots. The web app will automatically start up now.


sudo systemctl enable web-app

[jpllosa@rocky-balboa system]$ cd multi-user.target.wants/
[jpllosa@rocky-balboa multi-user.target.wants]$ ls -ali
total 8
34039637 drwxr-xr-x.  2 root root 4096 Mar 17 13:32 .
67738686 drwxr-xr-x. 10 root root 4096 Mar 17 13:27 ..
34113015 lrwxrwxrwx.  1 root root   38 Feb 10 12:28 auditd.service -> /usr/lib/systemd/system/auditd.service
34113143 lrwxrwxrwx.  1 root root   39 Feb 10 12:28 chronyd.service -> /usr/lib/systemd/system/chronyd.service
34039686 lrwxrwxrwx.  1 root root   37 Feb 10 12:27 crond.service -> /usr/lib/systemd/system/crond.service
34125258 lrwxrwxrwx.  1 root root   50 Mar 17 13:31 my-web-app.service -> /etc/systemd/system/my-web-app.service
34113189 lrwxrwxrwx.  1 root root   41 Feb 10 12:28 firewalld.service -> /usr/lib/systemd/system/firewalld.service
34124734 lrwxrwxrwx.  1 root root   37 Mar 17 13:29 httpd.service -> /usr/lib/systemd/system/httpd.service
34113963 lrwxrwxrwx.  1 root root   42 Feb 10 12:28 irqbalance.service -> /usr/lib/systemd/system/irqbalance.service
34112994 lrwxrwxrwx.  1 root root   37 Feb 10 12:28 kdump.service -> /usr/lib/systemd/system/kdump.service
34131546 lrwxrwxrwx.  1 root root   43 Feb 10 12:41 nessusagent.service -> /usr/lib/systemd/system/nessusagent.service
34112403 lrwxrwxrwx.  1 root root   46 Feb 10 12:28 NetworkManager.service -> /usr/lib/systemd/system/NetworkManager.service
34039638 lrwxrwxrwx.  1 root root   40 Feb 10 12:27 remote-fs.target -> /usr/lib/systemd/system/remote-fs.target
34111121 lrwxrwxrwx.  1 root root   39 Feb 10 12:28 rsyslog.service -> /usr/lib/systemd/system/rsyslog.service
34347699 lrwxrwxrwx.  1 root root   43 Feb 10 13:44 sentinelone.service -> /usr/lib/systemd/system/sentinelone.service
34113102 lrwxrwxrwx.  1 root root   36 Feb 10 12:28 sshd.service -> /usr/lib/systemd/system/sshd.service
34112926 lrwxrwxrwx.  1 root root   36 Feb 10 12:28 sssd.service -> /usr/lib/systemd/system/sssd.service
[jpllosa@rocky-balboa multi-user.target.wants]$

More Commands

Try the commands below to check the status, stop, or restart your web-app service.


sudo systemctl status web-app
sudo systemctl stop web-app
sudo systemctl restart web-app

Run App as SystemD Service

You can now sit back and relax. No more panic mode after VM reboots and when other people complain why things aren't working as they are supposed to. Goodbye zombie processes. Congratulations, you have automated yourself out of a job.

More Rocky Linux tips here...

Friday, March 6, 2026

Deploy to Kubernetes Example

Alright, now that you got a docker image in the previous example, Dockerize Spring Boot App Example, what now? The most obvious next step is to spin up the docker image in a Kubernetes cluster. But GKE (Google Kubernetes Engine) is bloody expensive for us mere mortals. Luckily we can practice Kubernetes stuff with Docker Desktop.

Assumptions

I'm assuming you have read my previous example, Dockerize Spring Boot App Example, So you should have the following already set up.

  • Docker Desktop 4.55.0
  • Windows 11 Home 10.0.26200
  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • OpenJDK 17.0.10
  • Crowsnest, example Spring Boot App
  • Lens 2025.6.261308-latest

Docker Desktop

First up is to start a Kubernetes cluster via Docker Desktop. Should just be a bunch of clicks. I just chose a single node cluster (Kubeadm). You should have something like below:

And if you have Lens K8S IDE. You'll have something like below.

Deploy to Kubernetes

First, let's check our image (docker images). Like so:


C:\workspace>docker images
                                                                                                    i Info →   U  In Use
IMAGE                                                                   ID             DISK USAGE   CONTENT SIZE   EXTRA
crowsnest:v1.0.0                                                        b9bfb190828e        567MB          197MB    U

Let's make sure we deploy to docker desktop kubernetes.


C:\workspace>kubectl config use-context docker-desktop
Switched to context "docker-desktop".

And the fun starts, create a deployment.


C:\workspace>kubectl create deployment crowsnest --image=crowsnest:v1.0.0
deployment.apps/crowsnest created

After deployment, take a look at Lens and Docker Desktop.

Or if you like CLI.


C:\workspace>kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
crowsnest-f56bb85ff-t9dn9   1/1     Running   0          28m

Well and good so far. But our web app is not reachable from the browser yet. We need to expose the port of the pod so we can get to the web app. Do note that your pod name might be different from this example.


C:\workspace>kubectl expose pod crowsnest-f56bb85ff-t9dn9 --port=8080 --name=crowsnest --type=LoadBalancer
service/crowsnest exposed

C:\workspace>kubectl get services
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
crowsnest    LoadBalancer   10.100.27.69   localhost     8080:31912/TCP   97s
kubernetes   ClusterIP      10.96.0.1              443/TCP          4d3h

You should have something like below.

Perfecto! We should be able to access Crowsnest on the browser now. localhost:8080.

Thank you Docker Desktop for helping us save money by practicing kubectl stuff with you, instead of the expensive GKE. Deploying to GKE should be similar. Just need to point kubectl to GKE.

Undeploy from Kubernetes

Most importantly when doing this on a paid Kubernestes service, don't forget to delete the pod, service, deployment, etc. GKE billing is astronomical (delete the project too to be sure!) Here are the commands to undo what you've done.


C:\workspace>kubectl delete service crowsnest --now
service "crowsnest" deleted from default namespace

C:\workspace>kubectl delete deployment crowsnest --now
deployment.apps "crowsnest" deleted from default namespace

C:\workspace>kubectl get services
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1            443/TCP   4d3h

C:\workspace>kubectl get pods
No resources found in default namespace.

Deploy to Kubernetes Wrap Up

Yes, we did all of the above steps manually. Yes, this can all be automated. But before we automate stuff, we have to do it manually. For example, you can create a GitHub action workflow to do all of the above steps to spin up your docker image to GKE, just with a yaml file. That would be another story. Happy spinning!

Learn more k8s...

Friday, February 20, 2026

Dockerize Spring Boot App Example

One of the benefits of a dockerize app (dockerize is synonymous with containerization, just like googling is to web searching) is the ease of deployment to different environments. Once you got that docker image built, you can stick it in any environment you like. Shall we dockerize your Spring Boot app? Here's a quick example.

Tools

Those are the tools I used to build this example.

Dockerfile

Before we can run the docker file, let's build the project first. Run, mvn clean install in IntelliJ or via command line. That should create a target directory. Now, let's take a look at the Dockerfile found at the root directory.


FROM eclipse-temurin:17-jdk-alpine
RUN addgroup -S crowsnest && adduser -S crowsnest -G crowsnest
USER crowsnest:crowsnest
COPY docker-files/ app-files
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} crowsnest.jar
ENTRYPOINT ["java", "-jar", "crowsnest.jar", "--spring.config.location=/app-files/application.properties"]

What does above file mean? What will it do? FROM means that this is the base image. Our crowsnest image will extend from this image. Docker Hub contains a lot of Docker images that are suitable as base images. In this case our base image is built by Eclipse Temurin loaded with OpenJDK 17.

The RUN command will execute the Alpine Linux shell commands addgroup and adduser. The end result is a user called crowsnest belonging to the crowsnest group. This user will run the Crowsnest Spring Boot app. We don't want root running the app. The -S is a flag for system services or daemon group.

The USER command will set the user name and user group to crowsnest as the default user and group. This user is used for succeeding RUN, ENTRYPOINT, and CMD commands.

The COPY command, as it says, will copy the files in our local docker-files directory to the app-files directroy in the Alpine Linux image.

The ARG command defines the JAR_FILE variable that is passed at build time to the builder.

The next COPY command, copies all the jar files from the target directory into a single jar called crowsnest.jar.

Lastly ENTRYPOINT command configures the container to run as an executable. In this case, it will run java with the -jar and --spring.config.location parameters.

Right, let's build the image using the docker build command. Like so:


C:\workspace\crowsnest>docker build -t crowsnest:v1.0.0 .
[+] Building 5.2s (8/8) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile

The above command builds the crowsnest image with the image identifier (i.e tag) crowsnest:v1.0.0 on the current path. I sometimes add the option --no-cache to not use cache when building the image. After a successful build, you should have something like below. Old school way is docker images on the CLI (Command Line Interface.

Running the Docker Image

Righto, let's see this image in action. Go to the command line and run docker run -p 8080:8080 crowsnest:v1.0.0. This command will create and run a new container from the image tagged crowsnest:v1.0.0 exposing port 8080 and routing incoming requests to Crowsnest running on port 8080. In the words, the first 8080 is the container port number. The second is your app's port number. You should have something like below.


C:\workspace\crowsnest>docker run -p 8080:8080 crowsnest:v1.0.0

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.9)

2026-02-18T15:26:52.406Z  INFO 1 --- [crowsnest] [           main] n.c.crowsnest.CrowsnestApplication       : Starting CrowsnestApplication v0.0.1-SNAPSHOT using Java 17.0.17 with PID 1 (/crowsnest.jar started by crowsnest in /)
2026-02-18T15:26:52.409Z  INFO 1 --- [crowsnest] [           main] n.c.crowsnest.CrowsnestApplication       : No active profile set, falling back to 1 default profile: "default"
2026-02-18T15:26:53.293Z  INFO 1 --- [crowsnest] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2026-02-18T15:26:53.307Z  INFO 1 --- [crowsnest] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-02-18T15:26:53.307Z  INFO 1 --- [crowsnest] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.50]
2026-02-18T15:26:53.343Z  INFO 1 --- [crowsnest] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2026-02-18T15:26:53.344Z  INFO 1 --- [crowsnest] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 878 ms
2026-02-18T15:26:53.796Z  INFO 1 --- [crowsnest] [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2026-02-18T15:26:53.974Z  INFO 1 --- [crowsnest] [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : Starting...

If you are not old school, the docker desktop UI will look like so:

You can start/stop the image from running via the Actions button. Chances are you might experience some connectivity problems. Can't connect to an API or database. Click on the three dots then "Open in terminal". This should give you a shell prompt like below. As you can see, we are running Alpine Linux. Remember FROM eclipse-temurin:17-jdk-alpine? On the root directory is app-files and crowsnest.jar. Built with the following commands COPY docker-files/ app-files and COPY ${JAR_FILE} crowsnest.jar. Here, you can try ping, curl, wget, telnet, etc. to check connectivity. Excellent!

Dockerize a Spring Boot App

Outstanding. We have built and ran a docker image. We can spin this image up in any environment we like. To recap, we build our image from a base image then add what we need like the necessary configuration files, the application itself, etc. Finally, we specify our app as the default executable. Happy dockerizing!

Docker some more...

Saturday, February 7, 2026

Spring SimpMessagingTemplate Example

Want to send notifications to your WebSocket clients? What if you want to send messages to connected clients so they can update their UI? You definitely can send messages from the back-end (e.g. from any part of your application). Any application component can send messages to the message broker. The easiest way to do so is to inject a SimpMessagingTemplate and use it to send messages. Typically, you would inject it by type. If another bean of the same type exists, qualify it by name (@Qualifier).

My example is taken from my simple monitoring app, Crowsnest (Crowsnest repository in GitHub). Here's the code where I use SimpMessagingTemplate. This web app just checks whether sites are online or offline.


package net.codesamples.crowsnest;

... imports snipped ...

@Component
public class ScheduledPings {
    private static final Logger log = LoggerFactory.getLogger(ScheduledPings.class);

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    @Autowired
    EnvironmentConfigurationWatcher environmentConfigurationWatcher;

    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;

    private WebClient webClient = WebClient.create();

    private List internalEnvironmentList = new ArrayList<>();

    private ObjectMapper mapper = new ObjectMapper();

    @Scheduled(cron = "${cron.expression}")
    public void pinger() {
        log.info("The time is now {}", dateFormat.format(new Date()));
        List environmentList = environmentConfigurationWatcher.getEnvironmentList();
        log.info("Environment list: {}", environmentList);

        for (Environment environment : environmentList) {
            for (App app : environment.getApps()) {
                Mono mono = webClient.get()
                        .uri(app.getUrl())
                        .exchangeToMono(clientResponse -> {
                            HttpStatusCode statusCode = clientResponse.statusCode();
                            if (statusCode.is2xxSuccessful()) {
                                app.setStatus("up");
                            }
                            log.info("StatusCode: {} = {}", statusCode, app.getUrl());
                            return clientResponse.bodyToMono(String.class);
                        })
                        .onErrorResume(Exception.class, exception -> {
                            log.info("Exception {}", exception.getMessage());
                            app.setStatus("down");
                            return Mono.empty();
                        });
                mono.subscribe();
            }
        }

        if (internalEnvironmentList.isEmpty()) {
            internalEnvironmentList = deepCopy(environmentList);
            simpMessagingTemplate.convertAndSend("/topic/environments", internalEnvironmentList);
        } else {
            if (!areTheSame(environmentList, internalEnvironmentList)) {
                log.info("environmentList NOT equal to internalEnvironmentList: \n {} \n {}",
                        environmentList, internalEnvironmentList);
                internalEnvironmentList = deepCopy(environmentList);
                simpMessagingTemplate.convertAndSend("/topic/environments", internalEnvironmentList);
            } else {
                log.info("environmentList equal to internalEnvironmentList");
            }
        }
    }

    private List deepCopy(List src) {
	    ... snipped ...
    }

    private boolean areTheSame(List list1, List list2) {
        ... snipped ...
    }
}

Straight to the point and no lollygagging, on lines 15, 52, and 58 are where the magic happens. So we let Spring inject the SimpMessagingTemplate, then send the message to the target destination. In this example, the destination is "/topic/environments". This is mapped to a method in a controller class. And that is how you update your connected clients via WebSocket.

In this example, on initial load where the environment list is still empty, we update all connected clients. The clients are updated as well when there is a change in the environment list. Here easily validated by checking if the old list matches the new list.

For setting up WebSocket on your Spring Boot app, front-end and back-end basics, I'll point you to my previous blog, Spring Boot WebSocket Example

Demonstration

I'm using IntelliJ IDEA 2023.3.4 (Community Edition). You can use any IDE you like. You can even go command line! Start up the Crowsnest web app. Once it has started, it should be accessible on http://localhost:8080 and you should have something like below when connected. Go ahead, click the connect button. Pay attention to the web console logs. Notice the JSON environments list.

Now, let's edit the environments.json file to force the back-end to notify the clients. For this, I'm just going to add "New" to the title. From "Development" to "New Development". On the back-end logs. The old environment list will not be equal to the new environment list which will trigger a publish message to the topic. You should see something like this on the logs.

Alright, we got a new title. Let's head back to the browser. It should now render the new title and you'll also see it log the new environment list. Like so.

Spring SimpMessagingTemplate

Now you don't need to poll the back-end incessantly. All you have to do is connect via WebSocket and wait for any notification. That should relieve the front-end of keeping up to date. Thank you for reading and happy WebSocket-ing.

More details here...

Friday, January 30, 2026

Java WatchService Example

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.

See more...