Turn your JAR (with dependencies) into a single self-executing file.
No more java -jar — just run it directly on Linux and macOS.
Features
- Single self-executing file combining a small POSIX
shlauncher with a runnable fat JAR - Automatic Java binary discovery (JAVA_HOME, PATH, common install locations)
- Min/max Java version checking and clear error messages when requirements aren't met
- Works on Linux and macOS
- Deterministic, reproducible output suitable for packaging
The execjar-maven-plugin is intended to run in the package phase and produce an additional executable artifact (it does not replace the original JAR).
Important: This plugin requires a fat JAR (uber JAR with all dependencies included). You must configure maven-assembly-plugin or maven-shade-plugin before the execjar plugin in your pom.xml.
The following is the relevant part of a pom.xml (see example-project/pom.xml) that creates a fat JAR with dependencies:
<groupId>com.example</groupId>
<artifactId>hello-execjar</artifactId>
<!-- ... -->
<build>
<plugins>
<!-- Step 1: Create fat JAR with dependencies -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.example.HelloExecJar</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Step 2: Create executable from fat JAR -->
<plugin>
<groupId>me.bechberger</groupId>
<artifactId>execjar</artifactId>
<version>0.1.1</version>
<executions>
<execution>
<goals>
<goal>execjar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>You can then run the resulting executable (e.g., target/hello-execjar) directly:
> ./target/hello-execjar
Hello from Exec Jar!Basic options:
| Option | Description | Default |
|---|---|---|
minJavaVersion |
Minimum required Java major version (e.g., 17) |
Auto-detected from maven.compiler.release or maven.compiler.target |
maxJavaVersion |
Maximum allowed Java major version (e.g., 21) |
No upper limit |
strictMode |
Require exact Java version match (use only java from PATH) |
false |
skip |
Skip plugin execution | false |
validateWithShellcheck |
Validate launcher script with shellcheck | false |
Additional Options
| Option | Description | Default |
|---|---|---|
jarFile |
Path to input JAR file | Auto-detected: ${project.build.directory}/${project.build.finalName}-${classifier}.jar |
classifier |
Classifier for the fat JAR | jar-with-dependencies |
outputFile |
Output path for the executable | ${project.build.directory}/${project.artifactId} |
| Option | Description | Default |
|---|---|---|
jvmOpts |
JVM options embedded in the launcher (e.g., -Xmx1g -XX:+UseG1GC) |
None |
javaProperties |
Java system properties as key-value pairs (converted to -Dkey=value) |
None |
environmentVariables |
Environment variables to set before launching | None |
prependArgs |
Arguments prepended before user-provided arguments | None |
appendArgs |
Arguments appended after user-provided arguments | None |
The plugin looks for ${project.build.finalName}-${classifier}.jar by default (e.g., myapp-1.0-jar-with-dependencies.jar).
Advanced Configuration Example
For a comprehensive example with Java properties, environment variables, and argument handling, see the advanced-config-test integration test:
<plugin>
<groupId>me.bechberger</groupId>
<artifactId>execjar</artifactId>
<version>0.1.1</version>
<executions>
<execution>
<goals>
<goal>execjar</goal>
</goals>
</execution>
</executions>
<configuration>
<minJavaVersion>17</minJavaVersion>
<maxJavaVersion>21</maxJavaVersion>
<!-- JVM options -->
<jvmOpts>-Xmx1g -XX:+UseG1GC</jvmOpts>
<!-- Java system properties (supports spaces and special characters) -->
<javaProperties>
<app.name>My Application</app.name>
<app.config>/path/to/my app/config</app.config>
<app.message>Hello World & Friends</app.message>
</javaProperties>
<!-- Environment variables -->
<environmentVariables>
<MY_APP_NAME>My Application</MY_APP_NAME>
<MY_APP_PATH>/opt/my app/bin</MY_APP_PATH>
</environmentVariables>
<!-- Arguments: prepended before user args, appended after -->
<prependArgs>--config "/path/to/config.yml"</prependArgs>
<appendArgs>--log-level info</appendArgs>
</configuration>
</plugin>With this configuration, running ./myapp --user-arg results in:
java -Xmx1g -XX:+UseG1GC \
-Dapp.name="My Application" \
-Dapp.config="/path/to/my app/config" \
-Dapp.message="Hello World & Friends" \
-jar myapp \
--config "/path/to/config.yml" \
--user-arg \
--log-level infoDownload the latest JAR or executable from the releases page or use JBang:
jbang execjar@parttimenerd/execjarCreate an executable from a runnable fat JAR:
./execjar input.jar -o myappUsage: execjar <jarFile> [options]
Arguments:
<jarFile> Input JAR file (must be a runnable fat JAR)
Options:
-o, --output <path> Output path for the executable
Default: <jar-name-without-.jar>
--min-java-version <major> Minimum required Java version (e.g., 17)
Default: auto-detected from JAR manifest
--max-java-version <major> Maximum allowed Java version (e.g., 21)
Default: no upper limit
--strict-mode Require exact Java version match
(use only 'java' from PATH)
Default: false
--jvm-opts <options> JVM options embedded in launcher
Example: "-Xmx1g -XX:+UseG1GC"
-D, --property <key=value> Java system property (can be repeated)
Example: -D app.name="My App"
-E, --env <key=value> Environment variable (can be repeated)
Example: -E MY_VAR="value"
--prepend-args <args> Arguments prepended before user args
Example: --prepend-args "--config /etc/app.conf"
--append-args <args> Arguments appended after user args
Example: --append-args "--log-level debug"
--validate-shellcheck Validate generated launcher with shellcheck
Default: false
-h, --help Show this help message
-v, --version Show version information
# Simple executable with Java version requirement
./execjar myapp.jar -o myapp --min-java-version 17
# With JVM options and Java properties
./execjar myapp.jar -o myapp \
--jvm-opts "-Xmx2g -XX:+UseZGC" \
-D app.name="My Application" \
-D app.config="/etc/myapp/config.yml"
# With environment variables and default arguments
./execjar myapp.jar -o myapp \
-E MY_APP_HOME="/opt/myapp" \
--prepend-args "--config /etc/myapp.conf" \
--append-args "--verbose"
# Strict mode for containerized environments
./execjar myapp.jar -o myapp \
--min-java-version 21 \
--max-java-version 21 \
--strict-mode
# Validate launcher script with shellcheck
./execjar myapp.jar -o myapp --validate-shellcheckThe tool creates a single executable file with:
- POSIX
shlauncher prepended to the JAR bytes - Embedded configuration (Java version requirements, JVM options, etc.)
- File layout:
#!/usr/bin/env shscript + original JAR data - Executable permissions set automatically
- No external dependencies required
The sections below describe internal implementation and launcher behavior (useful for debugging and advanced configuration).
Background: JAR files are ZIP files, and prepending data to a ZIP file is allowed as ZIP readers ignore leading bytes before the ZIP header as they look for the End of Central Directory (EOCD) record at the end of the file which contains offsets to all entries (see ZIP specification). This property enables the creation of self-executing JAR files by adding a launcher script at the beginning.
Launcher highlights
- Shebang:
#!/usr/bin/env sh— fully POSIXshcompatible - Template variables embedded into the launcher:
artifactName— used for human-friendly error messagesminJavaVersion,maxJavaVersion— version constraintsstrictMode— whether to search for Java or only use PATHjvmOpts— JVM optionsjavaProperties— Java system properties (-D flags)environmentVariables— environment variables to exportprependArgs,appendArgs— default arguments
- Debug mode: set
EXECJAR_DEBUG=1to print selected Java executable, detected version, resolved JVM options and the finalexeccommand - JVM option parsing mirrors async-profiler semantics:
-D*,-X*,-agent*→ passed to JVM-J<opt>→ treated as JVM option (leading-Jstripped)- First non-JVM option terminates JVM-option parsing and remaining args are passed to the application
Java discovery and version handling
The launcher searches for a usable java in this order:
$JAVA_HOME/bin/javajavaonPATH/etc/alternatives/java- Common JVM installation locations:
- macOS:
/Library/Java/JavaVirtualMachines/*/Contents/Home/bin/java - Linux:
/usr/lib/jvm/*/bin/java
- macOS:
For each candidate the launcher:
-
Extracts the major Java version (supports Java 8+):
- First tries to read from
releasefile (e.g.,$JAVA_HOME/release) which is much faster - Falls back to executing
java -versionif the release file is not found or cannot be parsed - The
releasefile containsJAVA_VERSION(e.g.,"17"or"26") which is parsed directly
- First tries to read from
-
Validates versions against the configured min/max range
-
Uses the first valid Java found (stops searching once a compatible version is found)
-
Exits with a clear error message if no compatible Java is found
-
Input must be a valid JAR with
META-INF/MANIFEST.MFcontainingMain-Class -
The tool validates the JAR and fails fast on missing
Main-Classor invalid JAR -
Output is created next to the input JAR by default and is marked executable
-
The tool overwrites the output by default (unless input == output)
GraalVM native image is a powerful technology for creating standalone executables with fast startup times, but it comes with some trade-offs:
- Binaries are platform-specific, requiring separate builds for each target OS/architecture (and some platforms are not supported at all)
- Binaries are far larger
- Build times are over 10 times longer due to ahead-of-time compilation
- Some Java features (like dynamic class loading, reflection, JNI) require additional configuration and may not work out-of-the-box
This is why execjar is better suited for small tools like jstall where ease of use, small size, and cross-platform compatibility are more important than absolute performance. Regarding startup time, the execjar jstall starts in around 160ms vs 60ms for the native image version, which seems to be acceptable.
mvn clean packageThe project includes comprehensive JUnit-based integration tests for both the Maven plugin and CLI tool:
# Run all tests (Maven plugin integration tests + unit tests)
./run-tests.sh
# Run only Maven plugin integration tests
./run-tests.sh --maven-only
# Run only unit tests (including CLI tests)
./run-tests.sh --unit-only
# Or use Maven directly
mvn clean install -DskipTests # Install plugin first
mvn verify -Prun-its # Run integration testsPlease file issues and feature requests on the project's GitHub issue tracker: https://github.com/parttimenerd/execjar/issues
Contributions are welcome — please follow typical PR practices (small focused changes, tests for new behavior).
Apache License 2.0
Copyright 2026 SAP SE or an SAP affiliate company, Johannes Bechberger and contributors