To main content

Creating an App Distribution with Apache Maven and JReleaser

Published by Benjamin Marwell on

A common question among those new to Apache Maven and Java is how to create an application distribution. This article guides you through creating an application distribution using Apache Maven and the powerful JReleaser tool, including its Maven plugin.

A note about the example, MemeforceHunt: This is a JavaFX application which is in fact not a great example, as it uses JavaFX. The resulting image will only run on Linux (or the platform where you execute the build) because of the way I set up the project. Your typical Java CLI application is not OS/arch specific and does not have this issue.

Prerequisites

You need a command-line interface (CLI) application to distribute. Ensure it has a main method and is executable.

Assembling the dependencies

First, collect all compile and runtime dependencies of your application. For this, you can use the maven-dependency-plugin. It offers two goals that sound similar but serve distinct purposes:

  • copy
    The copy goal will copy arbitrary artifacts from your local ~/.m2/repository folder to a directory. This is not what we want, as we want to copy the dependencies of our project.

  • copy-dependencies
    The copy-dependencies goal will copy all dependencies of your project to a directory.

Here is an example configuration of the maven-dependency-plugin:

<project>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <executions>
          <execution>
            <id>collect-dependencies</id>
            <phase>package</phase>
            <goals>
              <goal>copy-dependencies</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.directory}/libs</outputDirectory>
              <prependGroupId>true</prependGroupId>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Some comments here:

  • Prepending the group ID is optional but recommended to identify dependencies easily.

  • It also prevents conflicts between dependencies with identical artifact IDs but different group IDs.

  • The ID of the execution is arbitrary, but you should choose a meaningful name.

  • Use the package phase to collect dependencies just before packaging your application.

  • The output directory is also arbitrary, but you should choose a meaningful name.

If configured correctly, you should have a directory containing all project dependencies, as shown below:

Dependencies assembled in a directory
Figure 1. Dependencies in the libs directory

Creating the distribution using JReleaser

While JReleaser is typically a standalone CLI tool, its creator, Andres, also provides a Maven plugin (jreleaser-maven-plugin). JReleaser builds on and enhances the execution and assembly templates from MojoHaus’s Appassembler plugin.

To configure the plugin, you need to add the following to your pom.xml.

Make sure it comes AFTER the definition of both the maven-jar-plugin and the maven-dependency-plugin, as the assembly requires both the final jar to be built and the dependencies to be assembled!
<project>
  <build>
    <plugins>
      <plugin>
        <groupId>org.jreleaser</groupId>
        <artifactId>jreleaser-maven-plugin</artifactId>
        <version>1.17.0</version>
        <executions>
          <execution>
            <id>assemble-archive</id>
            <phase>package</phase>
            <goals>
              <goal>assemble</goal>
            </goals>
            <configuration>
              <jreleaser>
                <project>
                  <links>
                    <homepage>https://github.com/alttpj/MemeforceHunt</homepage>
                  </links>
                </project>
                <assemble>
                  <javaArchive>
                    <memeforcehunt>
                      <active>ALWAYS</active>
                      <stereotype>DESKTOP</stereotype>
                      <formats>
                        <format>ZIP</format>
                        <format>TGZ</format>
                      </formats>
                      <options>
                        <longFileMode>POSIX</longFileMode>
                        <bigNumberMode>POSIX</bigNumberMode>
                      </options>
                      <java>
                        <mainClass>${main.class}</mainClass>
                      </java>
                      <mainJar>
                        <path>${project.build.directory}/${project.build.finalName}.jar</path>
                      </mainJar>
                      <fileSets>
                        <fileSet>
                          <input>${project.build.directory}/libs/</input>
                          <output>lib/</output>
                          <includes>
                            <include>*.jar</include>
                          </includes>
                        </fileSet>
                      </fileSets>
                    </memeforcehunt>
                  </javaArchive>
                </assemble>
              </jreleaser>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

For a complete example, see this pull request: https://github.com/alttpj/MemeforceHunt/pull/80

