Merge branch '1.5.x'

pull/7193/merge
Phillip Webb 8 years ago
commit 450ab28163

@ -484,6 +484,11 @@ The following configuration options are available:
(defaults to a guess based on the archive type). See (defaults to a guess based on the archive type). See
<<build-tool-plugins-gradle-configuration-layouts,available layouts for more details>>. <<build-tool-plugins-gradle-configuration-layouts,available layouts for more details>>.
|'layoutFactory`
|A layout factory that can be used if a custom layout is required. Alternative layouts
can be provided by 3rd parties. Layout factories are only used when `layout` is not
specified.
|`requiresUnpack` |`requiresUnpack`
|A list of dependencies (in the form "`groupId:artifactId`" that must be unpacked from |A list of dependencies (in the form "`groupId:artifactId`" that must be unpacked from
fat jars in order to run. Items are still packaged into the fat jar, but they will be fat jars in order to run. Items are still packaged into the fat jar, but they will be
@ -530,6 +535,38 @@ loader should be included or not. The following layouts are available:
+[[build-tool-plugins-gradle-configuration-custom-repackager]]
+==== Using a custom layout
If you have custom requirements for how to arrange the dependencies and loader classes
inside the repackaged jar, you can use a custom layout. Any library which defines one
or more `LayoutFactory` implementations can be added to the build script dependencies
and then the layout factory becomes available in the `springBoot` configuration.
For example:
[source,groovy,indent=0,subs="verbatim,attributes"]
----
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:{spring-boot-version}")
classpath("com.example:custom-layout:1.0.0")
}
}
springBoot {
layoutFactory = new com.example.CustomLayoutFactory()
}
+----
NOTE: If there is only one custom `LayoutFactory` on the build classpath and it is
listed in `META-INF/spring.factories` then it is unnecessary to explicitly set it in the
`springBoot` configuration. Layout factories are only used when no explicit `layout` is
specified.
[[build-tool-plugins-understanding-the-gradle-plugin]] [[build-tool-plugins-understanding-the-gradle-plugin]]
=== Understanding how the Gradle plugin works === Understanding how the Gradle plugin works
When `spring-boot` is applied to your Gradle project a default task named `bootRepackage` When `spring-boot` is applied to your Gradle project a default task named `bootRepackage`

@ -32,6 +32,7 @@
<module>spring-boot-sample-atmosphere</module> <module>spring-boot-sample-atmosphere</module>
<module>spring-boot-sample-batch</module> <module>spring-boot-sample-batch</module>
<module>spring-boot-sample-cache</module> <module>spring-boot-sample-cache</module>
<module>spring-boot-sample-custom-layout</module>
<module>spring-boot-sample-data-cassandra</module> <module>spring-boot-sample-data-cassandra</module>
<module>spring-boot-sample-data-couchbase</module> <module>spring-boot-sample-data-couchbase</module>
<module>spring-boot-sample-data-elasticsearch</module> <module>spring-boot-sample-data-elasticsearch</module>

@ -0,0 +1,107 @@
<?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>
<parent>
<!-- Your own application should inherit from spring-boot-starter-parent -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-samples</artifactId>
<version>1.5.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-sample-custom-layout</artifactId>
<name>Spring Boot Custom Layout Sample</name>
<description>Spring Boot Custom Layout Sample</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader-tools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.gradle</groupId>
<artifactId>gradle-tooling-api</artifactId>
<version>${gradle.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-invoker-plugin</artifactId>
<configuration>
<cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
<settingsFile>src/it/settings.xml</settingsFile>
<localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
<postBuildHookScript>verify</postBuildHookScript>
<addTestClassPath>true</addTestClassPath>
<skipInvocation>${skipTests}</skipInvocation>
<streamLogs>true</streamLogs>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>install</goal>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-effective-pom</id>
<phase>generate-test-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${project.version}</version>
<type>effective-pom</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}</outputDirectory>
<destFileName>dependencies-pom.xml</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>gradle</id>
<url>http://repo.gradle.org/gradle/libs-releases-local</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

