DEV Community

Cover image for 3 Ways to Package Your Java Project into a JAR for Deployment
Song Yang
Song Yang

Posted on • Updated on

3 Ways to Package Your Java Project into a JAR for Deployment

In this tutorial, we will look at 3 ways to package your Java project into a JAR file, so you can ship your application to your users.

A JAR file is an archive that contains compiled Java source code and other things needed to run your program. With a functioning JAR, your user should only need a Java Runtime to run your code. They shouldn't need to compile your code by opening up an IDE or using a build tool. This makes it easy to deploy on different platforms. Hence, Java's slogan "Write once, run anywhere".

1. Building a single JAR

When your project is simple and you don't have many dependencies, building a JAR from your project is very easy. The jar task on Gradle is enough. Just make sure your build.gradle has your application's entry point (main class/main method) defined.

jar {
    // this is necessary to run your JAR
    manifest {
        attributes 'Main-Class': 'yourpackages.morepackages.YourMainClass'
    }

    // the rest is to access resource files like images, etc.
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}
Enter fullscreen mode Exit fullscreen mode

Then run this on the command line.

gradle jar
Enter fullscreen mode Exit fullscreen mode

Your JAR file is produced inside the directory depending on your project IDE. For projects initialized with gradle only, that is build/libs/.

2. Building a JAR and delivering it with other JARs

It is possible that a fat JAR cannot be produced for reasons outside of your control. Fat JARs are supposed to contain everything you need to run the program, besides the Java Runtime. But sometimes, a certain dependency creates a problem with the building process. For example, building a Java project with JavaFX creates problems when attaching the JavaFX runtime to your JAR.

To remedy this, you can deliver several JARs at once and provide a script that launches the Java Runtime properly by referencing the third-party JARs.

A. Acquire the third-party JARs

For every third-party JARs you cannot integrate into your JAR, get the version of those JARs that you need. For example, download a version of the JavaFX runtime JAR to your computer.

B. Build your JAR alone

This step is similar to the situation where you build a single JAR.

C. Write a shell script (aka command-line script)

A shell script is a file that contains commands that you would run interactively on the command line. Instead of typing every command by hand one by one, you can write them into a file and run the file through the command line. The language of a shell depends on the system you intend to deploy your Java project. Here is a short list of commonly found shell languages.

  • Windows:
    • Batch (.bat)
    • Bash (.sh), if Git Bash is installed
  • Mac:
    • Z Shell (.zsh)
    • Bash (.sh), if the Mac is set up properly
  • Linux:
    • Bash (.sh)

Begin your shell script with a shebang. This tells the command line what the file is. For example, the following shebang is for a Bash file. It must always be the first line of a shell script.

#!/bin/bash
Enter fullscreen mode Exit fullscreen mode

Create a shell script. Usually, they are named run.sh or execute.sh. Inside it, you call the Java Runtime while referencing the third-party libraries' JARs and your JAR.

java --module-path path/to/your/third-party-JARs --add-modules module.you.need,second.module.you.need -jar your-project/build/libs/YourOwnJAR.jar
Enter fullscreen mode Exit fullscreen mode

In this command, we call java by giving it a module path. The parameter path/to/your/third-party-JARs you supply here should be the location where you store your third-party JARs. Since we are deploying, we expect the JARs to be located in the same folder as the shell script. Therefore, we can use relative paths, e.g. javafx-sdk-20.0.1/lib. This assumes you moved the JavaFX folder next to your shell script.

Example file setup

your-current-working-directory/
|  YourOwnJAR.jar
|  javafx-sdk-20.0.1/
|  |  lib/
|  |  |  ... lots of JARs   
|  
|  run.sh
Enter fullscreen mode Exit fullscreen mode

Example run.sh for a JavaFX project

#!/bin/bash
java --module-path javafx-sdk-20.0.1/lib --add-modules javafx.controls,javafx.fxml,javafx.graphics,javafx.media -jar YourOwnJar.jar
Enter fullscreen mode Exit fullscreen mode

Don't forget to grant permission to the script with chmod.

chmod +x run.sh
Enter fullscreen mode Exit fullscreen mode

To deliver this to your user, you would zip your-current-working-directory and send the archive. You would notify your user to unzip the archive and run the shell script by double-clicking it or through the command line with ./run.sh. This assumes your user has the appropriate shell language and Java Runtime.

3. Delivering a native application

This method is unlike the other two because it does not require the user to have anything installed, not even a Java Runtime! For situations where you want to deliver maximum application independence, this should be your go-to solution. For example, if you are dealing with people who have no idea what a shell script is or even what coding is, you should be sending everything as a self-contained double-clickable program.

The way this last method works is by shipping the Java Runtime with your JAR and dependencies. For those who don't know, the Java Runtime is an application designed to run compiled Java code on a specific operating system. As developers, we use the runtime that comes with the Java Development Kit (JDK). Two limitations of this method are the rather large deployment binary and the breaking of the slogan "Write once, run anywhere".

We will make use of the running example. This method builds upon Method 2.

A. Put all of your JARs (third-party or not) into a folder with your shell script

This step is the same as all of Method 2.

B. Download a copy of the Java Runtime Environment (JRE)

