Creating self-contained executable JARs

What is an executable JAR?

Main-Class: path.to.MainClass     #1
  1. MainClass has a static main(String…​ args) method

Handling the classpath

java -cp lib/one.jar;lib/two.jar;/var/lib/three.jar path.to.MainClass
  1. You need to define the same libraries in the same version
  2. More importantly, the -cp argument doesn’t work with JARs. To reference other JARs, the classpath needs to be set in a JAR’s manifest via the Class-Path attribute:
    Class-Path: lib/one.jar;lib/two.jar;/var/lib/three.jar
  3. For this reason, you need to put JARs in the same location, relative or absolute, on the target filesystem as per the manifest. That implies to open the JAR and read the manifest first.

The Apache Assembly plugin

<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef> <!--1-->
</descriptorRefs>
<archive>
<manifest>
<mainClass>
c.f.b.executablejar.ExecutableJarApplication <!--2-->
</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>single</goal> <!--3-->
</goals>
<phase>package</phase> <!--4-->
</execution>
</executions>
</plugin>
  1. Reference the pre-defined self-contained JAR configuration
  2. Set the main class to execute
  3. Execute the single goal
  4. Bind the goal to the package phase i.e. after the original JAR has been built
  1. <name>-<version>.jar
  2. <name>-<version>-with-dependencies.jar
java -jar target/executable-jar-0.0.1-SNAPSHOT.jar
%d [%thread] %-5level %logger - %msg%n java.lang.IllegalArgumentException:
No auto configuration classes found in META-INF/spring.factories.
If you are using a custom packaging, make sure that file is correct.

The Apache Shade plugin

  • ApacheLicenseResourceTransformer: Prevents license duplication
  • ApacheNoticeResourceTransformer: Prepares merged `NOTICE`
  • AppendingTransformer: Adds content to a resource
  • ComponentsXmlResourceTransformer: Aggregates Plexus `components.xml`
  • DontIncludeResourceTransformer: Prevents inclusion of matching resources
  • GroovyResourceTransformer: Merges Apache Groovy extends modules
  • IncludeResourceTransformer: Adds files from the project
  • ManifestResourceTransformer: Sets entries in the `MANIFEST`
  • PluginXmlResourceTransformer: Aggregates Mavens `plugin.xml`
  • ResourceBundleAppendingTransformer: Merges ResourceBundles
  • ServicesResourceTransformer: Relocated class names in `META-INF/services` resources and merges them
  • XmlAppendingTransformer: Adds XML content to an XML resource
  • PropertiesTransformer: Merges properties files owning an ordinal to solve conflicts
  • OpenWebBeansPropertiesTransformer: Merges Apache OpenWebBeans configuration files
  • MicroprofileConfigTransformer: Merges conflicting Microprofile Config properties based on an ordinal
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<id>shade</id>
<goals>
<goal>shade</goal> <!--1-->
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <!--2-->
<mainClass> <!--3-->
c.f.b.executablejar.ExecutableJarApplication
</mainClass>
<manifestEntries>
<Multi-Release>true</Multi-Release> <!--4-->
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
  1. The shade goal is bound to the package phase by default
  2. This transformer is dedicated to generating manifest files
  3. Set the Main-Class entry
  4. Configure the final JAR to be a multi-release JAR. This is necessary when any of the initial JARs is a multi-release JAR
  1. <name>-<version>.jar: the self-contained executable JAR
  2. original-<name>-<version>.jar: the "normal" JAR without the embedded dependencies
  • META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat: This Log4J2 file contains pre-compiled Log4J2 plugin data. It's encoded in binary format and none of the out-of-the-box transformers can merge such files. Yet, a casual search reveals somebody already had this issue and released a transformer to handle the merge.
  • META-INF/spring.factories: These Spring-specific files have a single key/multiple values format. While they are text-based, no out-of-the-box transformer can merge them correctly. However, the Spring developers provide this capability (and much more) in their plugin.
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>
c.f.b.executablejar.ExecutableJarApplication
</mainClass>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</transformer>
<transformer implementation="com.github.edwgiz.maven_shade_plugin.log4j2_cache_transformer.PluginsCacheFileTransformer" /> <!--1-->
<transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer"> <!--2-->
<resource>META-INF/spring.factories</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.edwgiz</groupId>
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId> <!--3-->
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <!--3-->
<version>2.4.1</version>
</dependency>
</dependencies>
</plugin>
  1. Merge Log4J2 .dat files
  2. Merge /META-INF/spring.factories files
  3. Add the required transformers code
  • Licenses, notices and similar files
  • Spring Boot specific files i.e. spring.handlers, spring.schemas and spring.tooling
  • Spring Boot-Kotlin specific files e.g. spring-boot.kotlin_module, spring-context.kotlin_module, etc.
  • Service loader configuration files

The Spring Boot plugin

<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
/
|__ BOOT-INF
| |__ classes #1
| |__ lib #2
|__ META-INF
| |__ MANIFEST.MF
|__ org
|__ springframework
|__ loader #3
  1. Project compiled classes
  2. JAR dependencies
  3. Spring Boot class-loading classes
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: ch.frankel.blog.executablejar.ExecutableJarApplication

Conclusion

  1. Assembly is a good fit for simple projects
  2. When the project starts being more complex and you need to handle duplicate files, use Shade
  3. Finally, for Spring Boot projects, your best bet is the dedicated plugin

To go further:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store