Tools for generating executable JAR/WAR files with embedded containers for Spring Boot applications
—
Tools for creating launch scripts that make JAR files directly executable on Unix-like systems. Launch scripts are prepended to JAR files, allowing them to be executed like native executables while maintaining JAR functionality.
Core interface for launch scripts that can be prepended to JAR files.
public interface LaunchScript {
/**
* Get the launch script content as a byte array.
* The script is prepended to the JAR file to make it executable.
*
* @return the script content as bytes
*/
byte[] toByteArray();
}Standard implementation that provides a configurable Unix shell script for launching JAR files.
public class DefaultLaunchScript implements LaunchScript {
/**
* Create a launch script from a template file with property substitution.
*
* @param file the script template file
* @param properties properties for template variable substitution
* @throws IOException if the template file cannot be read
*/
public DefaultLaunchScript(File file, Map<?, ?> properties) throws IOException;
/**
* Get the launch script content as bytes.
*
* @return the processed script content
*/
@Override
public byte[] toByteArray();
}import org.springframework.boot.loader.tools.*;
import java.io.File;
import java.util.Map;
// Create a basic launch script
File scriptTemplate = new File("src/main/scripts/launch.sh");
Map<String, String> properties = Map.of(
"initInfoProvides", "myapp",
"initInfoShortDescription", "My Spring Boot Application"
);
LaunchScript launchScript = new DefaultLaunchScript(scriptTemplate, properties);
// Use with JarWriter
File targetJar = new File("myapp.jar");
try (JarWriter writer = new JarWriter(targetJar, launchScript)) {
// Write JAR contents
// ... (JAR writing code)
}
// The resulting JAR can be executed directly:
// chmod +x myapp.jar
// ./myapp.jarimport org.springframework.boot.loader.tools.*;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
// Configure advanced launch script properties
Map<String, String> properties = new HashMap<>();
// Service information
properties.put("initInfoProvides", "myapp");
properties.put("initInfoShortDescription", "My Spring Boot Application");
properties.put("initInfoDescription", "A comprehensive Spring Boot application for data processing");
// Runtime configuration
properties.put("confFolder", "/etc/myapp");
properties.put("logFolder", "/var/log/myapp");
properties.put("pidFolder", "/var/run/myapp");
properties.put("logFilename", "myapp.log");
// JVM options
properties.put("javaOpts", "-Xmx1024m -Xms512m -Dspring.profiles.active=production");
// User and permissions
properties.put("runAsUser", "myapp");
properties.put("mode", "service"); // or "auto", "force", "run"
// File locations
properties.put("useStartStopDaemon", "true");
properties.put("stopWaitTime", "60");
File scriptTemplate = new File("launch-template.sh");
LaunchScript launchScript = new DefaultLaunchScript(scriptTemplate, properties);
// Create executable JAR
File targetJar = new File("myapp.jar");
try (JarWriter writer = new JarWriter(targetJar, launchScript)) {
// Write JAR contents...
}
// The JAR can now be used as a system service:
// sudo cp myapp.jar /etc/init.d/myapp
// sudo chmod +x /etc/init.d/myapp
// sudo update-rc.d myapp defaults
// sudo service myapp startCreate a custom launch script template (launch-template.sh):
#!/bin/bash
#
# {{initInfoProvides}} {{initInfoShortDescription}}
#
# chkconfig: 35 80 20
# description: {{initInfoDescription}}
#
. /lib/lsb/init-functions
USER="{{runAsUser}}"
DAEMON="{{initInfoProvides}}"
ROOT_DIR="/var/lib/{{initInfoProvides}}"
SERVER="$ROOT_DIR/{{initInfoProvides}}.jar"
LOCK_FILE="/var/lock/subsys/{{initInfoProvides}}"
start() {
log_daemon_msg "Starting $DAEMON"
if [ ! -f "$SERVER" ]; then
log_failure_msg "$SERVER not found"
exit 1
fi
if start-stop-daemon --start --quiet --oknodo --pidfile "$PID_FILE" \
--chuid "$USER" --background --make-pidfile \
--exec /usr/bin/java -- {{javaOpts}} -jar "$SERVER"
then
log_end_msg 0
touch "$LOCK_FILE"
else
log_end_msg 1
fi
}
stop() {
log_daemon_msg "Stopping $DAEMON"
if start-stop-daemon --stop --quiet --oknodo --pidfile "$PID_FILE"
then
log_end_msg 0
rm -f "$LOCK_FILE"
else
log_end_msg 1
fi
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
status)
status_of_proc -p $PID_FILE "$DAEMON" "$DAEMON" && exit 0 || exit $?
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0
# The actual JAR content follows after this scriptimport org.springframework.boot.loader.tools.*;
import java.io.File;
import java.util.Map;
// Create executable JAR with launch script using Repackager
File sourceJar = new File("target/myapp.jar");
File destination = new File("dist/myapp");
Repackager repackager = new Repackager(sourceJar);
// Configure launch script
File scriptTemplate = getClass().getResource("/launch-template.sh").getFile();
Map<String, String> scriptProperties = Map.of(
"initInfoProvides", "myapp",
"initInfoShortDescription", "My Application",
"runAsUser", "appuser",
"javaOpts", "-Xmx512m -Dspring.profiles.active=prod"
);
LaunchScript launchScript = new DefaultLaunchScript(scriptTemplate, scriptProperties);
// Repackage with launch script
repackager.repackage(destination, Libraries.NONE, launchScript);
// Set executable permissions
destination.setExecutable(true, false);
System.out.println("Created executable JAR: " + destination.getAbsolutePath());
System.out.println("Run with: " + destination.getAbsolutePath());import org.springframework.boot.loader.tools.*;
import java.io.File;
import java.util.Map;
// Only add launch script for Unix-like systems
public class ConditionalLaunchScript {
public static LaunchScript createLaunchScript() {
String osName = System.getProperty("os.name").toLowerCase();
if (osName.contains("windows")) {
// No launch script for Windows
return null;
}
// Create launch script for Unix-like systems
try {
File template = new File("unix-launch-template.sh");
Map<String, String> props = Map.of(
"initInfoProvides", "myapp",
"initInfoShortDescription", "My App"
);
return new DefaultLaunchScript(template, props);
} catch (Exception e) {
System.err.println("Failed to create launch script: " + e.getMessage());
return null;
}
}
}
// Usage
File sourceJar = new File("myapp.jar");
Repackager repackager = new Repackager(sourceJar);
LaunchScript launchScript = ConditionalLaunchScript.createLaunchScript();
if (launchScript != null) {
repackager.repackage(Libraries.NONE, launchScript);
System.out.println("Created executable JAR with launch script");
} else {
repackager.repackage(Libraries.NONE);
System.out.println("Created JAR without launch script");
}import org.springframework.boot.loader.tools.LaunchScript;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
// Custom launch script implementation
public class CustomLaunchScript implements LaunchScript {
private final String applicationName;
private final String javaOpts;
private final String workingDir;
public CustomLaunchScript(String applicationName, String javaOpts, String workingDir) {
this.applicationName = applicationName;
this.javaOpts = javaOpts;
this.workingDir = workingDir;
}
@Override
public byte[] toByteArray() {
String script = generateScript();
return script.getBytes(StandardCharsets.UTF_8);
}
private String generateScript() {
return String.format("""
#!/bin/bash
#
# %s startup script
#
JAVA_OPTS="%s"
WORKING_DIR="%s"
# Change to working directory
cd "$WORKING_DIR" || exit 1
# Find Java executable
if [ -n "$JAVA_HOME" ]; then
JAVA_EXE="$JAVA_HOME/bin/java"
else
JAVA_EXE="java"
fi
# Execute the JAR
exec "$JAVA_EXE" $JAVA_OPTS -jar "$0" "$@"
# JAR content follows
""", applicationName, javaOpts, workingDir);
}
}
// Usage
LaunchScript customScript = new CustomLaunchScript(
"MyApp",
"-Xmx1g -Dspring.profiles.active=prod",
"/opt/myapp"
);
try (JarWriter writer = new JarWriter(new File("myapp.jar"), customScript)) {
// Write JAR contents...
}import org.springframework.boot.loader.tools.*;
import java.io.File;
import java.util.Map;
// Launch script that detects and adapts to the runtime environment
public class EnvironmentAwareLaunchScript implements LaunchScript {
private final Map<String, String> properties;
public EnvironmentAwareLaunchScript(Map<String, String> properties) {
this.properties = properties;
}
@Override
public byte[] toByteArray() {
String script = """
#!/bin/bash
#
# Environment-aware launch script
#
# Detect environment
if [ -f "/etc/debian_version" ]; then
INIT_SYSTEM="systemd"
USER_HOME="/home"
elif [ -f "/etc/redhat-release" ]; then
INIT_SYSTEM="systemd"
USER_HOME="/home"
elif [ "$(uname)" = "Darwin" ]; then
INIT_SYSTEM="launchd"
USER_HOME="/Users"
else
INIT_SYSTEM="generic"
USER_HOME="/home"
fi
# Set environment-specific defaults
case "$INIT_SYSTEM" in
systemd)
LOG_DIR="/var/log/%s"
PID_DIR="/var/run"
CONFIG_DIR="/etc/%s"
;;
launchd)
LOG_DIR="$USER_HOME/Library/Logs/%s"
PID_DIR="/tmp"
CONFIG_DIR="$USER_HOME/Library/Application Support/%s"
;;
*)
LOG_DIR="/tmp/logs/%s"
PID_DIR="/tmp"
CONFIG_DIR="/tmp/config/%s"
;;
esac
# Create directories if they don't exist
mkdir -p "$LOG_DIR" "$PID_DIR" "$CONFIG_DIR"
# Set JVM options based on available memory
TOTAL_MEM=$(free -m 2>/dev/null | awk 'NR==2{printf "%%d", $2}' || echo "1024")
if [ "$TOTAL_MEM" -gt 4096 ]; then
JAVA_OPTS="-Xmx2g -Xms1g"
elif [ "$TOTAL_MEM" -gt 2048 ]; then
JAVA_OPTS="-Xmx1g -Xms512m"
else
JAVA_OPTS="-Xmx512m -Xms256m"
fi
# Add application-specific properties
JAVA_OPTS="$JAVA_OPTS -Dlogging.file.path=$LOG_DIR"
JAVA_OPTS="$JAVA_OPTS -Dspring.config.additional-location=$CONFIG_DIR/"
# Execute the application
exec java $JAVA_OPTS -jar "$0" "$@"
""".formatted(
properties.getOrDefault("appName", "app"),
properties.getOrDefault("appName", "app"),
properties.getOrDefault("appName", "app"),
properties.getOrDefault("appName", "app"),
properties.getOrDefault("appName", "app")
);
return script.getBytes(StandardCharsets.UTF_8);
}
}Common template variables supported by DefaultLaunchScript:
| Property | Description | Default |
|---|---|---|
initInfoProvides | Service name | Application name |
initInfoShortDescription | Brief description | Application description |
initInfoDescription | Full description | Application description |
confFolder | Configuration directory | Same as JAR location |
logFolder | Log file directory | /var/log |
pidFolder | PID file directory | /var/run |
logFilename | Log file name | Application name + .log |
javaOpts | JVM options | Empty |
runAsUser | User to run as | Current user |
mode | Execution mode | auto |
useStartStopDaemon | Use system daemon | true |
stopWaitTime | Stop timeout (seconds) | 60 |
Launch scripts enable Spring Boot JARs to integrate seamlessly with system service managers:
The generated executable JARs can be installed as system services, providing process management, automatic restart, and system integration capabilities.
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-loader-tools