From 793481843cd93368359df2d3568bfed0410d068e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 9 Apr 2015 10:51:21 -0700 Subject: [PATCH 1/2] 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 --- .../gradle/SpringBootPluginExtension.groovy | 20 +++ .../boot/gradle/repackage/RepackageTask.java | 21 ++- .../loader/tools/DefaultLaunchScript.java | 111 ++++++++++++ .../boot/loader/tools/JarWriter.java | 35 +++- .../boot/loader/tools/LaunchScript.java | 33 ++++ .../boot/loader/tools/Repackager.java | 24 ++- .../boot/loader/tools/launch.script | 164 ++++++++++++++++++ .../tools/DefaultLaunchScriptTests.java | 111 ++++++++++++ .../boot/loader/tools/RepackagerTests.java | 37 +++- .../src/it/jar-custom-launcher/pom.xml | 61 +++++++ .../src/launcher/custom.script | 3 + .../main/java/org/test/SampleApplication.java | 24 +++ .../src/it/jar-custom-launcher/verify.groovy | 7 + .../src/it/jar-non-executable/pom.xml | 58 +++++++ .../main/java/org/test/SampleApplication.java | 24 +++ .../src/it/jar-non-executable/verify.groovy | 7 + .../src/it/jar/verify.groovy | 2 +- .../boot/maven/RepackageMojo.java | 37 +++- .../springframework/boot/maven/Verify.java | 35 +++- 19 files changed, 796 insertions(+), 18 deletions(-) create mode 100644 spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLaunchScript.java create mode 100644 spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LaunchScript.java create mode 100644 spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script create mode 100644 spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/DefaultLaunchScriptTests.java create mode 100644 spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/pom.xml create mode 100644 spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/launcher/custom.script create mode 100644 spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/main/java/org/test/SampleApplication.java create mode 100644 spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/verify.groovy create mode 100644 spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/pom.xml create mode 100644 spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/src/main/java/org/test/SampleApplication.java create mode 100644 spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/verify.groovy diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy index 86abbc3a745e..a81a27bfdc9e 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy @@ -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 embeddedLaunchScriptProperties; + } diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java index 932017ba0d98..97d7fdea71be 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java @@ -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; @@ -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(); @@ -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); @@ -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; + } + } /** @@ -228,10 +244,7 @@ protected String findMainMethod(java.util.jar.JarFile source) throws IOException } } } - } - void setOutputFile(File file) { - this.outputFile = file; } } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLaunchScript.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLaunchScript.java new file mode 100644 index 000000000000..0ba4f7a0aa89 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLaunchScript.java @@ -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 {{name:default}}. + * + * @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); + } + +} diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java index ef35fc473763..f2c365ac6e1d 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java @@ -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; @@ -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 permissions = new HashSet( + Files.getPosixFilePermissions(path)); + permissions.add(PosixFilePermission.OWNER_EXECUTE); + Files.setPosixFilePermissions(path, permissions); + } + catch (Throwable ex) { + // Ignore and continue creating the jar + } } /** diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LaunchScript.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LaunchScript.java new file mode 100644 index 000000000000..f9b3bfada4c9 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LaunchScript.java @@ -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(); + +} diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index 97da6ffc71b8..b79c9b84b374 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -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)) { @@ -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(); @@ -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 seen = new HashSet(); writer.writeManifest(buildManifest(sourceJar)); diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script b/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script new file mode 100644 index 000000000000..6da00583dda7 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script @@ -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 + diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/DefaultLaunchScriptTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/DefaultLaunchScriptTests.java new file mode 100644 index 000000000000..6c190b1feb07 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/DefaultLaunchScriptTests.java @@ -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}}")); + } + +} diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index a8a92de05ea0..0afdac652f2d 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -18,6 +18,8 @@ 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.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 void jarIsOnlyRepackagedOnce() throws Exception { 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 void differentDestination() throws Exception { equalTo(false)); assertThat(hasLauncherClasses(source), equalTo(false)); assertThat(hasLauncherClasses(dest), equalTo(true)); - } @Test @@ -380,7 +381,6 @@ public void doWithLibraries(LibraryCallback callback) throws IOException { 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 void doWithLibraries(LibraryCallback callback) throws IOException { } } + @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 @@ private Manifest getManifest(File file) throws IOException { } } + 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; + } + + } + } diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/pom.xml b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/pom.xml new file mode 100644 index 000000000000..1facd708d44b --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + ${basedir}/src/launcher/custom.script + + world + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + some.random.Main + + + Foo + + + + + + + + + org.springframework + spring-context + @spring.version@ + + + javax.servlet + javax.servlet-api + @servlet-api.version@ + provided + + + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/launcher/custom.script b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/launcher/custom.script new file mode 100644 index 000000000000..f8663275c8a1 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/launcher/custom.script @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Hello {{name}}" diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/main/java/org/test/SampleApplication.java b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..403cd968451b --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/main/java/org/test/SampleApplication.java @@ -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) { + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/verify.groovy new file mode 100644 index 000000000000..8c4a68e53f9b --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/verify.groovy @@ -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" +); + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/pom.xml b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/pom.xml new file mode 100644 index 000000000000..a65dd9ae0d94 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + false + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + some.random.Main + + + Foo + + + + + + + + + org.springframework + spring-context + @spring.version@ + + + javax.servlet + javax.servlet-api + @servlet-api.version@ + provided + + + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/src/main/java/org/test/SampleApplication.java b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..403cd968451b --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/src/main/java/org/test/SampleApplication.java @@ -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) { + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/verify.groovy new file mode 100644 index 000000000000..6ce822cd5670 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/verify.groovy @@ -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 +); + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/verify.groovy index 07b375b51fca..8b5af07aa876 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/verify.groovy +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/verify.groovy @@ -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" ); diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index fee5481c326a..49e83edc2fc9 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -19,6 +19,7 @@ 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.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 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 @@ protected String findMainMethod(JarFile source) throws IOException { 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 @@ private File getTargetFile() { + this.project.getPackaging()); } + private LaunchScript getLaunchScript() throws IOException { + if (this.executable) { + return new DefaultLaunchScript(this.embeddedLaunchScript, + this.embeddedLaunchScriptProperties); + } + return null; + } + public static enum LayoutType { /** diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java index f55377de2c31..2eeab97bfbaa 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java @@ -26,8 +26,12 @@ 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 static void verifyJar(File file) throws Exception { 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 AbstractArchiveVerification(File file) { } 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); From ed57adb5feaea01891439b580fa0ed62ba075140 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 9 Apr 2015 10:57:40 -0700 Subject: [PATCH 2/2] Update deployment documentation Change the "cloud deployment" section to cover general "deployment" and add documentation for init.d and systemd support. Closes gh-1117 --- .../src/main/asciidoc/build-tool-plugins.adoc | 12 ++ ...{cloud-deployment.adoc => deployment.adoc} | 103 ++++++++++++++++-- .../main/asciidoc/documentation-overview.adoc | 7 +- spring-boot-docs/src/main/asciidoc/index.adoc | 2 +- .../asciidoc/production-ready-features.adoc | 4 +- .../src/main/asciidoc/using-spring-boot.adoc | 4 +- 6 files changed, 111 insertions(+), 21 deletions(-) rename spring-boot-docs/src/main/asciidoc/{cloud-deployment.adoc => deployment.adoc} (79%) diff --git a/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc b/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc index 3fff6e10c4bb..536700a63c69 100644 --- a/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc +++ b/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc @@ -383,6 +383,18 @@ want the other Boot features but not this one) |`customConfiguration` |The name of the custom configuration which is used to populate the nested lib directory (without specifying this you get all compile and runtime dependencies). + +|`executable` +|Boolean flag to indicate if jar files are fully executable on Unix like operating + systems. Defaults to `true`. + +|`embeddedLaunchScript` +|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. + +|`embeddedLaunchScriptProperties` +|Additional properties that to be expanded in the launch script. The default script + supports a `mode` property which can contain the values `auto`, `service` or `run`. |=== diff --git a/spring-boot-docs/src/main/asciidoc/cloud-deployment.adoc b/spring-boot-docs/src/main/asciidoc/deployment.adoc similarity index 79% rename from spring-boot-docs/src/main/asciidoc/cloud-deployment.adoc rename to spring-boot-docs/src/main/asciidoc/deployment.adoc index 84ffecc6743b..c2f7c247703f 100644 --- a/spring-boot-docs/src/main/asciidoc/cloud-deployment.adoc +++ b/spring-boot-docs/src/main/asciidoc/deployment.adoc @@ -1,8 +1,20 @@ -[[cloud-deployment]] -= Deploying to the cloud +[[deployment]] +== Deploying Spring Boot applications [partintro] -- +Spring Boot's flexible packaging options provide a great deal of choice when it comes to +deploying your application. You can easily deploy Spring Boot applications to a variety +of cloud platforms, to a container images (such as Docker) or to virtual/real machines. + +This section covers some of the more common deployment scenarios. +-- + + + +[[cloud-deployment]] +== Deploying to the cloud + Spring Boot's executable jars are ready-made for most popular cloud PaaS (platform-as-a-service) providers. These providers tend to require that you "`bring your own container`"; they manage application processes (not Java applications @@ -23,12 +35,11 @@ to run packaged within it. In this section we'll look at what it takes to get the <> in the "`Getting Started`" section up and running in the Cloud. --- [[cloud-deployment-cloud-foundry]] -== Cloud Foundry +=== Cloud Foundry Cloud Foundry provides default buildpacks that come into play if no other buildpack is specified. The Cloud Foundry https://github.com/cloudfoundry/java-buildpack[Java buildpack] has excellent support for Spring applications, including Spring Boot. You can deploy @@ -102,7 +113,7 @@ able to hit the application at the URI given, in this case [[cloud-deployment-cloud-foundry-services]] -=== Binding to services +==== Binding to services By default, metadata about the running application as well as service connection information is exposed to the application as environment variables (for example: `$VCAP_SERVICES`). This architecture decision is due to Cloud Foundry's polyglot @@ -142,7 +153,7 @@ auto-configuration support and a `spring-boot-starter-cloud-connectors` starter [[cloud-deployment-heroku]] -== Heroku +=== Heroku Heroku is another popular PaaS platform. To customize Heroku builds, you provide a `Procfile`, which provides the incantation required to deploy an application. Heroku assigns a `port` for the Java application to use and then ensures that routing to the @@ -225,7 +236,7 @@ Your application should now be up and running on Heroku. [[cloud-deployment-openshift]] -== Openshift +=== Openshift https://www.openshift.com/[Openshift] is the RedHat public (and enterprise) PaaS solution. Like Heroku, it works by running scripts triggered by git commits, so you can script the launching of a Spring Boot application in pretty much any way you like as long as the @@ -288,14 +299,85 @@ run the app. [[cloud-deployment-gae]] -== Google App Engine +=== Google App Engine Google App Engine is tied to the Servlet 2.5 API, so you can't deploy a Spring Application there without some modifications. See the <> of this guide. +[[deployment-service]] +== Installing Spring Boot applications +In additional to running Spring Boot applications using `java -jar` it is also possible +to execute applications directly on Unix systems (Linux, OSX, FreeBSD etc). This makes it +very easy to install and manage Spring Boot applications in common production +environments. As long as you are generating '`fully executable`' jars from your build, and +you are not using a custom `embeddedLaunchScript`, the following techniques can be used. + + + +=== Unix/Linux services +Spring Boot application can be easily started as Unix/Linux services using either `init.d` +or `systemd`. + + + +==== Installation as a init.d (system v) service +The default executable script that is embedded into Spring Boot executable jars will act +as an `init.d` script when it is symlinked to `/etc/init.d`. The standard `start`, `stop`, +`restart` and `status` commands can be used. The script supports the following features: + +* Starts the services as the user that owns the jar file +* Tracks application PIDs using `/var/run/.pid` +* Writes console logs to `/var/log/.log` + +Assuming that you have a Spring Boot application installed in `/var/myapp`, to install a +Spring Boot application as an `init.d` service simply create a symlink: + +[indent=0,subs="verbatim,quotes,attributes"] +---- + $ sudo link -s /var/myapp/myapp.jar /etc/init.d/myapp +---- + +TIP: It is advisable to create a specific user account to run you application. Ensure +that you have set the owner of the jar file using `chown` before installing your service. -[[cloud-deployment-whats-next]] +Once installed, you can start and stop the service in the usual way. You can also flag the +application to start automatically using your standard operating system tools. For example, +if you use Debian: + +[indent=0,subs="verbatim,quotes,attributes"] +---- + $ update-rc.d myapp defaults +---- + + + +==== Installation as a systemd service +Systemd is the successor to `init.d` scripts, and now being used by many many modern Linux +distributions. Although you can continue to use `init.d` script with `systemd`, it is also +possible to launch Spring Boot applications using `systemd` '`service`' scripts. + +For example, to run a Spring Boot application installed in `var/myapp` you can add the +following script in `/etc/systemd/system/myapp.service`: + +[indent=0] +---- + [Unit] + Description=myapp + After=syslog.target + + [Service] + ExecStart=/var/myapp/myapp.jar + + [Install] + WantedBy=multi-user.target +---- + +TIP: Remember to change the `Description` and `ExecStart` fields for your application. + + + +[[deployment-whats-next]] == What to read next Check out the http://www.cloudfoundry.com/[Cloud Foundry], https://www.heroku.com/[Heroku] and https://www.openshift.com[Openshift] web sites for more information about the kinds of @@ -307,6 +389,3 @@ The next section goes on to cover the _<>_. - - - diff --git a/spring-boot-docs/src/main/asciidoc/documentation-overview.adoc b/spring-boot-docs/src/main/asciidoc/documentation-overview.adoc index 8b0990bd170f..53ebc8f876a7 100644 --- a/spring-boot-docs/src/main/asciidoc/documentation-overview.adoc +++ b/spring-boot-docs/src/main/asciidoc/documentation-overview.adoc @@ -136,10 +136,9 @@ When you're ready to push your Spring Boot application to production, we've got == Advanced topics Lastly, we have a few topics for the more advanced user. -* *Deploy to the cloud:* -<> | -<> | -<> +* *Deploy Spring Boot Applications:* +<> | +<> * *Build tool plugins:* <> | <> diff --git a/spring-boot-docs/src/main/asciidoc/index.adoc b/spring-boot-docs/src/main/asciidoc/index.adoc index f9a1cb452ec4..e9a2416bd449 100644 --- a/spring-boot-docs/src/main/asciidoc/index.adoc +++ b/spring-boot-docs/src/main/asciidoc/index.adoc @@ -45,7 +45,7 @@ include::getting-started.adoc[] include::using-spring-boot.adoc[] include::spring-boot-features.adoc[] include::production-ready-features.adoc[] -include::cloud-deployment.adoc[] +include::deployment.adoc[] include::spring-boot-cli.adoc[] include::build-tool-plugins.adoc[] include::howto.adoc[] diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index eca4e004c7be..16436c871fa7 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -1043,7 +1043,7 @@ If you want to explore some of the concepts discussed in this chapter, you can t look at the actuator {github-code}/spring-boot-samples[sample applications]. You also might want to read about graphing tools such as http://graphite.wikidot.com/[Graphite]. -Otherwise, you can continue on, to read about <> or jump ahead +Otherwise, you can continue on, to read about <> or jump ahead for some in-depth information about Spring Boot's _<>_. diff --git a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc index 4103189e94b8..b9cf076bdf49 100644 --- a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc +++ b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc @@ -5,8 +5,8 @@ [partintro] -- This section goes into more detail about how you should use Spring Boot. It covers topics -such as build systems, auto-configuration and run/deployment options. We also cover some -Spring Boot best practices. Although there is nothing particularly special about +such as build systems, auto-configuration and how to run your applications. We also cover +some Spring Boot best practices. Although there is nothing particularly special about Spring Boot (it is just another library that you can consume), there are a few recommendations that, when followed, will make your development process just a little easier.