This is how the final layout looks after invoking `mvn package:

Final distribution by JReleaser
❯ tree .
.
├── bin
│   ├── memeforcehunt
│   └── memeforcehunt.bat
└── lib
    ├── com.fasterxml.jackson.core.jackson-annotations-2.18.3.jar
    ├── com.fasterxml.jackson.core.jackson-core-2.18.3.jar
    ├── com.fasterxml.jackson.core.jackson-databind-2.18.3.jar
    ├── com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.18.3.jar
    ├── com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.18.3.jar
    ├── com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.18.3.jar
    ├── com.github.spotbugs.spotbugs-annotations-4.9.1.jar
    ├── com.google.code.findbugs.jsr305-3.0.2.jar
    ├── info.picocli.picocli-4.7.6.jar
    ├── io.github.alttpj.library.alttpj-library-1.0.0-SNAPSHOT.jar
    ├── io.github.alttpj.memeforcehunt.memeforcehunt-common-value-2.2.0-SNAPSHOT.jar
    ├── io.github.alttpj.memeforcehunt.memforcehunt-common-sprites-2.2.0-SNAPSHOT.jar
    ├── io.github.alttpj.memeforcehunt.memforcehunt-lib-2.2.0-SNAPSHOT.jar
    ├── memeforce-app-2.2.0-SNAPSHOT.jar
    ├── org.controlsfx.controlsfx-11.0.0.jar
    ├── org.jfxtras.jmetro-11.6.10.jar
    ├── org.openjfx.javafx-base-21.0.6-linux.jar
    ├── org.openjfx.javafx-controls-21.0.6-linux.jar
    ├── org.openjfx.javafx-fxml-21.0.6-linux.jar
    ├── org.openjfx.javafx-graphics-21.0.6-linux.jar
    ├── org.openjfx.javafx-media-21.0.6-linux.jar
    ├── org.openjfx.javafx-swing-21.0.6-linux.jar
    ├── org.openjfx.javafx-web-21.0.6-linux.jar
    └── org.yaml.snakeyaml-2.4.jar

You can include configuration files in the distribution, placing them in the lib directory or a dedicated config directory. As the JReleaser plugin allows customization options for both system properties and the environment variables per Operating System, you can define the location to your needs.

Why not use other tools?

We could of course use jlink to create a custom runtime image of our application. However, jlink distributions are significantly larger than those created by JReleaser. The reason is that jlink distributions contain a substantial subset of the JRE. As there is a good chance a user might already have a JRE installed, this is not necessary.

However, I used to use jlink to create a custom runtime image of my application. It worked well and might be a good addition to a release. Just make sure to create a jlink distribution for each combination of OS and architecture.

Maven Assembly Plugin & Appassembler Maven Plugin (Mojohaus)

The maven-assembly-plugin is a great tool to create distributions of your application. In fact, I use the maven-assembly-plugin for source code distributions and other distributions. But it is not as powerful as JReleaser: JReleaser creates a startup script for both windows and unix systems.

To create additional startup scripts, the appassembler-maven-plugin was a good choice in the past. However, since JReleaser does the same and offers modular start scripts, there is no reason to use the appassembler-maven-plugin anymore.

One-Jar, Uno-Jar, and Spring Boot Repackage

The mentioned tools were great in the past, but they have lost their appeal for very important reasons. For one, One-Jar has a difficult license and is not maintained anymore. Uno-Jar is not maintained anymore either and has a difficult license. Spring-Boot-Repackage is a great tool, but it has the same drawbacks as all the three plugins: If you repackage all dependencies into one single jar, you will:

  • Lose modularity, making maintenance harder

  • Lose JAR signing, which is critical for some libraries

  • Some JARs, like Bouncy Castle, require signing and may not function.

  • it uses a custom loader class which might cause problems

  • While spring’s repackage goal allows unpacking specific dependencies to a temporary location, this might not fix all problems.

That said, it might be a good idea to try the spring-boot-plugin to see if it fits your use-case. However, it still lacks the startup scripts for both windows and unix systems.

Note: One-Jar and Uno-Jar are no longer maintained and may not be suitable for modern projects.

Conclusion

JReleaser is an excellent tool for creating application distributions. I use it all the time, not just in public projects. Andres has done an outstanding job with JReleaser and its documentation, which becomes intuitive with use. He is also very responsive to issues and pull requests.

Recently, Andres introduced modular startup scripts. What you can see in the example above is not the modular version, but it will be automatically selected if you add a modulePath configuration option.

For questions, consult the JReleaser documentation or join the GitHub Discussions!