1. Introduction
Any Java application from the system point-of-view is just an instance of the Java Virtual Machine. In this short tutorial, we’ll see how we can make our applications run as system services.
We’ll use the facilities of the systemd software package. systemd is the initialization and service management system in most modern Linux distributions.
Throughout the tutorial, we’ll consider two implementations: one for a simple case and one for a more advanced case.
2. Simple Service
In the systemd world, to create a system service, we need to prepare a unit file and register it the proper way. We’ll discuss the file location shortly, but first, let’s start with the content:
[Unit]
Description=My Java driven simple service
After=syslog.target network.target
[Service]
SuccessExitStatus=143
User=appuser
Group=appgroup
Type=simple
Environment="JAVA_HOME=/path/to/jvmdir"
WorkingDirectory=/path/to/app/workdir
ExecStart=${JAVA_HOME}/bin/java -jar javaapp.jar
ExecStop=/bin/kill -15 $MAINPID
[Install]
WantedBy=multi-user.target
We set the Type of service to simple because the system starts the JVM process directly, without spawning a child process.
ExecStop specifies the service termination command, and systemd is smart enough to figure out the PID of the started process. It automatically creates a MAINPID environment variable.
Then, we instruct systemd to send a 15 (SIGTERM) system signal to terminate the process gracefully.
The JVM designers made Java return a non-zero exit code in case it is terminated by a system signal. As a non-zero base, they took 128, and the resulting exit code is a sum of 128 and the signal numeric value.
By setting SuccessExitStatus to 143, we tell systemd to handle that value (128+15) as a normal exit**.
3. Forking Service
The simple service unit file above might be quite sufficient for trivial applications. However, more practical cases will probably include additional settings.
These can be JVM parameters as well as any other application-specific parameters, for example, config or data file locations. That may result in writing a wrapper shell script where we can set up all the required parameters before starting the JVM.
Let’s imagine we already have a wrapper script and now just want to turn it into a system service:
#!/bin/bash
JAVA_HOME=/path/to/jvmdir
WORKDIR=/path/to/app/workdir
JAVA_OPTIONS=" -Xms256m -Xmx512m -server "
APP_OPTIONS=" -c /path/to/app.config -d /path/to/datadir "
cd $WORKDIR
"${JAVA_HOME}/bin/java" $JAVA_OPTIONS -jar javaapp.jar $APP_OPTIONS
Since we use a shell script to start the service, the JVM will be started by the shell (bash) process. This operation is known as fork, and it’s why we set the service Type to forking.
Moving variable definitions into the script’s body also makes the unit file more concise:
[Unit]
Description=My Java forking service
After=syslog.target network.target
[Service]
SuccessExitStatus=143
User=appuser
Group=appgroup
Type=forking
ExecStart=/path/to/wrapper
ExecStop=/bin/kill -15 $MAINPID
[Install]
WantedBy=multi-user.target
4. Registering and Running the Service
No matter what service type we choose, to complete the mission, we must know how to set up and run the system service itself.
First, we need to name the unit file after the service name we want to have. In our examples, that could be javasimple.service or javaforking.service.
Then, we put the unit file under one of the locations where systemd can find it. For an arbitrary service, /etc/systemd/system is a good choice.
The full path to our system units, in that case, will be:
- /etc/systemd/system/javasimple.service
- /etc/systemd/system/javaforking.service
Another possible path to place system units is /usr/lib/systemd/system. This is typically the location used by the system installation packages.
However, we should consider it more appropriate when we develop our own .rpm or .deb installation packages containing system services.
In either case, we’ll control the service using the systemctl utility and pass either the start, stop, or status command.
Before that, however, we should notify systemd that it has to rebuild its internal service database. Doing this will make it aware of the new system unit we introduced. We can do this by passing the daemon-reload command to systemctl.
Now, we’re ready to run all the commands we mentioned:
sudo systemctl daemon-reload
sudo systemctl start javasimple.service
sudo systemctl status javasimple.service
● javasimple.service - My Java driven simple service
Loaded: loaded (/etc/systemd/system/javasimple.service; disabled; vendor preset: disabled)
Active: active (running) since Sun 2021-01-17 20:10:19 CET; 8s ago
Main PID: 8124 (java)
CGroup: /system.slice/javasimple.service
└─8124 /path/to/jvmdir/bin/java -jar javaapp.jar
We’ll need to run the daemon-reload command each time we modify the unit file.
Next, we notice the system reports our service running but disabled. Disabled services will not start automatically when the system boots.
Of course, we can configure it to start up automatically along with the system. This is where we use another systemctl command — enable:
sudo systemctl enable javasimple.service
Created symlink from /etc/systemd/system/multi-user.target.wants/javasimple.service to /etc/systemd/system/javasimple.service
Now, we can see that it’s enabled:
sudo systemctl status javasimple.service
● javasimple.service - My Java driven simple service
Loaded: loaded (/etc/systemd/system/javasimple.service; enabled; vendor preset: disabled)
Active: active (running) since Sun 2021-01-17 20:10:19 CET; 14min ago
Main PID: 8124 (java)
....
5. Conclusion
In this article, we looked at two possible ways of turning Java applications into system service by means of systemd.
Java is still one of the most popular programming languages. A lot of Java applications are designed to run non-interactively for a variety of tasks, such as processing data, providing an API, monitoring events, and so on. Thus, they all are good candidates to become system services.