Building “executable” JAR files with Maven

In this article, I will describe how to use Apache Maven to build “executable” JAR files. This is useful if you develop nontrivial applications, especially those using one or more other external JAR files.

Introduction

First, it’s essential to understand the meaning of “executable” JAR. At the fundamental level, a JAR file is “executable” when you can start your application from the command line using a simple command like:

> java -jar path-to-jar/yourapp.jar arguments...

Note that the path-to-jar/ is obviously optional (it depends on where the JAR file is physically located with respect to the current directory), and the arguments are also optional, depending on what the application requires (or not) as parameters.

Before continuing, it’s essential to keep clear that what I will say in the following sections is valid mainly for “basic” Java applications like command-line or GUI applications (Swing, JavaFX, etc.). Instead, it is not appropriate for complex applications that use frameworks like Spring Boot, Quarkus, Micronaut, etc. These frameworks generally have their own custom way of packaging applications. The same thing goes for Java EE applications, where packaging and execution are very different.

So, if you are in any of the “basic” scenarios, the critical point for building an “executable” JAR is to take control of the JAR’s manifest file.

The JAR manifest

The “manifest” is a special file contained in a JAR under the path META-INF/MANIFEST.MF, precisely with that name in upper-case. This file, in fact, is nothing particularly fancy since it is a simple text file that contains key-value pairs in the typical form of:

Key-Name: value

The manifest can contain various types of information that the Java VM and/or the application can use. There is a detailed Specification on JAR files at JAR File Specification (here for JavaSE 21), but you are not required to understand all of that document!

To build an executable JAR, you must set two essential attributes in the manifest: the Main-Class and the Class-Path.

Handwriting the manifest file is certainly possible but a bit tricky. Unfortunately, for historical reasons, the manifest file has some picky rules, which you can read about in the “Notes on Manifest and Signature Files” section of the Specification. It is generally much more preferable to use a build tool like Maven or Gradle to generate the manifest. This article focuses only on Maven.

The Main-Class attribute

This attribute specifies the FQN (Fully Qualified Name) of the application class that contains the well-known main(String[] args) method, which is the “entry point” of the application. For example, it is something like:

Main-Class: com.example.myapp.AppMain

This attribute technically makes a JAR file “executable” using the -jar option on the Java launcher executable (java or java.exe, depending on the platform). Without this attribute, you can still launch a Java application but need a more extended command.

Setting the Main-Class attribute is the first important step but is insufficient if your application depends on one or more other JAR files. For this, we have to control the Class-Path attribute.

The Class-Path attribute

This attribute specifies a list of JAR files (and/or directories) the Java VM will use to search classes and resources. It is conceptually similar to the -classpath (-cp abbreviated) option of the JDK tools but with a different syntax.

The Class-Path attribute must list JAR files separated by one space and specified using relative URLs, such as path/to/lib1.jar. You must use slashes (“/”), not backslashes. All these URLs are relative to the JAR file containing this manifest.

A typical example of this attribute is:

Class-Path: lib/somelib1.jar lib/somelib2.jar lib/somelib3.jar

As stated above, the manifest structure has some picky rules, so it is advised to avoid handwriting the manifest. There is also the restriction to navigate to a parent directory ("../") except when the main JAR is loaded from the file system. But in general, you should avoid going upwards on parent folders.

If you understand these two attributes, we can continue to see how to set up the manifest using Maven.

Building an “executable” JAR using Maven

Apache Maven is a project management tool heavily based on “plugins”. In fact, plugins are the core components that perform practically everything from compiling to packaging and more.

Two plugins in particular are helpful to create an executable JAR:

As a first step, it is handy to set up two simple custom properties in your pom.xml file:

<properties>
           .....
    <project.main.class>com.example.myapp.AppMain</project.main.class>
    <project.dependencies.directory>lib/</project.dependencies.directory>
</properties>

Note that the property names are absolutely at your own discretion. I chose project.main.class and project.dependencies.directory for good sense, but you can use anything else not used by Maven itself or other plugins. Also, the name of the library directory can be changed, for example, to libs/, libraries/, deps/, etc.

Configuring the maven-jar-plugin

At this point, we can configure the maven-jar-plugin, which physically builds your project’s JAR file.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.4.1</version>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <classpathPrefix>${project.dependencies.directory}</classpathPrefix>
                <mainClass>${project.main.class}</mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

As you can see, we are configuring the <manifest> under <archive> with three basic parameters:

  • <addClasspath> with a true value means that the Class-Path attribute will be added to the manifest (otherwise, no).
  • <classpathPrefix> tells the plugin to prefix all dependencies with our lib/ prefix. For instance, if you use the Apache commons-lang3 library version 3.14.0, then the Class-Path will contain lib/commons-lang3-3.14.0.jar.
  • <mainClass> specifies the main class from our custom property.

This is already a good result because the JAR file will have a manifest that contains, for example:

Class-Path: lib/commons-lang3-3.14.0.jar lib/xxxx.jar ............[other dependencies]
Main-Class: com.example.myapp.AppMain

It’s also evident that if your project has no dependencies, you can skip and ignore the <addClasspath> and <classpathPrefix> parameters. However, the above step is insufficient if your project has one or more dependencies.

Indeed, the Class-Path attribute is correct, but the JAR files of dependencies still need to be physically present in a lib/ directory under your <project>/target directory! To perform this step, you need to use the maven-dependency-plugin.

Configuring the maven-dependency-plugin

The maven-dependency-plugin provides several goals for performing various general operations on dependencies. One of these goals is, not surprisingly, called copy-dependencies.

The copy-dependencies goal lets you copy all the required dependencies from your local Maven repo (~/.m2/repository) to a directory of your choice. You can also use specific parameters to rename the files and strip the version (features not used in this article).

Warning: this plugin also provides a goal named “copy”. Do not confuse this with copy-dependencies. The “copy” goal is meant for other purposes and requires either the <artifact> or <artifactItems> tag.

So, add also the following plugin to your pom.xml:

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

As you can see, we are configuring a custom execution of the copy-dependencies goal, which is tied to the package phase. Thus, it runs only when you launch at least an mvn package (or any later phase like install or deploy).

There is one essential and important parameter to use:

  • <outputDirectory> obviously tells where to copy the dependencies. In our case, the path will be like:
       path-to/<project>/target/lib/

Final result

If you have correctly configured the maven-jar-plugin plus the maven-dependency-plugin if you have any dependencies, you should find the following structure under the target/ directory after an mvn package command:

yourapp.jar
lib/dependency1.jar
lib/dependency2.jar
lib/...other dependencies...

If you keep this “relative” structure intact, you can copy your JAR file and the lib directory anywhere you want on your local drive, pen drives, etc. The application will continue to start correctly.

Similar Posts