@ -0,0 +1,28 @@
buildscript {
repositories {
flatDir {
dirs '../..'
}
mavenLocal()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${project.bootVersion}"
classpath "org.springframework.boot:spring-boot-sample-custom-layout:${project.bootVersion}"
}
}
repositories {
mavenLocal()
mavenCentral()
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
springBoot {
layoutFactory = new sample.layout.SampleLayoutFactory('custom')
}
dependencies {
compile 'org.springframework.boot:spring-boot-starter'
}

@ -0,0 +1,41 @@
<?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>custom</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<layoutFactory implementation="sample.layout.SampleLayoutFactory">
<name>custom</name>
</layoutFactory>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
</dependencies>
</project>

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

@ -0,0 +1,7 @@
import java.io.*;
import sample.layout.*;
Verify.verify(
new File( basedir, "target/custom-0.0.1.BUILD-SNAPSHOT.jar" ), "custom"
);

@ -0,0 +1,24 @@
buildscript {
repositories {
flatDir {
dirs '../..'
}
mavenLocal()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${project.bootVersion}"
classpath "org.springframework.boot:spring-boot-sample-custom-layout:${project.bootVersion}"
}
}
repositories {
mavenLocal()
mavenCentral()
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
dependencies {
compile 'org.springframework.boot:spring-boot-starter'
}

@ -0,0 +1,36 @@
<?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>default</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
</dependencies>
</project>

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

@ -0,0 +1,7 @@
import java.io.*;
import sample.layout.*;
Verify.verify(
new File( basedir, "target/default-0.0.1.BUILD-SNAPSHOT.jar" ), "sample"
);

@ -0,0 +1,42 @@
/*
* Copyright 2012-2016 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 sample.layout;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.springframework.boot.loader.tools.CustomLoaderLayout;
import org.springframework.boot.loader.tools.Layouts;
import org.springframework.boot.loader.tools.LoaderClassesWriter;
/**
* @author pwebb
*/
public class SampleLayout extends Layouts.Jar implements CustomLoaderLayout {
private String name;
public SampleLayout(String name) {
this.name = name;
}
@Override
public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException {
writer.writeEntry(this.name, new ByteArrayInputStream("test".getBytes()));
}
}

@ -0,0 +1,48 @@
/*
* Copyright 2012-2016 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 sample.layout;
import java.io.File;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.LayoutFactory;
public class SampleLayoutFactory implements LayoutFactory {
private String name = "sample";
public SampleLayoutFactory() {
}
public SampleLayoutFactory(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public Layout getLayout(File source) {
return new SampleLayout(this.name);
}
}

@ -0,0 +1,2 @@
org.springframework.boot.loader.tools.LayoutFactory=\
sample.layout.SampleLayoutFactory

@ -0,0 +1,89 @@
/*
* Copyright 2012-2016 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 sample.layout;
import java.io.File;
import java.io.FileReader;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.gradle.tooling.GradleConnector;
import org.gradle.tooling.ProjectConnection;
import org.gradle.tooling.internal.consumer.DefaultGradleConnector;
import org.junit.Test;
import org.xml.sax.InputSource;
import org.springframework.util.FileCopyUtils;
public class GradeIT {
@Test
public void sampleDefault() throws Exception {
test("default", "sample");
}
@Test
public void sampleCustom() throws Exception {
test("custom", "custom");
}
private void test(String name, String expected) throws Exception {
File projectDirectory = new File("target/gradleit/" + name);
File javaDirectory = new File(
"target/gradleit/" + name + "/src/main/java/org/test/");
projectDirectory.mkdirs();
javaDirectory.mkdirs();
File script = new File(projectDirectory, "build.gradle");
FileCopyUtils.copy(new File("src/it/" + name + "/build.gradle"), script);
FileCopyUtils.copy(
new File("src/it/" + name
+ "/src/main/java/org/test/SampleApplication.java"),
new File(javaDirectory, "SampleApplication.java"));
GradleConnector gradleConnector = GradleConnector.newConnector();
gradleConnector.useGradleVersion("2.9");
((DefaultGradleConnector) gradleConnector).embedded(true);
ProjectConnection project = gradleConnector.forProjectDirectory(projectDirectory)
.connect();
project.newBuild().forTasks("clean", "build")
.withArguments("-PbootVersion=" + getBootVersion()).run();
Verify.verify(
new File("target/gradleit/" + name + "/build/libs/" + name + ".jar"),
expected);
}
public static String getBootVersion() {
return evaluateExpression(
"/*[local-name()='project']/*[local-name()='version']" + "/text()");
}
private static String evaluateExpression(String expression) {
try {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
XPathExpression expr = xpath.compile(expression);
String version = expr.evaluate(
new InputSource(new FileReader("target/dependencies-pom.xml")));
return version;
}
catch (Exception ex) {
throw new IllegalStateException("Failed to evaluate expression", ex);
}
}
}

@ -0,0 +1,45 @@
/*
* Copyright 2012-2016 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 sample.layout;
import java.io.File;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class Verify {
private Verify() {
}
public static void verify(File file, String entry) throws Exception {
ZipFile zipFile = new ZipFile(file);
try {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
if (entries.nextElement().getName().equals(entry)) {
return;
}
}
throw new AssertionError("No entry " + entry);
}
finally {
zipFile.close();
}
}
}

@ -26,6 +26,7 @@ import org.gradle.api.plugins.JavaPlugin;
import org.springframework.boot.gradle.buildinfo.BuildInfo; import org.springframework.boot.gradle.buildinfo.BuildInfo;
import org.springframework.boot.loader.tools.Layout; import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.LayoutFactory;
import org.springframework.boot.loader.tools.Layouts; import org.springframework.boot.loader.tools.Layouts;
/** /**
@ -90,6 +91,12 @@ public class SpringBootPluginExtension {
*/ */
LayoutType layout; LayoutType layout;
/**
* The layout factory that will be used when no explicit layout is specified.
* Alternative layouts can be provided by 3rd parties.
*/
LayoutFactory layoutFactory;
/** /**
* Libraries that must be unpacked from fat jars in order to run. Use Strings in the * Libraries that must be unpacked from fat jars in order to run. Use Strings in the
* form {@literal groupId:artifactId}. * form {@literal groupId:artifactId}.
@ -196,6 +203,14 @@ public class SpringBootPluginExtension {
this.layout = layout; this.layout = layout;
} }
public LayoutFactory getLayoutFactory() {
return this.layoutFactory;
}
public void setLayoutFactory(LayoutFactory layoutFactory) {
this.layoutFactory = layoutFactory;
}
public Set<String> getRequiresUnpack() { public Set<String> getRequiresUnpack() {
return this.requiresUnpack; return this.requiresUnpack;
} }

@ -21,7 +21,6 @@ import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.gradle.api.Action; import org.gradle.api.Action;
import org.gradle.api.DefaultTask; import org.gradle.api.DefaultTask;
@ -35,6 +34,7 @@ import org.springframework.boot.gradle.SpringBootPluginExtension;
import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.LaunchScript; import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.boot.loader.tools.Repackager; import org.springframework.boot.loader.tools.Repackager;
import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningListener;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
/** /**
@ -46,8 +46,6 @@ import org.springframework.util.FileCopyUtils;
*/ */
public class RepackageTask extends DefaultTask { public class RepackageTask extends DefaultTask {
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private String customConfiguration; private String customConfiguration;
private Object withJarTask; private Object withJarTask;
@ -215,7 +213,10 @@ public class RepackageTask extends DefaultTask {
copy(file, outputFile); copy(file, outputFile);
file = outputFile; file = outputFile;
} }
Repackager repackager = new LoggingRepackager(file); Repackager repackager = new Repackager(file,
this.extension.getLayoutFactory());
repackager.addMainClassTimeoutWarningListener(
new LoggingMainClassTimeoutWarningListener());
setMainClass(repackager); setMainClass(repackager);
if (this.extension.convertLayout() != null) { if (this.extension.convertLayout() != null) {
repackager.setLayout(this.extension.convertLayout()); repackager.setLayout(this.extension.convertLayout());
@ -305,26 +306,13 @@ public class RepackageTask extends DefaultTask {
/** /**
* {@link Repackager} that also logs when searching takes too long. * {@link Repackager} that also logs when searching takes too long.
*/ */
private class LoggingRepackager extends Repackager { private class LoggingMainClassTimeoutWarningListener
implements MainClassTimeoutWarningListener {
LoggingRepackager(File source) {
super(source);
}
@Override @Override
protected String findMainMethod(java.util.jar.JarFile source) throws IOException { public void handleTimeoutWarning(long duration, String mainMethod) {
long startTime = System.currentTimeMillis(); getLogger().warn("Searching for the main-class is taking "
try { + "some time, consider using setting " + "'springBoot.mainClass'");
return super.findMainMethod(source);
}
finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
getLogger().warn("Searching for the main-class is taking "
+ "some time, consider using setting "
+ "'springBoot.mainClass'");
}
}
} }
} }

@ -0,0 +1,37 @@
/*
* Copyright 2012-2016 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.IOException;
/**
* Additional interface that can be implemented by {@link Layout Layouts} that write their
* own loader classes.
*
* @author Phillip Webb
* @since 1.5.0
*/
public interface CustomLoaderLayout {
/**
* Write the required loader classes into the JAR.
* @param writer the writer used to write the classes
* @throws IOException if the classes cannot be written
*/
void writeLoadedClasses(LoaderClassesWriter writer) throws IOException;
}

@ -0,0 +1,34 @@
/*
* Copyright 2012-2016 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;
/**
* Default implementation of {@link LayoutFactory}.
*
* @author Phillip Webb
* @since 1.5.0
*/
public class DefaultLayoutFactory implements LayoutFactory {
@Override
public Layout getLayout(File source) {
return Layouts.forFile(source);
}
}

@ -49,7 +49,7 @@ import java.util.zip.ZipEntry;
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class JarWriter { public class JarWriter implements LoaderClassesWriter {
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar"; private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
@ -155,6 +155,7 @@ public class JarWriter {
* @param inputStream The stream from which the entry's data can be read * @param inputStream The stream from which the entry's data can be read
* @throws IOException if the write fails * @throws IOException if the write fails
*/ */
@Override
public void writeEntry(String entryName, InputStream inputStream) throws IOException { public void writeEntry(String entryName, InputStream inputStream) throws IOException {
JarEntry entry = new JarEntry(entryName); JarEntry entry = new JarEntry(entryName);
writeEntry(entry, new InputStreamEntryWriter(inputStream, true)); writeEntry(entry, new InputStreamEntryWriter(inputStream, true));
@ -204,8 +205,20 @@ public class JarWriter {
* Write the required spring-boot-loader classes to the JAR. * Write the required spring-boot-loader classes to the JAR.
* @throws IOException if the classes cannot be written * @throws IOException if the classes cannot be written
*/ */
@Override
public void writeLoaderClasses() throws IOException { public void writeLoaderClasses() throws IOException {
URL loaderJar = getClass().getClassLoader().getResource(NESTED_LOADER_JAR); writeLoaderClasses(NESTED_LOADER_JAR);
}
/**
* Write the required spring-boot-loader classes to the JAR.
* @param loaderJarResourceName the name of the resource containing the loader classes
* to be written
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses(String loaderJarResourceName) throws IOException {
URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);
JarInputStream inputStream = new JarInputStream( JarInputStream inputStream = new JarInputStream(
new BufferedInputStream(loaderJar.openStream())); new BufferedInputStream(loaderJar.openStream()));
JarEntry entry; JarEntry entry;

@ -18,9 +18,13 @@ package org.springframework.boot.loader.tools;
/** /**
* Strategy interface used to determine the layout for a particular type of archive. * Strategy interface used to determine the layout for a particular type of archive.
* Layouts may additionally implement {@link CustomLoaderLayout} if they wish to write
* custom loader classes.
* *
* @author Phillip Webb * @author Phillip Webb
* @see Layouts * @see Layouts
* @see RepackagingLayout
* @see CustomLoaderLayout
*/ */
public interface Layout { public interface Layout {

@ -0,0 +1,36 @@
/*
* Copyright 2012-2016 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;
/**
* Factory interface used to create a {@link Layout}.
*
* @author Dave Syer
* @author Phillip Webb
*/
public interface LayoutFactory {
/**
* Return a {@link Layout} for the specified source file.
* @param source the source file
* @return the layout to use for the file
*/
Layout getLayout(File source);
}

@ -0,0 +1,53 @@
/*
* Copyright 2012-2016 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.IOException;
import java.io.InputStream;
/**
* Writer used by {@link CustomLoaderLayout CustomLoaderLayouts} to write classes into a
* repackaged JAR.
*
* @author Phillip Webb
* @since 1.5.0
*/
public interface LoaderClassesWriter {
/**
* Write the default required spring-boot-loader classes to the JAR.
* @throws IOException if the classes cannot be written
*/
void writeLoaderClasses() throws IOException;
/**
* Write custom required spring-boot-loader classes to the JAR.
* @param loaderJarResourceName the name of the resource containing the loader classes
* to be written
* @throws IOException if the classes cannot be written
*/
void writeLoaderClasses(String loaderJarResourceName) throws IOException;
/**
* Write a single entry to the JAR.
* @param name the name of the entry
* @param inputStream the input stream content
* @throws IOException if the entry cannot be written
*/
void writeEntry(String name, InputStream inputStream) throws IOException;
}

@ -24,11 +24,15 @@ import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.tools.JarWriter.EntryTransformer; import org.springframework.boot.loader.tools.JarWriter.EntryTransformer;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/** /**
* Utility class that can be used to repackage an archive so that it can be executed using * Utility class that can be used to repackage an archive so that it can be executed using
@ -52,6 +56,10 @@ public class Repackager {
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private List<MainClassTimeoutWarningListener> mainClassTimeoutListeners = new ArrayList<MainClassTimeoutWarningListener>();
private String mainClass; private String mainClass;
private boolean backupSource = true; private boolean backupSource = true;
@ -60,12 +68,28 @@ public class Repackager {
private Layout layout; private Layout layout;
private LayoutFactory layoutFactory;
public Repackager(File source) { public Repackager(File source) {
this(source, null);
}
public Repackager(File source, LayoutFactory layoutFactory) {
if (source == null || !source.exists() || !source.isFile()) { if (source == null || !source.exists() || !source.isFile()) {
throw new IllegalArgumentException("Source must refer to an existing file"); throw new IllegalArgumentException("Source must refer to an existing file");
} }
this.source = source.getAbsoluteFile(); this.source = source.getAbsoluteFile();
this.layout = Layouts.forFile(source); this.layoutFactory = layoutFactory;
}
/**
* Add a listener that will be triggered to dispaly a warning if searching for the
* main class takes too long.
* @param listener the listener to add
*/
public void addMainClassTimeoutWarningListener(
MainClassTimeoutWarningListener listener) {
this.mainClassTimeoutListeners.add(listener);
} }
/** /**
@ -97,6 +121,15 @@ public class Repackager {
this.layout = layout; this.layout = layout;
} }
/**
* Sets the layout factory for the jar. The factory can be used when no specific
* layout is specific.
* @param layoutFactory the layoutFactory to set
*/
public void setLayoutFactory(LayoutFactory layoutFactory) {
this.layoutFactory = layoutFactory;
}
/** /**
* Repackage the source file so that it can be run using '{@literal java -jar}'. * Repackage the source file so that it can be run using '{@literal java -jar}'.
* @param libraries the libraries required to run the archive * @param libraries the libraries required to run the archive
@ -134,6 +167,9 @@ public class Repackager {
if (libraries == null) { if (libraries == null) {
throw new IllegalArgumentException("Libraries must not be null"); throw new IllegalArgumentException("Libraries must not be null");
} }
if (this.layout == null) {
this.layout = getLayoutFactory().getLayout(this.source);
}
if (alreadyRepackaged()) { if (alreadyRepackaged()) {
return; return;
} }
@ -161,6 +197,19 @@ public class Repackager {
} }
} }
private LayoutFactory getLayoutFactory() {
if (this.layoutFactory != null) {
return this.layoutFactory;
}
List<LayoutFactory> factories = SpringFactoriesLoader
.loadFactories(LayoutFactory.class, null);
if (factories.isEmpty()) {
return new DefaultLayoutFactory();
}
Assert.state(factories.size() == 1, "No unique LayoutFactory found");
return factories.get(0);
}
/** /**
* Return the {@link File} to use to backup the original source. * Return the {@link File} to use to backup the original source.
* @return the file to use to backup the original source * @return the file to use to backup the original source
@ -203,21 +252,7 @@ public class Repackager {
} }
}); });
writer.writeManifest(buildManifest(sourceJar)); repackage(sourceJar, writer, unpackLibraries, standardLibraries);
Set<String> seen = new HashSet<String>();
writeNestedLibraries(unpackLibraries, seen, writer);
if (this.layout instanceof RepackagingLayout) {
writer.writeEntries(sourceJar,
new RenamingEntryTransformer(((RepackagingLayout) this.layout)
.getRepackagedClassesLocation()));
}
else {
writer.writeEntries(sourceJar);
}
writeNestedLibraries(standardLibraries, seen, writer);
if (this.layout.isExecutable()) {
writer.writeLoaderClasses();
}
} }
finally { finally {
try { try {
@ -229,6 +264,23 @@ public class Repackager {
} }
} }
private void repackage(JarFile sourceJar, JarWriter writer,
final List<Library> unpackLibraries, final List<Library> standardLibraries)
throws IOException {
writer.writeManifest(buildManifest(sourceJar));
Set<String> seen = new HashSet<String>();
writeNestedLibraries(unpackLibraries, seen, writer);
if (this.layout instanceof RepackagingLayout) {
writer.writeEntries(sourceJar, new RenamingEntryTransformer(
((RepackagingLayout) this.layout).getRepackagedClassesLocation()));
}
else {
writer.writeEntries(sourceJar);
}
writeNestedLibraries(standardLibraries, seen, writer);
writeLoaderClasses(writer);
}
private void writeNestedLibraries(List<Library> libraries, Set<String> alreadySeen, private void writeNestedLibraries(List<Library> libraries, Set<String> alreadySeen,
JarWriter writer) throws IOException { JarWriter writer) throws IOException {
for (Library library : libraries) { for (Library library : libraries) {
@ -244,6 +296,15 @@ public class Repackager {
} }
} }
private void writeLoaderClasses(JarWriter writer) throws IOException {
if (this.layout instanceof CustomLoaderLayout) {
((CustomLoaderLayout) this.layout).writeLoadedClasses(writer);
}
else if (this.layout.isExecutable()) {
writer.writeLoaderClasses();
}
}
private boolean isZip(File file) { private boolean isZip(File file) {
try { try {
FileInputStream fileInputStream = new FileInputStream(file); FileInputStream fileInputStream = new FileInputStream(file);
@ -280,7 +341,7 @@ public class Repackager {
startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE); startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
} }
if (startClass == null) { if (startClass == null) {
startClass = findMainMethod(source); startClass = findMainMethodWithTimeoutWarning(source);
} }
String launcherClassName = this.layout.getLauncherClassName(); String launcherClassName = this.layout.getLauncherClassName();
if (launcherClassName != null) { if (launcherClassName != null) {
@ -300,11 +361,25 @@ public class Repackager {
(this.layout instanceof RepackagingLayout) (this.layout instanceof RepackagingLayout)
? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() ? ((RepackagingLayout) this.layout).getRepackagedClassesLocation()
: this.layout.getClassesLocation()); : this.layout.getClassesLocation());
manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE);
this.layout.getLibraryDestination("", LibraryScope.COMPILE)); if (StringUtils.hasLength(lib)) {
manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib);
}
return manifest; return manifest;
} }
private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException {
long startTime = System.currentTimeMillis();
String mainMethod = findMainMethod(source);
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) {
listener.handleTimeoutWarning(duration, mainMethod);
}
}
return mainMethod;
}
protected String findMainMethod(JarFile source) throws IOException { protected String findMainMethod(JarFile source) throws IOException {
return MainClassFinder.findSingleMainClass(source, return MainClassFinder.findSingleMainClass(source,
this.layout.getClassesLocation()); this.layout.getClassesLocation());
@ -323,6 +398,21 @@ public class Repackager {
} }
} }
/**
* Callback interface used to present a warning when finding the main class takes too
* long.
*/
public interface MainClassTimeoutWarningListener {
/**
* Handle a timeout warning.
* @param duration the amount of time it took to find the main method
* @param mainMethod the main method that was actually found
*/
void handleTimeoutWarning(long duration, String mainMethod);
}
/** /**
* An {@code EntryTransformer} that renames entries by applying a prefix. * An {@code EntryTransformer} that renames entries by applying a prefix.
*/ */

@ -16,6 +16,7 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -347,14 +348,46 @@ public class RepackagerTests {
final LibraryScope scope = mock(LibraryScope.class); final LibraryScope scope = mock(LibraryScope.class);
given(layout.getLauncherClassName()).willReturn("testLauncher"); given(layout.getLauncherClassName()).willReturn("testLauncher");
given(layout.getLibraryDestination(anyString(), eq(scope))).willReturn("test/"); given(layout.getLibraryDestination(anyString(), eq(scope))).willReturn("test/");
given(layout.getLibraryDestination(anyString(), eq(LibraryScope.COMPILE)))
.willReturn("test-lib/");
repackager.setLayout(layout); repackager.setLayout(layout);
repackager.repackage(new Libraries() { repackager.repackage(new Libraries() {
@Override @Override
public void doWithLibraries(LibraryCallback callback) throws IOException { public void doWithLibraries(LibraryCallback callback) throws IOException {
callback.library(new Library(libJarFile, scope)); callback.library(new Library(libJarFile, scope));
} }
}); });
assertThat(hasEntry(file, "test/" + libJarFile.getName())).isTrue(); assertThat(hasEntry(file, "test/" + libJarFile.getName())).isTrue();
assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib"))
.isEqualTo("test-lib/");
assertThat(getManifest(file).getMainAttributes().getValue("Main-Class"))
.isEqualTo("testLauncher");
}
@Test
public void customLayoutNoBootLib() throws Exception {
TestJarFile libJar = new TestJarFile(this.temporaryFolder);
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
final File libJarFile = libJar.getFile();
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
File file = this.testJarFile.getFile();
Repackager repackager = new Repackager(file);
Layout layout = mock(Layout.class);
final LibraryScope scope = mock(LibraryScope.class);
given(layout.getLauncherClassName()).willReturn("testLauncher");
repackager.setLayout(layout);
repackager.repackage(new Libraries() {
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
callback.library(new Library(libJarFile, scope));
}
});
assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib"))
.isNull();
assertThat(getManifest(file).getMainAttributes().getValue("Main-Class")) assertThat(getManifest(file).getMainAttributes().getValue("Main-Class"))
.isEqualTo("testLauncher"); .isEqualTo("testLauncher");
} }
@ -536,6 +569,29 @@ public class RepackagerTests {
} }
} }
@Test
public void customLayoutFactoryWithoutLayout() throws Exception {
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
File source = this.testJarFile.getFile();
Repackager repackager = new Repackager(source, new TestLayoutFactory());
repackager.repackage(NO_LIBRARIES);
JarFile jarFile = new JarFile(source);
assertThat(jarFile.getEntry("test")).isNotNull();
jarFile.close();
}
@Test
public void customLayoutFactoryWithLayout() throws Exception {
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
File source = this.testJarFile.getFile();
Repackager repackager = new Repackager(source, new TestLayoutFactory());
repackager.setLayout(new Layouts.Jar());
repackager.repackage(NO_LIBRARIES);
JarFile jarFile = new JarFile(source);
assertThat(jarFile.getEntry("test")).isNull();
jarFile.close();
}
private boolean hasLauncherClasses(File file) throws IOException { private boolean hasLauncherClasses(File file) throws IOException {
return hasEntry(file, "org/springframework/boot/") return hasEntry(file, "org/springframework/boot/")
&& hasEntry(file, "org/springframework/boot/loader/JarLauncher.class"); && hasEntry(file, "org/springframework/boot/loader/JarLauncher.class");
@ -580,4 +636,22 @@ public class RepackagerTests {
} }
public static class TestLayoutFactory implements LayoutFactory {
@Override
public Layout getLayout(File source) {
return new TestLayout();
}
}
private static class TestLayout extends Layouts.Jar implements CustomLoaderLayout {
@Override
public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException {
writer.writeEntry("test", new ByteArrayInputStream("test".getBytes()));
}
}
} }

@ -22,14 +22,11 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarFile;
import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency; import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Mojo;
@ -43,9 +40,11 @@ import org.apache.maven.shared.artifact.filter.collection.ScopeFilter;
import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.LaunchScript; import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.boot.loader.tools.Layout; import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.LayoutFactory;
import org.springframework.boot.loader.tools.Layouts; import org.springframework.boot.loader.tools.Layouts;
import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.Repackager; import org.springframework.boot.loader.tools.Repackager;
import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningListener;
/** /**
* Repackages existing JAR and WAR archives so that they can be executed from the command * Repackages existing JAR and WAR archives so that they can be executed from the command
@ -59,8 +58,6 @@ import org.springframework.boot.loader.tools.Repackager;
@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) @Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class RepackageMojo extends AbstractDependencyFilterMojo { public class RepackageMojo extends AbstractDependencyFilterMojo {
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
/** /**
* The Maven project. * The Maven project.
* @since 1.0 * @since 1.0
@ -133,6 +130,15 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
@Parameter @Parameter
private LayoutType layout; private LayoutType layout;
/**
* The layout factory that will be used to create the executable archive if no
* explicit layout is set. Alternative layouts implementations can be provided by 3rd
* parties.
* @since 1.5
*/
@Parameter
private LayoutFactory layoutFactory;
/** /**
* A list of the libraries that must be unpacked from fat jars in order to run. * A list of the libraries that must be unpacked from fat jars in order to run.
* Specify each library as a <code>&lt;dependency&gt;</code> with a * Specify each library as a <code>&lt;dependency&gt;</code> with a
@ -224,7 +230,9 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
} }
private Repackager getRepackager(File source) { private Repackager getRepackager(File source) {
Repackager repackager = new LoggingRepackager(source, getLog()); Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(
new LoggingMainClassTimeoutWarningListener());
repackager.setMainClass(this.mainClass); repackager.setMainClass(this.mainClass);
if (this.layout != null) { if (this.layout != null) {
getLog().info("Layout: " + this.layout); getLog().info("Layout: " + this.layout);
@ -356,30 +364,15 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
} }
private static class LoggingRepackager extends Repackager { private class LoggingMainClassTimeoutWarningListener
implements MainClassTimeoutWarningListener {
private final Log log;
LoggingRepackager(File source, Log log) {
super(source);
this.log = log;
}
@Override @Override
protected String findMainMethod(JarFile source) throws IOException { public void handleTimeoutWarning(long duration, String mainMethod) {
long startTime = System.currentTimeMillis(); getLog().warn("Searching for the main-class is taking some time, "
try { + "consider using the mainClass configuration " + "parameter");
return super.findMainMethod(source);
}
finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
this.log.warn("Searching for the main-class is taking some time, "
+ "consider using the mainClass configuration "
+ "parameter");
}
}
} }
} }
} }

@ -0,0 +1,58 @@
-----
Use a custom layout
-----
Dave Syer
-----
2016-10-30
-----
Spring Boot repackages the jar file for this project using a custom layout factory
defined in the additional jar file, provided as a dependency to the build plugin:
---
<project>
...
<build>
...
<plugins>
...
<plugin>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<layoutFactory implementation="com.example.CustomLayoutFactory">
<customProperty>value</customProperty>
</layoutFactory>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.example</groupid>
<artifactId>custom-layout</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
</dependency>
</dependencies>
...
</plugin>
...
</plugins>
...
</build>
...
</project>
+---
The layout factory is provided as an implementation of <<<LayoutFactory>>> (from
spring-boot-loader-tools) explicitly specified in the pom. If there is only one custom
<<<LayoutFactory>>> on the plugin classpath and it is listed in
<<<META-INF/spring.factories>>> then it is unnecessary to explicitly set it in the
plugin configuration.
Layout factories are always ignored if an explicit <<layout>> is set.

@ -48,6 +48,8 @@ Spring Boot Maven Plugin
* {{{./examples/repackage-disable-attach.html}Local repackaged artifact}} * {{{./examples/repackage-disable-attach.html}Local repackaged artifact}}
* {{{./examples/custom-layout.html}Custom layout}}
* {{{./examples/exclude-dependency.html}Exclude a dependency}} * {{{./examples/exclude-dependency.html}Exclude a dependency}}
* {{{./examples/run-debug.html}Debug the application}} * {{{./examples/run-debug.html}Debug the application}}

Loading…
Cancel
Save