Support embedded jar initialization scripts

Update the Maven and Gradle plugin to generate fully executable jar
files on Unix like machines. A launcher bash script is added to the
front of the jar file which handles execution.

The default execution script will either launch the application or
handle init.d service operations (start/stop/restart) depending on if
the application is executed directly, or via a symlink to init.d.

See gh-1117
pull/2238/merge
Phillip Webb 10 years ago
parent ffc5d565c8
commit 793481843c

@ -16,6 +16,9 @@
package org.springframework.boot.gradle
import java.io.File;
import java.util.Map;
import org.springframework.boot.loader.tools.Layout
import org.springframework.boot.loader.tools.Layouts
@ -130,4 +133,21 @@ public class SpringBootPluginExtension {
*/
boolean applyExcludeRules = true;
/**
* If a fully executable jar (for *nix machines) should be generated by prepending a
* launch script to the jar.
*/
boolean executable = true;
/**
* The embedded launch script to prepend to the front of the jar if it is fully
* executable. If not specified the 'Spring Boot' default script will be used.
*/
File embeddedLaunchScript;
/**
* Properties that should be expanded in the embedded launch script.
*/
Map<String,String> embeddedLaunchScriptProperties;
}

@ -28,6 +28,8 @@ import org.gradle.api.Project;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.bundling.Jar;
import org.springframework.boot.gradle.SpringBootPluginExtension;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.boot.loader.tools.Repackager;
import org.springframework.util.FileCopyUtils;
@ -80,6 +82,10 @@ public class RepackageTask extends DefaultTask {
this.classifier = classifier;
}
void setOutputFile(File file) {
this.outputFile = file;
}
@TaskAction
public void repackage() {
Project project = getProject();
@ -170,7 +176,8 @@ public class RepackageTask extends DefaultTask {
}
repackager.setBackupSource(this.extension.isBackupSource());
try {
repackager.repackage(file, this.libraries);
LaunchScript launchScript = getLaunchScript();
repackager.repackage(file, this.libraries, launchScript);
}
catch (IOException ex) {
throw new IllegalStateException(ex.getMessage(), ex);
@ -201,6 +208,15 @@ public class RepackageTask extends DefaultTask {
getLogger().info("Setting mainClass: " + mainClass);
repackager.setMainClass(mainClass);
}
private LaunchScript getLaunchScript() throws IOException {
if (this.extension.isExecutable()) {
return new DefaultLaunchScript(this.extension.getEmbeddedLaunchScript(),
this.extension.getEmbeddedLaunchScriptProperties());
}
return null;
}
}
/**
@ -228,10 +244,7 @@ public class RepackageTask extends DefaultTask {
}
}
}
}
void setOutputFile(File file) {
this.outputFile = file;
}
}

@ -0,0 +1,111 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Default implementation of {@link LaunchScript}. Provides the default Spring Boot launch
* script or can load a specific script File. Also support mustache style template
* expansion of the form <code>{{name:default}}</code>.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class DefaultLaunchScript implements LaunchScript {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final int BUFFER_SIZE = 4096;
private static final Pattern PLACEHOLDER_PATTERN = Pattern
.compile("\\{\\{(\\w+)(:.*?)?\\}\\}");
private final String content;
/**
* Create a new {@link DefaultLaunchScript} instance.
* @param file the source script file or {@code null} to use the default
* @param properties an optional set of script properties used for variable expansion
* @throws IOException if the script cannot be loaded
*/
public DefaultLaunchScript(File file, Map<?, ?> properties) throws IOException {
String content = loadContent(file);
this.content = expandPlaceholders(content, properties);
}
private String loadContent(File file) throws IOException {
if (file == null) {
return loadContent(getClass().getResourceAsStream("launch.script"));
}
return loadContent(new FileInputStream(file));
}
private String loadContent(InputStream inputStream) throws IOException {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
copy(inputStream, outputStream);
return new String(outputStream.toByteArray(), UTF_8);
}
finally {
inputStream.close();
}
}
private void copy(InputStream inputStream, OutputStream outputStream)
throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
private String expandPlaceholders(String content, Map<?, ?> properties) {
StringBuffer expanded = new StringBuffer();
Matcher matcher = PLACEHOLDER_PATTERN.matcher(content);
while (matcher.find()) {
String name = matcher.group(1);
String value = matcher.group(2);
if (properties != null && properties.containsKey(name)) {
value = (String) properties.get(name);
}
else {
value = (value == null ? matcher.group(0) : value.substring(1));
}
matcher.appendReplacement(expanded, value);
}
matcher.appendTail(expanded);
return expanded.toString();
}
@Override
public byte[] toByteArray() {
return this.content.getBytes(UTF_8);
}
}

