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
Thecopy
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
Thecopy-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:
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:
❯ 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?
jlink
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.
You can also use JReleaser to create cross-platform jlink distributions. |
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!
Links
JReleaser: https://jreleaser.org/
JReleaser GitHub: https://github.com/jreleaser/jreleaser
JReleaser Maven Plugin: https://jreleaser.org/guide/latest/tools/jreleaser-maven.html
JReleaser JavaArchive Assembly: https://jreleaser.org/guide/latest/reference/assemble/java-archive.html
Apache Maven: https://maven.apache.org/
Apache Maven Dependency Plugin (copy-dependencies): https://maven.apache.org/plugins/maven-dependency-plugin/copy-dependencies-mojo.html
Appassembler Maven Plugin: https://github.com/mojohaus/appassembler
MemeforceHunt: https://github.com/alttpj/MemeforceHunt