In the past I coded some command line tools in Java which are quite useful for me and which I ought to be useful for others1. Providing just fat JAR
files implies launching of those Java applications to be more or less cumbersome: Besides requiring a JVM being installed, some annoying command line got to be typed in, needing the exact location of the fat JAR
file in question at hand to be passed as argument2. Whilst trying to ease the situation, I stumbled over some tricks and ideas creating deliverables helping to lessen and even eliminate the pains.
Deliverables
There are some approaches I was playing around to get a more satisfying user experience when providing my command line tools’ deliverables3. Here I will focus on the launcher
, bundle
and installer
approaches, which means providing either a launcher, being a shell or operating system specific launcher prepended(!) to your fat JAR
file, taking care of seeking a suitable JVM on the local machine which then is invoked for launching (just one self contained file is being delivered) or an executable bundle containing a JVM under the hood, which transparently is installed the first time the executable is launched (for each targeted platform we need a dedicated executable)4 and finally a classic installer, installing the application on your operating system. Besides these approaches, there are more approaches such as the native
deliverables5.
Toolchain
For building launcher
, bundle
as well as the installer
deliverables, we require a fat JAR
file dropping out of our build process, containing all the resources, third party libraries and dependencies your application requires to run when being invoked via java -jar ...
. This can be achieved in several ways: As I am using Maven for build automation, I decided to go with the Maven Shade Plugin - I wont’t use any heavy weight frameworks such as Spring Boot5 for my command line tools!
If not mentioned otherwise, shell scripts *.sh
proposed to be invoked are intended to be issued in a bash
alike shell on GNU Linux or within a GNU userland such as Cygwin or Git BASH on MS Windows. Plain commands may also be issued using CMD
on MS Windows.
Java & Maven
First of all an according JDK needs to be installed, either by directly using the downloads from openjdk.org or by using SDKMAN!6. Make sure you have set the JAVA_HOME
environment variable correctly and your JDK is on your system’s path7. You may verify your installation by invoking java --version
in a terminal:
As we use Java with a version >=
16
, make sure your JDK is chosen accordingly.
The printed version information should contain a version >=
16
somewhere in the output.
Maven is expected to be installed already8, though when using SDKMAN!6, it is easily installed as of sdk install maven 3.8.6
.
Archetypes
Below find a selection of Maven Archetypes of which we will use one in the next section to get our Maven project with some sample Java code up and running:
For a jump start into developing Java driven command line tools, I created some fully pre-configured Maven Archetypes available on Maven Central. Those Maven Archetypes already provide means to directly create native executables, bundles as well as launchers and support out of the box command line argument parsing as well as out of the box property file handling.
Please adjust
my.corp
with your actualGroup-ID
andmyapp
with your actualArtifact-ID
:
refcodes-archetype-alt-c2
Use the refcodes-archetype-alt-c2
archetype to create a bare metal command & control (CLI
) driven Java application:
“mvn archetype:generate [...]
”
refcodes-archetype-alt-cli
Use the refcodes-archetype-alt-cli
to create a bare metal command line interface (CLI
) driven Java application:
“mvn archetype:generate [...]
”
refcodes-archetype-alt-csv
Use the refcodes-archetype-alt-csv
archetype to create a bare metal CSV
(CLI
) driven Java application:
“mvn archetype:generate [...]
”
refcodes-archetype-alt-decoupling
Use the refcodes-archetype-alt-decoupling
archetype to create a bare metal dependency injection and inversion of control (IoC
) driven Java application breaking up dependencies between components or modules of a software system:
“mvn archetype:generate [...]
”
refcodes-archetype-alt-filter
Use the refcodes-archetype-alt-filter
archetype to create a bare metal command line interfaceCLI
) driven Pipes and Filters Java application (the pipes
are provided by the shell, your application will be the filter
to interact with other UNIX filters
via pipes
):
“mvn archetype:generate [...]
”
refcodes-archetype-alt-rest
Use the refcodes-archetype-alt-rest
to create a bare metal REST driven Java application within just one source code file:
“mvn archetype:generate [...]
”
Project
To examine the launcher
, bundle
as well as the installer
approaches, we now create a Maven project for a Pipes and Filters Java application using the refcodes-archetype-alt-filter
archetype:
Please adjust
my.corp
with your actualGroup-ID
andmyapp
with your actualArtifact-ID
:
In the root folder of the newly created project you find the pom.xml
alongside all required source code and configuration files as of the common Maven project layout.
You may have to invoke
chmod +x .sh
on your project’s root folder to make the scripts executable!
This project already ships the required scripts bundle.sh
, scriptify.sh
, jexefy.sh
as well as installer.sh
for automating the processes described later.
Bundle
The bundle
approach provides an executable bundling a JVM under the hood, which transparently is installed the first time the executable bundle
is launched: For each targeted platform we need to produce a dedicated executable bundle
e.g. for GNU Linux or for MS Windows accordingly. To achieve this, we use the warp
tool found on GitHub. This tool packages a predefined directory structure together with an application launcher into a single executable. To create a bundle for our purposes, the following steps are necessary (and already automated by the bundle.sh
script):
- A folder to be bundled is to be prepared, here we use the
.bundle
folder in our project’s root folder. - The operating system specific JRE implementation to be bundled is to be placed into the
.bundle
folder. - The fat
JAR
to be launched is to be placed into the.bundle
folder. - Finally a launcher file, e.g.
launcher.sh
for GNU Linux orlauncher.cmd
for MS Windows, launching the fatJAR
file with the prepared JRE, is also to be placed into the.bundle
folder. - To finish off, the
warp
command is invoked with according arguments passed.
This got to be done for each targeted platform in question (e.g. GNU Linux or MS Windows), but no worries, this process can be automated by invoking the
bundle.sh
script!
For GNU Linux, the prepared .bundle
folder structure may look as follows:
.bundle
│
├── application.jar
│
├── jre
│ │
│ ├── bin
│ │
│ ├── conf
│ │
│ ├── DISCLAIMER
│ │
│ ├── legal
│ │
│ ├── lib
│ │
│ ├── readme.txt
│ │
│ ├── release
│ │
│ └── Welcome.html
│
└── launcher.sh
For MS Windows, the prepared .bundle
folder structure may look as follows:
.bundle
│
├── application.jar
│
├── jre
│ │
│ ├── bin
│ │
│ ├── conf
│ │
│ ├── DISCLAIMER
│ │
│ ├── legal
│ │
│ ├── lib
│ │
│ ├── readme.txt
│ │
│ ├── release
│ │
│ └── Welcome.html
│
└── launcher.cmd
To create a bundle on GNU Linux, the warp
command is invoked as follows:
To create a bundle on MS Windows, the warp
command using CMD
is invoked as follows:
All those steps above are automated by the bundle.sh
script to easily create bundles for MS Windows as well as for GNU Linux. The initial bundle.sh
configuration is located in the bundle.conf
file, both files being located in the root folder of your project.
Building bundles both for MS Windows as well as for GNU Linux means changing into your project’s root folder and executing the bundle.sh
script:
This creates the following folder structure below your project’s root folder:
.bundle
│
├── linux_x86_64
│ │
│ ├── application.jar
│ │
│ ├── jre
│ │ │
│ │ ├── bin
│ │ │
│ │ ├── conf
│ │ │
│ │ ├── DISCLAIMER
│ │ │
│ │ ├── legal
│ │ │
│ │ ├── lib
│ │ │
│ │ ├── readme.txt
│ │ │
│ │ ├── release
│ │ │
│ │ └── Welcome.html
│ │
│ └── launcher.sh
│
└── windows_x86_64
│
├── application.jar
│
├── jre
│ │
│ ├── bin
│ │
│ ├── conf
│ │
│ ├── DISCLAIMER
│ │
│ ├── legal
│ │
│ ├── lib
│ │
│ ├── readme.txt
│ │
│ ├── release
│ │
│ └── Welcome.html
│
└── launcher.cmd
The bundle.sh
script downloads (if not already done) a JRE for MS Windows as well as for GNU Linux (configured in the bundle.conf
file) into the .bundle
folder of your project and creates the resulting bundle files *.exe
as well as*.elf
in the target
folder of your project. Your bundled application may now be launched by invoking the *.exe
respectively the *.elf
file, which upon the first launch, extracts the JRE alongside the fat JAR
as well as the launcher script either below %USERPROFILE%\AppData
on MS Windows or below ~/.local
on GNU Linux in an bundle’s specific(!) subfolder.
Given that we go for myapp
as Artifact-ID, in a bash
shell on GNU Linux, launch as follows:
Given that we go for myapp
as Artifact-ID, using CMD
on MS Windows, launch as follows:
Launcher
The launcher
approach provides a shell or an operating system specific launcher prepended(!) to your fat JAR
file, taking care of seeking a suitable JVM on the local machine which then is invoked for launching. This way only one file is being delivered, the fat JAR
prepended with the according launcher!
This approach solely works because the JVM, when being provided with a fat JAR
to be launched, considers this fat JAR
as being a stream, which is scanned till the beginning of a valid JAR
record is found. All bytes before the beginning of the valid JAR
record are ignored by the JVM. On the other hand, a shell such as the bash
shell, when executing a script, just reads that amount of data which is required to execute the current instruction. Anything appended to the end of the script is ignored (CMD
on MS Windows actually does not work this way, so we cannot use the launcher
mechanism here). Similar to the mechanism applied by the bash
shell are the mechanics provided by GNU Linux and MS Windows when loading binary executable files (e.g. *.exe
or *elf
files): Only those pages from the executable file are loaded into memory which are required by the CPU to start execution. In case the CPU hits pages not yet mapped into memory, a page fault is issued, causing the according page to be loaded into memory. Pages not required by the execution of the binary executable are never loaded! Anything appended to the end of such a binary executable is ignored.
We now can take advantage of the fact that anything appended to a bash
script or an *.exe
or *elf
binary executable is ignored by appending our fat JAR
to either a launcher script or a binary executable. The launcher script or the binary executables just have to detect an appropriate JVM on the local machine and invoke this JVM with itself as argument, being the accordingly modified script or binary executable with the appended fat JAR
file. The JVM will detect the embedded JAR
file and execute it flawlessly.
A bash
script launcher may look as follows:
“#!/bin/bash [...]
”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash
if [ ! -z "${TERM}" ] ; then
if command -v "tput" &> /dev/null ; then
if [ -z "$COLUMNS" ]; then
export COLUMNS="$(tput cols)"
fi
if [ -z "$LINES" ]; then
export LINES="$(tput lines)"
fi
fi
fi
# See "https://coderwall.com/p/ssuaxa/how-to-make-a-jar-file-linux-executable"
SCRIPT_PATH=`which "$0" 2>/dev/null`
[ $? -gt 0 -a -f "$0" ] && SCRIPT_PATH="./$0"
LAUNCHER_DIR="$(dirname $SCRIPT_PATH 2>/dev/null)"
if [ $? -ne 0 ]; then
LAUNCHER_DIR="."
fi
if type "uname" &> /dev/null; then
if [ `uname -o` = "Cygwin" ]; then
SCRIPT_PATH=$(cygpath -w ${SCRIPT_PATH})
LAUNCHER_DIR=$(cygpath -w ${LAUNCHER_DIR})
fi
fi
java=java
if test -n "$JAVA_HOME"; then
java="$JAVA_HOME/bin/java"
fi
javaArgs[0]="-Dlauncher.dir=$LAUNCHER_DIR"
javaIndex="1"
appIndex="0"
for var in "$@"; do
if [[ ${var} == -Dlauncher.dir=* ]]; then
javaArgs[0]="${var}"
elif [[ ${var} == -D* ]]; then
javaArgs[${javaIndex}]="${var}"
javaIndex=$((javaIndex + 1))
else
appArgs[${appIndex}]="${var}"
appIndex=$((appIndex + 1))
fi
done
stty -echoctl &> /dev/null
exec "${java}" -XX:TieredStopAtLevel=1 "${javaArgs[@]}" -server -jar "$SCRIPT_PATH" "${appArgs[@]}"
exit $?
All those steps above are automated by the scriptify.sh
script to easily create launchers for the bash
shell.
To build a launcher, change into your project’s root folder and execute the scriptity.sh
script:
This results in a self contained launcher
below the project’s target
folder, which can be invoked as follows:
The *.exe
and *elf
launchers work by the same principle, with funcodes-jexefy
being a project I actually created using the RUST programming language to create native launchers. This project compiles to launcher executables for GNU Linux and MS Windows.
All those steps above are automated by the jexefy.sh
script to easily create launchers for GNU Linux and MS Windows.
Building launcher executables (*.exe
and *.elf
) means changing into your project’s root folder and executing the jexefy.sh
script:
This results in a self contained launcher
below the project’s target
folder, which can be invoked in a bash
shell on GNU Linux as follows:
Using CMD
on MS Windows, the self contained launcher
below the project’s target
folder can be invoked as follows:
Installer
Creating an *.msi
installer for MS Windows is as easy as simply using the jpackage
command provided by the installed JDK:
All those steps above are automated by the installer.sh
script to easily create *.msi
installers for MS Windows.
Building an installer means changing into the project’s root folder and executing the installer.sh
script:
The installer variant is, as of my opinion, the most uncomfortable one for end users but the most straight forward approach when building.
Further reading
The refcodes-archetype
collection provides easy means do build native
deliverables in addition to launcher
, bundle
or installer
deliverables. See GraalVM: Native command line tools for Linux and Windows written in Java for detailed insights on how building native
deliverables is achieved.
See also
- GraalVM: Native command line tools for Linux and Windows written in Java
- refcodes-properties: Managing your application’s configuration
- refcodes-cli: Parse your args[]
- Downloads
-
Java command line tools, aka
public static void main( String[] args){ ... }
. ↩ -
Launching a fat
JAR
usually is achieved by invokingjava -jar <the/path/to/your/fat/jar/file/in/question>.jar
. ↩ -
The command line tools can be found in the Downloads section of this site. ↩
-
The JVM is either installed below
%USERPROFILE%\AppData
on MS Windows or~/.local
on GNU Linux in a bundle’s specific folder. ↩ -
For
native
deliverables see GraalVM: Native command line tools for Linux and Windows written in Java. ↩ ↩2 -
As I often switch forth and back between different JVM and GraalVM installations, I preferably use SDKMAN!(e.g.
sdk install java 17.0.5-tem
, verify the available candidates viasdk list java
). ↩ ↩2 -
The basic setup of your JDKis described in this JDK installation Guide. ↩
-
Maven may be either installed directly following the installation guide or by using SDKMAN! (e.g.
sdk install maven 3.8.6
, verify the available candidates viasdk list maven
)6. ↩