@ -27,6 +27,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
@ -63,7 +66,37 @@ public class JarWriter {
* @throws FileNotFoundException
*/
public JarWriter(File file) throws FileNotFoundException, IOException {
this.jarOutput = new JarOutputStream(new FileOutputStream(file));
this(file, null);
}
/**
* Create a new {@link JarWriter} instance.
* @param file the file to write
* @param launchScript an optional launch script to prepend to the front of the jar
* @throws IOException
* @throws FileNotFoundException
*/
public JarWriter(File file, LaunchScript launchScript) throws FileNotFoundException,
IOException {
FileOutputStream fileOutputStream = new FileOutputStream(file);
if (launchScript != null) {
fileOutputStream.write(launchScript.toByteArray());
setExecutableFilePermission(file);
}
this.jarOutput = new JarOutputStream(fileOutputStream);
}
private void setExecutableFilePermission(File file) {
try {
Path path = file.toPath();
Set<PosixFilePermission> permissions = new HashSet<PosixFilePermission>(
Files.getPosixFilePermissions(path));
permissions.add(PosixFilePermission.OWNER_EXECUTE);
Files.setPosixFilePermissions(path, permissions);
}
catch (Throwable ex) {
// Ignore and continue creating the jar
}
}
/**

@ -0,0 +1,33 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools;
/**
* A script that can be prepended to the front of a JAR file to make it executable.
*
* @author Phillip Webb
* @since 1.3.0
*/
public interface LaunchScript {
/**
* The the content of the launch script as a byte array.
* @return the script bytes
*/
byte[] toByteArray();
}

@ -104,17 +104,29 @@ public class Repackager {
* @throws IOException
*/
public void repackage(File destination, Libraries libraries) throws IOException {
repackage(destination, libraries, null);
}
/**
* Repackage to the given destination so that it can be launched using '
* {@literal java -jar}'
* @param destination the destination file (may be the same as the source)
* @param libraries the libraries required to run the archive
* @param launchScript an optional launch script prepended to the front of the jar
* @throws IOException
* @since 1.3.0
*/
public void repackage(File destination, Libraries libraries, LaunchScript launchScript)
throws IOException {
if (destination == null || destination.isDirectory()) {
throw new IllegalArgumentException("Invalid destination");
}
if (libraries == null) {
throw new IllegalArgumentException("Libraries must not be null");
}
if (alreadyRepackaged()) {
return;
}
destination = destination.getAbsoluteFile();
File workingSource = this.source;
if (this.source.equals(destination)) {
@ -127,7 +139,7 @@ public class Repackager {
try {
JarFile jarFileSource = new JarFile(workingSource);
try {
repackage(jarFileSource, destination, libraries);
repackage(jarFileSource, destination, libraries, launchScript);
}
finally {
jarFileSource.close();
@ -152,9 +164,9 @@ public class Repackager {
}
}
private void repackage(JarFile sourceJar, File destination, Libraries libraries)
throws IOException {
final JarWriter writer = new JarWriter(destination);
private void repackage(JarFile sourceJar, File destination, Libraries libraries,
LaunchScript launchScript) throws IOException {
final JarWriter writer = new JarWriter(destination, launchScript);
try {
final Set<String> seen = new HashSet<String>();
writer.writeManifest(buildManifest(sourceJar));

@ -0,0 +1,164 @@
#!/bin/bash
#
# . ____ _ __ _ _
# /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
# ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
# \\/ ___)| |_)| | | | | || (_| | ) ) ) )
# ' |____| .__|_| |_|_| |_\__, | / / / /
# =========|_|==============|___/=/_/_/_/
# :: Spring Boot Startup Script ::
#
WORKING_DIR="$(pwd)"
PID_FOLDER="/var/run"
USER_PID_FOLDER="/tmp"
LOG_FOLDER="/var/log"
USER_LOG_FOLDER="/tmp"
# Setup defaults
[[ -z "$mode" ]] && mode="{{mode:auto}}" # modes are "auto", "service" or "run"
# ANSI Colors
echoRed() { echo $'\e[0;31m'$1$'\e[0m'; }
echoGreen() { echo $'\e[0;32m'$1$'\e[0m'; }
echoYellow() { echo $'\e[0;33m'$1$'\e[0m'; }
# Follow symlinks to find the real jar and detect init.d script
cd $(dirname "$0")
[[ -z "$jarfile" ]] && jarfile=$(pwd)/$(basename "$0")
while [[ -L "$jarfile" ]]; do
[[ "$jarfile" =~ "init.d" ]] && init_script=$(basename "$jarfile")
jarfile=$(readlink "$jarfile")
cd $(dirname "$jarfile")
jarfile=$(pwd)/$(basename "$jarfile")
done
cd "$WORKING_DIR"
# Determine the script mode
action="run"
if [[ "$mode" == "auto" && -n "$init_script" ]] || [[ "$mode" == "service" ]]; then
action="$1"
shift
fi
# Create an identity for log/pid files
if [[ -n "$init_script" ]]; then
identity="${init_script}"
else
jar_folder=$(dirname "$jarfile")
identity=$(basename "${jarfile%.*}")_${jar_folder//\//}
fi
# Build the pid and log filenames
if [[ -n "$init_script" ]]; then
pid_file="$PID_FOLDER/${identity}/${identity}.pid"
log_file="$LOG_FOLDER/${identity}.log"
else
pid_file="$USER_PID_FOLDER/${identity}.pid"
log_file="$USER_LOG_FOLDER/${identity}.log"
fi
# Determine the user to run as
[[ $(id -u) == "0" ]] && run_user=$(ls -ld "$jarfile" | awk '{print $3}')
# Find Java
if type -p java 2>&1> /dev/null; then
javaexe=java
elif [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
javaexe="$JAVA_HOME/bin/java"
elif [[ -x "/usr/bin/java" ]]; then
javaexe="/usr/bin/java"
else
echo "Unable to find Java"
exit 1
fi
# Build actual command to execute
command="$javaexe -jar -Dsun.misc.URLClassPath.disableJarChecking=true $jarfile $@"
# Utility functions
checkPermissions() {
touch "$pid_file" &> /dev/null || { echoRed "Operation not permitted (cannot access pid file)"; exit 1; }
touch "$log_file" &> /dev/null || { echoRed "Operation not permitted (cannot access log file)"; exit 1; }
}
isRunning() {
ps -p $1 &> /dev/null
}
# Action functions
start() {
if [[ -f "$pid_file" ]]; then
pid=$(cat "$pid_file")
isRunning $pid && { echoYellow "Already running [$pid]"; exit 0; }
fi
pushd $(dirname "$jarfile") > /dev/null
if [[ -n "$run_user" ]]; then
mkdir "$PID_FOLDER/${identity}" &> /dev/null
checkPermissions
chown "$run_user" "$PID_FOLDER/${identity}"
chown "$run_user" "$pid_file"
chown "$run_user" "$log_file"
su -c "$command &> \"$log_file\" & echo \$!" $run_user > "$pid_file"
pid=$(cat "$pid_file")
else
checkPermissions
$command &> "$log_file" &
pid=$!
disown $pid
echo "$pid" > "$pid_file"
fi
[[ -z $pid ]] && { echoRed "Failed to start"; exit 1; }
echoGreen "Started [$pid]"
}
stop() {
[[ -f $pid_file ]] || { echoRed "Not running (pidfile not found)"; exit 1; }
pid=$(cat "$pid_file")
isRunning $pid || { echoRed "Not running (process ${pid} not found)"; exit 1; }
kill -HUP $pid &> /dev/null || { echoRed "Unable to kill process ${pid}"; exit 1; }
for i in $(seq 1 20); do
isRunning ${pid} || { echoGreen "Stopped [$pid]"; rm -f $pid_file; exit 0; }
sleep 1
done
echoRed "Unable to kill process ${pid}";
exit 3;
}
restart() {
stop
start
}
status() {
[[ -f $pid_file ]] || { echoRed "Not running"; exit 1; }
pid=$(cat "$pid_file")
isRunning $pid || { echoRed "Not running (process ${pid} not found)"; exit 1; }
echoGreen "Running [$pid]"
exit 0
}
run() {
pushd $(dirname "$jarfile") > /dev/null
exec $command
popd
}
# Call the appropriate action function
case "$action" in
start)
start "$@";;
stop)
stop "$@";;
restart)
restart "$@";;
status)
status "$@";;
run)
run "$@";;
*)
echo "Usage: $0 {start|stop|restart|status|run}"; exit 1;
esac
exit 0

@ -0,0 +1,111 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools;
import java.io.File;
import java.util.Properties;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DefaultLaunchScript}.
*
* @author Phillip Webb
*/
public class DefaultLaunchScriptTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void loadsDefaultScript() throws Exception {
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
String content = new String(script.toByteArray());
assertThat(content, containsString("Spring Boot Startup Script"));
assertThat(content, containsString("mode=\"auto\""));
}
@Test
public void loadFromFile() throws Exception {
File file = this.temporaryFolder.newFile();
FileCopyUtils.copy("ABC".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
String content = new String(script.toByteArray());
assertThat(content, equalTo("ABC"));
}
@Test
public void expandVariables() throws Exception {
File file = this.temporaryFolder.newFile();
FileCopyUtils.copy("h{{a}}ll{{b}}".getBytes(), file);
Properties properties = new Properties();
properties.put("a", "e");
properties.put("b", "o");
DefaultLaunchScript script = new DefaultLaunchScript(file, properties);
String content = new String(script.toByteArray());
assertThat(content, equalTo("hello"));
}
@Test
public void expandVariablesMultiLine() throws Exception {
File file = this.temporaryFolder.newFile();
FileCopyUtils.copy("h{{a}}l\nl{{b}}".getBytes(), file);
Properties properties = new Properties();
properties.put("a", "e");
properties.put("b", "o");
DefaultLaunchScript script = new DefaultLaunchScript(file, properties);
String content = new String(script.toByteArray());
assertThat(content, equalTo("hel\nlo"));
}
@Test
public void expandVariablesWithDefaults() throws Exception {
File file = this.temporaryFolder.newFile();
FileCopyUtils.copy("h{{a:e}}ll{{b:o}}".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
String content = new String(script.toByteArray());
assertThat(content, equalTo("hello"));
}
@Test
public void expandVariablesWithDefaultsOverride() throws Exception {
File file = this.temporaryFolder.newFile();
FileCopyUtils.copy("h{{a:e}}ll{{b:o}}".getBytes(), file);
Properties properties = new Properties();
properties.put("a", "a");
DefaultLaunchScript script = new DefaultLaunchScript(file, properties);
String content = new String(script.toByteArray());
assertThat(content, equalTo("hallo"));
}
@Test
public void expandVariablesMissingAreUnchanged() throws Exception {
File file = this.temporaryFolder.newFile();
FileCopyUtils.copy("h{{a}}ll{{b}}".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
String content = new String(script.toByteArray());
assertThat(content, equalTo("h{{a}}ll{{b}}"));
}
}

@ -18,6 +18,8 @@ package org.springframework.boot.loader.tools;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@ -34,6 +36,7 @@ import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
@ -141,7 +144,6 @@ public class RepackagerTests {
Repackager repackager = new Repackager(file);
repackager.repackage(NO_LIBRARIES);
repackager.repackage(NO_LIBRARIES);
Manifest actualManifest = getManifest(file);
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"),
equalTo("org.springframework.boot.loader.JarLauncher"));
@ -230,7 +232,6 @@ public class RepackagerTests {
equalTo(false));
assertThat(hasLauncherClasses(source), equalTo(false));
assertThat(hasLauncherClasses(dest), equalTo(true));
}
@Test
@ -380,7 +381,6 @@ public class RepackagerTests {
callback.library(new Library(nestedFile, LibraryScope.COMPILE));
}
});
JarFile jarFile = new JarFile(file);
try {
assertThat(jarFile.getEntry("lib/" + nestedFile.getName()).getMethod(),
@ -393,6 +393,22 @@ public class RepackagerTests {
}
}
@Test
public void addLauncherScript() throws Exception {
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
File source = this.testJarFile.getFile();
File dest = this.temporaryFolder.newFile("dest.jar");
Repackager repackager = new Repackager(source);
LaunchScript script = new MockLauncherScript("ABC");
repackager.repackage(dest, NO_LIBRARIES, script);
byte[] bytes = FileCopyUtils.copyToByteArray(dest);
assertThat(Files.getPosixFilePermissions(dest.toPath()),
hasItem(PosixFilePermission.OWNER_EXECUTE));
assertThat(new String(bytes), startsWith("ABC"));
assertThat(hasLauncherClasses(source), equalTo(false));
assertThat(hasLauncherClasses(dest), equalTo(true));
}
private boolean hasLauncherClasses(File file) throws IOException {
return hasEntry(file, "org/springframework/boot/")
&& hasEntry(file, "org/springframework/boot/loader/JarLauncher.class");
@ -422,4 +438,19 @@ public class RepackagerTests {
}
}
private static class MockLauncherScript implements LaunchScript {
private final byte[] bytes;
public MockLauncherScript(String script) {
this.bytes = script.getBytes();
}
@Override
public byte[] toByteArray() {
return this.bytes;
}
}
}

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<embeddedLaunchScript>${basedir}/src/launcher/custom.script</embeddedLaunchScript>
<embeddedLaunchScriptProperties>
<name>world</name>
</embeddedLaunchScriptProperties>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<mainClass>some.random.Main</mainClass>
</manifest>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring.version@</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>@servlet-api.version@</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

@ -0,0 +1,24 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

@ -0,0 +1,7 @@
import java.io.*;
import org.springframework.boot.maven.*;
Verify.verifyJar(
new File( basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar" ), "some.random.Main", "Hello world"
);

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<executable>false</executable>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<mainClass>some.random.Main</mainClass>
</manifest>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring.version@</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>@servlet-api.version@</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

@ -0,0 +1,24 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

@ -0,0 +1,7 @@
import java.io.*;
import org.springframework.boot.maven.*;
Verify.verifyJar(
new File( basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar" ), "some.random.Main", false
);

@ -2,6 +2,6 @@ import java.io.*;
import org.springframework.boot.maven.*;
Verify.verifyJar(
new File( basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar" ), "some.random.Main"
new File( basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar" ), "some.random.Main", "Spring Boot Startup Script"
);

@ -19,6 +19,7 @@ package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarFile;
@ -34,6 +35,8 @@ import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.Layouts;
import org.springframework.boot.loader.tools.Libraries;
@ -124,6 +127,29 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
@Parameter
private List<Dependency> requiresUnpack;
/**
* Make a fully executable jar for *nix machines by prepending a launch script to the
* jar.
* @since 1.3
*/
@Parameter(defaultValue = "true")
private boolean executable;
/**
* The embedded launch script to prepend to the front of the jar if it is fully
* executable. If not specified the 'Spring Boot' default script will be used.
* @since 1.3
*/
@Parameter
private File embeddedLaunchScript;
/**
* Properties that should be expanded in the embedded launch script.
* @since 1.3
*/
@Parameter
private Properties embeddedLaunchScriptProperties;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) {
@ -167,7 +193,8 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
getLog());
try {
repackager.repackage(target, libraries);
LaunchScript launchScript = getLaunchScript();
repackager.repackage(target, libraries, launchScript);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
@ -190,6 +217,14 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
+ this.project.getPackaging());
}
private LaunchScript getLaunchScript() throws IOException {
if (this.executable) {
return new DefaultLaunchScript(this.embeddedLaunchScript,
this.embeddedLaunchScriptProperties);
}
return null;
}
public static enum LayoutType {
/**

@ -26,8 +26,12 @@ import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
@ -44,8 +48,14 @@ public class Verify {
new JarArchiveVerification(file, SAMPLE_APP).verify();
}
public static void verifyJar(File file, String main) throws Exception {
new JarArchiveVerification(file, main).verify();
public static void verifyJar(File file, String main, String... scriptContents)
throws Exception {
verifyJar(file, main, true, scriptContents);
}
public static void verifyJar(File file, String main, boolean executable,
String... scriptContents) throws Exception {
new JarArchiveVerification(file, main).verify(executable, scriptContents);
}
public static void verifyWar(File file) throws Exception {
@ -149,9 +159,30 @@ public class Verify {
}
public void verify() throws Exception {
verify(true);
}
public void verify(boolean executable, String... scriptContents) throws Exception {
assertTrue("Archive missing", this.file.exists());
assertTrue("Archive not a file", this.file.isFile());
if (scriptContents.length > 0 && executable) {
String contents = new String(FileCopyUtils.copyToByteArray(this.file));
contents = contents.substring(0, contents.indexOf(new String(new byte[] {
0x50, 0x4b, 0x03, 0x04 })));
for (String content : scriptContents) {
assertThat(contents, containsString(content));
}
}
if (!executable) {
String contents = new String(FileCopyUtils.copyToByteArray(this.file));
assertTrue(
"Is executable",
contents.startsWith(new String(new byte[] { 0x50, 0x4b, 0x03,
0x04 })));
}
ZipFile zipFile = new ZipFile(this.file);
try {
ArchiveVerifier verifier = new ArchiveVerifier(zipFile);

Loading…
Cancel
Save