Merge branch 'gh-1117'

This commit is contained in:
Phillip Webb 2015-04-09 11:00:27 -07:00
commit ba2ea301f1
25 changed files with 907 additions and 39 deletions

View File

@ -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`.
|===

View File

@ -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
<<getting-started.adoc#getting-started-first-application, simple application that we
developed>> 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 <<howto.adoc#howto-servlet-2-5, Servlet 2.5 section>>
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.
[[cloud-deployment-whats-next]]
=== 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/<appname>.pid`
* Writes console logs to `/var/log/<appname>.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.
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 <priority>
----
==== 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 _<<spring-boot-cli.adoc#cli, Spring Boot C
or you can jump ahead to read about
_<<build-tool-plugins.adoc#build-tool-plugins, build tool plugins>>_.

View File

@ -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:*
<<cloud-deployment.adoc#cloud-deployment-cloud-foundry, Cloud Foundry>> |
<<cloud-deployment.adoc#cloud-deployment-heroku, Heroku>> |
<<cloud-deployment.adoc#cloud-deployment-cloudbees, CloudBees>>
* *Deploy Spring Boot Applications:*
<<deployment.adoc#cloud-deployment, Cloud Deployment>> |
<<deployment.adoc#deployment-service, OS Service>>
* *Build tool plugins:*
<<build-tool-plugins.adoc#build-tool-plugins-maven-plugin, Maven>> |
<<build-tool-plugins.adoc#build-tool-plugins-gradle-plugin, Gradle>>

View File

@ -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[]

View File

@ -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 <<cloud-deployment.adoc#cloud-deployment,
'`cloud deployment options`'>> or jump ahead
Otherwise, you can continue on, to read about <<deployment.adoc#deployment,
'`deployment options`'>> or jump ahead
for some in-depth information about Spring Boot's
_<<build-tool-plugins.adoc#build-tool-plugins, build tool plugins>>_.

View File

@ -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.

View File

@ -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;
}

View File

@ -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;
}
}

View 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);
}
}

View File

@ -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
}
}
/**

View File

@ -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();
}

View File

@ -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));

View File

@ -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

View 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.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}}"));
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -0,0 +1,3 @@
#!/bin/sh
echo "Hello {{name}}"

View File

@ -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) {
}
}

View File

@ -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"
);

View File

@ -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>

View File

@ -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) {
}
}

View File

@ -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
);

View File

@ -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"
);

View File

@ -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 {
/**

View File

@ -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);