Skip to content

Commit

Permalink
Support embedded jar initialization scripts
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Apr 9, 2015
1 parent ffc5d56 commit 7934818
Show file tree
Hide file tree
Showing 19 changed files with 796 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
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;

Expand Down Expand Up @@ -80,6 +82,10 @@ public void setClassifier(String classifier) {
this.classifier = classifier;
}

void setOutputFile(File file) {
this.outputFile = file;
}

@TaskAction
public void repackage() {
Project project = getProject();
Expand Down Expand Up @@ -170,7 +176,8 @@ private void repackage(File file) {
}
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);
Expand Down Expand Up @@ -201,6 +208,15 @@ else if (getProject().getTasks().getByName("run").hasProperty("main")) {
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;
}

}

/**
Expand Down Expand Up @@ -228,10 +244,7 @@ protected String findMainMethod(java.util.jar.JarFile source) throws IOException
}
}
}
}

void setOutputFile(File file) {
this.outputFile = file;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
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;
Expand Down Expand Up @@ -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
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,29 @@ public void repackage(Libraries libraries) throws IOException {
* @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)) {
Expand All @@ -127,7 +139,7 @@ public void repackage(File destination, Libraries libraries) throws IOException
try {
JarFile jarFileSource = new JarFile(workingSource);
try {
repackage(jarFileSource, destination, libraries);
repackage(jarFileSource, destination, libraries, launchScript);
}
finally {
jarFileSource.close();
Expand All @@ -152,9 +164,9 @@ private boolean alreadyRepackaged() throws IOException {
}
}

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));
Expand Down
Loading

0 comments on commit 7934818

Please sign in to comment.