The gap between development and production environments
https://github.com/creditkarma/maven-exec-jar-plugin
The Maven executable jar plugin was made at Credit Karma by developers who wanted their local environment to match their application’s production environment. In other deployment formats used by Java/Scala apps, jars are smashed together into either one “uber” jar or zipped into a file that must be uncompressed before running the application with a generated bash script. Both approaches work, but each have their drawbacks. With the uber jar, bad merges of all the jars can cause unpredictable and nasty bugs. With the latter approach, there are security issues that can crop up due to a simple bash script being responsible for gluing the jars into a running JVM application.
We wanted a more elegant solution. So my team created an executable jar format that uses its own classloader to load classes and resources from a jar of jars. This allowed for developer classpaths to be used in production deployments with all executable code coming from one signed jar. It removed the possibility for weird bugs due to different classpaths being used in development and production environments.
Here is how our new plugin for a single, signed executable jar file for Scala projects works:
- A JarClassLoader is compiled that enables class files to load from jar files embedded inside of a jar file
- A main wrapper is generated that calls either the main-class indicated by the jar file generated by the maven-jar-plugin, or a main-class specified in the configuration of the exec-jar-plugin’s execution
- A new jar file is generated that contains the same .class files as the jar generated by maven-jar-plugin plus all the dependent jars (including the transitive closures i.e. the jars that the dependent jars depend upon) and any resources and additional dependencies indicated in the configuration of the exec-jar-plugin
- A launcher script is generated by a template that launches the executable jar. Be careful renaming the launcher script as it uses its own name to determine which jar file to exec
A simple example with one main-class (i.e. entry point)
<build>
<plugins>
...
<plugin>
<groupId>com.creditkarma</groupId>
<artifactId>exec-jar-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<id>exec-jar</id>
<configuration>
<attachToBuild>true</attachToBuild>
</configuration>
<goals>
<goal>exec-jar</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
In this, a fragment of XML is put into the build section of a pom.xml, and a jar is generated by the exec-jar-plugin. If the jar generated by the maven-jar-plugin is called foo-1.0.jar, then the generated executable jar will be named foo-1.0.executable.jar. In addition, a foo-1.0.executable.sh file will be generated that can launch the executable jar. In this example the launcher script would contain:
#!/bin/bash
SCRIPT_NAME=${0}
JAR_DIR_PREFIX=$(dirname "${SCRIPT_NAME}")
java -jar $JAR_DIR_PREFIX/JAR_NAME $@
Including .so or .dll files in the executable jar
Sometimes you need to use native libraries or Java agents in your application. To support this, the exec-jar plugin provides two pieces of functionality. First, you can include .so and .dll files directly in the executable jar file. Do this by using the binlibs tag and including a set of files to be included into the executable jar file. Then in your launcher script, extract out the necessary .so or .dll files and they will be automagically added to your LD_LIBRARY_PATH for use by your application.
Second, the generated launcher script can be customized. To do so, just create a file called script.tpl and place it into the same directory as the pom.xml file. This allows you to customize the generated launcher script. The default launcher script is:
#!/bin/bash
SCRIPT_NAME=${0}
JAR_DIR_PREFIX=$(dirname "${SCRIPT_NAME}")
java EXTRA_ARGS -jar $JAR_DIR_PREFIX/JAR_NAME $@
In this script template you can place extra JVM arguments, javaagent calls and any other command line argument to Java that can’t be passed into your application any other way. The EXTRA_ARGS part is replaced by the value you place into the extraLauncherArgs which defaults to nothing (i.e. the empty string).
Here is a real world example that launches with the correct extra information to start Kamon:
<build>
<plugins>
...
<plugin>
<groupId>com.creditkarma</groupId>
<artifactId>exec-jar-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<id>exec-jar</id>
<configuration>
<attachToBuild>true</attachToBuild>
<binlibs>
<fileSet>
<directory>${nativeDir}</directory>
<includes>
<include>libsigar-amd64-linux.so</include>
<include>libsigar-ia64-linux.so</include>
<include>libsigar-x86-linux.so</include>
<include>libsigar-universal-macosx.dylib</include>
<include>libsigar-universal64-macosx.dylib</include>
</includes>
</fileSet>
</binlibs>
<extraLauncherArgs>-Dkamon.auto-start=true</extraLauncherArgs>
</configuration>
<goals>
<goal>exec-jar</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
…and the customized script template (i.e. script.tpl):
#!/bin/bash
SCRIPT_NAME=${0}
JAR_DIR_PREFIX=$(dirname "${SCRIPT_NAME}")
jar xf $JAR_DIR_PREFIX/JAR_NAME lib/aspectjweaver-1.8.9.jar
java -javaagent:lib/aspectjweaver-1.8.9.jar EXTRA_ARGS -jar $JAR_DIR_PREFIX/JAR_NAME $@
An example with a custom filename
To set a custom filename, two values need to be added to the configuration:
- classifier defaults to executable and, if set, it replaces executable in the name of the generated executable jar file
- filename is the name of the executable jar file to be generated
These two values are used at different places in the jar generation process and they do not have to match.However, that will result in weird changes in the executable jar file name when it is published to Artifactory.
<build>
<plugins>
...
<plugin>
<groupId>com.creditkarma</groupId>
<artifactId>exec-jar-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<id>exec-jar</id>
<configuration>
<attachToBuild>true</attachToBuild>
<classifier>consumer</classifier>
<filename>${project.build.finalName}.consumer</filename>
<mainclass>com.creditkarma.boot.ConsumerMain</mainclass>
<scriptTemplate>src/main/resources/myApp.sh.tpl</scriptTemplate>
</configuration>
<goals>
<goal>exec-jar</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
Additionally, there are several other tags included in this example:
- mainclass is the fully qualified name of the main class that this executable jar should use
- scriptTemplate is a template that can be overridden to allow for custom launcher scripts to be generated.
Generating multiple executable jars for multiple entry points
An example showing two similar entry points:
<build>
<plugins>
...
<plugin>
<groupId>com.creditkarma</groupId>
<artifactId>exec-jar-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<id>exec-jar</id>
<configuration>
<attachToBuild>true</attachToBuild>
<classifier>consumer</classifier>
<filename>${project.build.finalName}.consumer</filename>
<mainclass>com.creditkarma.boot.ConsumerMain</mainclass>
<scriptTemplate>src/main/resources/myApp.sh.tpl</scriptTemplate>
</configuration>
<goals>
<goal>exec-jar</goal>
</goals>
</execution>
<execution>
<id>exec2-jar</id>
<configuration>
<attachToBuild>true</attachToBuild>
<classifier>consumer2</classifier>
<filename>${project.build.finalName}.consumer2</filename>
<mainclass>com.creditkarma.boot.ConsumerMain2</mainclass>
<scriptTemplate>src/main/resources/myApp.sh.tpl</scriptTemplate>
</configuration>
<goals>
<goal>exec-jar</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
Putting it all together
A complete example:
<build>
<plugins>
...
<plugin>
<groupId>com.creditkarma</groupId>
<artifactId>exec-jar-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<id>consumer-exec-jar</id>
<configuration>
<attachToBuild>true</attachToBuild>
<classifier>consumer</classifier>
<filename>${project.build.finalName}.consumer</filename>
<mainclass>com.creditkarma.boot.ConsumerMain</mainclass>
<scriptTemplate>src/main/resources/myApp.sh.tpl</scriptTemplate>
<binlibs>
<fileSet>
<directory>${nativeDir}</directory>
<includes>
<include>libsigar-amd64-linux.so</include>
<include>libsigar-ia64-linux.so</include>
<include>libsigar-x86-linux.so</include>
<include>libsigar-universal-macosx.dylib</include>
<include>libsigar-universal64-macosx.dylib</include>
</includes>
</fileSet>
</binlibs>
</configuration>
<goals>
<goal>exec-jar</goal>
</goals>
</execution>
<execution>
<id>printer-exec-jar</id>
<configuration>
<attachToBuild>true</attachToBuild>
<classifier>logFilePrinter</classifier>
<filename>${project.build.finalName}.logFilePrinter</filename>
<mainclass>com.creditkarma.boot.LogFilePrinterMain</mainclass>
<scriptTemplate>src/main/resources/myApp.sh.tpl</scriptTemplate>
<binlibs>
<fileSet>
<directory>${nativeDir}</directory>
<includes>
<include>libsigar-amd64-linux.so</include>
<include>libsigar-ia64-linux.so</include>
<include>libsigar-x86-linux.so</include>
<include>libsigar-universal-macosx.dylib</include>
<include>libsigar-universal64-macosx.dylib</include>
</includes>
</fileSet>
</binlibs>
</configuration>
<goals>
<goal>exec-jar</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
The future of Maven’s executable jar plugin
You can look out for two improvements in the future:
- Making the generated script template able to be re-named. Launcher scripts can’t be renamed as they use their own name to compute the name of the executable jar to run.
- Allowing the jar classloader to load the .class files into its own memory instead of using /tmp.
Closing the gap between development and production
In conclusion, the Maven exec-jar plugin created a more secure and more easily used distributable for our teams at Credit Karma. With this plugin you can build your executable and execute it in a secure fashion without intermediate steps – and with the exact same classpath as you use in development. This prevents unusual bugs in production caused by merging classes and classpaths together. Also, this reduces the gap between development and production environments.
If you try the plugin out, let us know how it worked for you @CreditKarmaEng!