Next, you need to download a version of the Java Runtime. Head over to a distributor like the Eclipse Foundation and download a JRE that matches the version of your JDK. Although you can ship the JDK to your user, the file size would be very large (200+ MB) and it would contain many features that your user will never use. That's why we only need the JRE (40+ MB).

C. Move the JRE into your bundling folder

Assuming you have the same file structure from Method 2, you only need to move the unzipped JRE into your folder. The final file structure should look like this.

Example bundling folder structure

your-bundling-directory/
|  jre/ # <------------------------ moved here
|  YourOwnJAR.jar
|  javafx-sdk-20.0.1/
|  |  lib/
|  |  |  ... lots of JARs   
|  
|  run.sh
Enter fullscreen mode Exit fullscreen mode

D. Edit the shell script

After being bundled (in later steps), the shell script cannot be directly run by the user clicking on it. As such, we need to make the script more independent.

One dependence that the script from Method 2 has is the assumption that the user has the java command installed on their machine. This time, we don't make this assumption and use the java from the Java Runtime copied to your-bundling-directory/jre/.

Shell scripts like Bash are also dependent on where the user calls them from. After being bundled, the user no longer calls the run.sh script directly. But it is still called by the bundling tool's runtime service when the user clicks on the executable. Therefore, we need to make sure the paths inside the script are all relative to the script's location.

In Bash, there is a special variable called BASH_SOURCE which contains the path to the directory where the Bash script is located. We will use this as a way to have relative paths.

Declare a variable in your run.sh called HERE. Pay attention to the spacing around the assignment operator =. It matters in Bash!

HERE=${BASH_SOURCE%/*}
Enter fullscreen mode Exit fullscreen mode

In the above assignment, we make use of another Bash feature, string manipulation. This is done with the curly braces. In our case, we use the percent symbol % to truncate the / symbol and everything after inside the variable. * is the wild card operator from regular expressions. Since BASH_SOURCE contains a path to a directory, it ends with a slash on Linux and we don't want that.

Modify the java command of your run.sh.

"$HERE/jre/bin/java" --module-path "$HERE/javafx-sdk-20.0.1/lib" --add-modules javafx.controls,javafx.fxml,javafx.graphics,javafx.media -jar "$HERE/YourOwnJAR.jar" "$@" 
Enter fullscreen mode Exit fullscreen mode

Now, all the paths starts with the $HERE variable, which means it is all relative to the location of the run.sh script. In Bash, we reference variable using the $ symbol followed by the name of the variable. Notice here we put all the paths containing $HERE in double quotes. That is to take care of potential spaces inside path names, such as "Program Files".

You may also notice the last argument is "$@". This is also one of the default variables in Bash. $@ is a special variable that represents all the parameters the run.sh script has received through the command line. If your Java application takes command line arguments, this is absolutely necessary. Otherwise, it doesn't hurt to have it. This special variable is also enclosed in double quotes, because some arguments may contain white space themselves and that can cause argument splitting issues.

In summary, this is what a run.sh can look like for independent bundling.

#!/bin/bash

HERE=${BASH_SOURCE%/*}

"$HERE/jre/bin/java" --module-path "$HERE/javafx-sdk-20.0.1/lib" --add-modules javafx.controls,javafx.fxml,javafx.graphics,javafx.media -jar "$HERE/YourOwnJAR.jar" "$@"
Enter fullscreen mode Exit fullscreen mode

Don't forget to test your shell script. Assuming you are outside the bundling directory, you can run the script through the command line.

./bundle/run.sh
Enter fullscreen mode Exit fullscreen mode

E. Use warp-packer to produce a binary

Download the warp-packer tool to your system.

If you put the tool next to your bundling directory, then you can run the following commands.

./warp-packer --arch linux-x64 --input_dir your-bundling-directory --exec run.sh --output app.bin
chmod +x app.bin
Enter fullscreen mode Exit fullscreen mode

In the first command,

  • --arch: the architecture you are deploying to. linux-x64 is the Linux operating system with x64 hardware.
  • --input_dir: the bundling directory you just prepared all this time
  • --exec: the executable shell script that runs when the binary is clicked
  • --output: the name of the produced binary with an extension of your choosing.

The second command grants permission to run the binary.

Test your binary.

./app.bin
Enter fullscreen mode Exit fullscreen mode

In the JavaFX example, the binary size is about 108M for one of my students' projects. This is quite expected if you have resources like images in your Java project. Even after warp-packer compresses your files, most of the space is still taken by the Java Runtime Environment.

Competing tools for bundling Java

In a previous version of this article, I recommended jlink and jpackage for bundling Java into a native application. For simple applications, that can still work. However, these default tools in Java are no longer as reliable and as flexible as before. You can still find tutorials on them, but they only teach you how to bundle "Hello World"-level apps with little dependencies. This article dives deeper into software deployment with problematic Java dependencies such as JavaFX. As of the update of this article, I still couldn't get the default JDK tools to work with JavaFX.

Conclusion

In this blog, I explained 3 ways of deploying your Java project. Each method depends on your target user base. If they are okay with having some installed software on their machine. Delivering a JAR or a group of JARs + script is an acceptable solution. However, if you cannot expect your user to have any software installed, you should bundle everything into a standalone executable.

Top comments (0)