3 Ways to Package Your Java Project into a JAR for Deployment
Tags: tutorial, java, devops, beginners
Published: 2023-05-07 16:51:42

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](https://www.geeksforgeeks.org/why-is-java-write-once-and-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.

```gradle
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) }
    }
}
```

Then run this on the command line. 
```bash
gradle jar
```

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](https://gluonhq.com/products/javafx/) 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](https://git-scm.com/downloads) 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](https://en.wikipedia.org/wiki/Shebang_(Unix)). 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.
```bash
#!/bin/bash
```

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.
```bash
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
```

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
```

Example `run.sh` for a JavaFX project
```bash
#!/bin/bash
java --module-path javafx-sdk-20.0.1/lib --add-modules javafx.controls,javafx.fxml,javafx.graphics,javafx.media -jar YourOwnJar.jar
```

Don't forget to grant permission to the script with `chmod`.
```bash
chmod +x run.sh
```

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](https://adoptium.net/temurin/releases/) 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
```

### 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!
```bash
HERE=${BASH_SOURCE%/*}
```

In the above assignment, we make use of another Bash feature, [string manipulation](https://www.linux.com/topic/desktop/all-about-curly-braces-bash/). 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`.
```bash
"$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" "$@" 
```

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.
```bash
#!/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" "$@"
```

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

### E. Use `warp-packer` to produce a binary
Download the [`warp-packer`](https://github.com/dgiagio/warp) tool to your system.

If you put the tool next to your bundling directory, then you can run the following commands.
```bash
./warp-packer --arch linux-x64 --input_dir your-bundling-directory --exec run.sh --output app.bin
chmod +x app.bin
```

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.
```bash
./app.bin
```

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.