diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc index 805c56a401..ebd5523039 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc @@ -264,7 +264,7 @@ include::../gradle/packaging/boot-war-properties-launcher.gradle.kts[tags=proper [[packaging-layered-jars]] ==== Packaging layered jars -By default, the `bootJar` tasks builds an archive that contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively. +By default, the `bootJar` task builds an archive that contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively. For cases where a docker image needs to be built from the contents of the jar, the jar format can be enhanced to support layer folders. To use this feature, the layering feature must be enabled: @@ -280,12 +280,16 @@ include::../gradle/packaging/boot-jar-layered.gradle[tags=layered] include::../gradle/packaging/boot-jar-layered.gradle.kts[tags=layered] ---- -The jar will then be split into layer folders which may include: +By default, the following layers are created: -* `application` -* `resources` -* `snapshots-dependencies` -* `dependencies` +* `dependencies` for any dependency whose version does not contain `SNAPSHOT`. +* `snapshot-dependencies` for any dependency whose version contains `SNAPSHOT`. +* `resources` for static resources at the default locations, i.e. `META-INF/resources/`, `resources/`, `static/`, `public/`. +* `application` for any other classes and resources. + +The layers order is important as it determines how likely previous layers can be cached when part of the application changes. +The default order is `dependencies`, `snapshot-dependencies`, `resources`, and `application`. +Content that is least likely to change should be added first, followed by layers that are more likely to change. When you create a layered jar, the `spring-boot-layertools` jar will be added as a dependency to your jar. With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers. @@ -301,4 +305,33 @@ include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle[tags=layered] .Kotlin ---- include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts[tags=layered] ----- \ No newline at end of file +---- + +[[packaging-layers-configuration]] +===== Custom Layers configuration +Depending on your application, you may want to tune how layers are created and add new ones. +This can be done using configuration that lists the layers and their order as well as the strategies to apply to libraries and classes. + +The following example shows what the implicit layer configuration described above does: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-layered-custom.gradle[tags=layered] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-layered-custom.gradle.kts[tags=layered] +---- + +Each `layerContent` closure defines a strategy to include or exclude an entry of the jar in a layer. +When an entry matches a strategy, it is included in the layer and further strategies are ignored. +This is illustrated by the `dependencies` and `application` layers that have a "catch-all" include filter used to add any libraries or classes that were not processed by previous strategies. + +The content of a `libraries` layer can be customized using filters to `include` or `exclude` based on the dependency coordinates. +The format is `groupId:artifactId[:version]`. +In the example above, any artifact whose version ends with `SNAPSHOT` is going to be included in the `snapshot-dependencies` layer. + +The content of a `classes` layer can be customized using filters to `included` or `exclude` based on location of the entry using Ant-style pattern matching. \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle new file mode 100644 index 0000000000..8e802ced8e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClassName 'com.example.ExampleApplication' +} + +// tag::layered[] +bootJar { + layers { + layers "dependencies", "snapshot-dependencies", "resources", "application" + libraries { + layerContent("snapshot-dependencies") { + coordinates { + include "*:*:*SNAPSHOT" + } + } + layerContent("dependencies") { + coordinates { + include "*:*" + } + } + } + classes { + layerContent("resources") { + locations { + include "META-INF/resources/**", "resources/**" + include "static/**", "public/**" + } + } + layerContent("application") { + locations { + include "**" + } + } + } + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts new file mode 100644 index 0000000000..d0b5d4ffb3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts @@ -0,0 +1,40 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{version}" +} + +// tag::layered[] +tasks.getByName("bootJar") { + layers { + includeLayerTools = false + layers("dependencies", "snapshot-dependencies", "resources", "application") + libraries { + layerContent("snapshot-dependencies") { + coordinates { + include("*:*:*SNAPSHOT") + } + } + layerContent("dependencies") { + coordinates { + include("*:*") + } + } + } + classes { + layerContent("resources") { + locations { + include("META-INF/resources/**", "resources/**") + include("static/**", "public/**") + } + } + layerContent("application") { + locations { + include("**") + } + } + } + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle index 4fe2da44f4..3b8902309a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle @@ -9,7 +9,7 @@ bootJar { // tag::layered[] bootJar { - layered { + layers { includeLayerTools = false } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts index 8c61012da4..7f2a5e9976 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts @@ -11,7 +11,7 @@ tasks.getByName("bootJar") { // tag::layered[] tasks.getByName("bootJar") { - layered { + layers { includeLayerTools = false } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle index 502f87f4c6..4ec810698f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle @@ -9,6 +9,6 @@ bootJar { // tag::layered[] bootJar { - layered() + layers() } // end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts index 2d6735740a..a7c5a41482 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts @@ -11,6 +11,6 @@ tasks.getByName("bootJar") { // tag::layered[] tasks.getByName("bootJar") { - layered() + layers() } // end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java index dd6c18f6b5..db4073f82d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java @@ -27,6 +27,7 @@ import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.artifacts.publish.ArchivePublishArtifact; import org.gradle.api.plugins.ApplicationPlugin; @@ -87,11 +88,12 @@ final class JavaPluginAction implements PluginApplicationAction { bootJar.setDescription( "Assembles an executable jar archive containing the main classes and their dependencies."); bootJar.setGroup(BasePlugin.BUILD_GROUP); - bootJar.classpath((Callable) () -> { - SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME); - return mainSourceSet.getRuntimeClasspath(); - }); + SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + bootJar.classpath((Callable) () -> mainSourceSet.getRuntimeClasspath()); + Configuration runtimeClasspathConfiguration = project.getConfigurations() + .getByName(mainSourceSet.getRuntimeClasspathConfigurationName()); + runtimeClasspathConfiguration.getIncoming().afterResolve(bootJar::resolvedDependencies); bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath)); return bootJar; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index d431bbc152..5e5ebd4d1b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -24,13 +24,18 @@ import java.io.InputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.gradle.api.Action; +import org.gradle.api.artifacts.ArtifactCollection; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCopyDetails; @@ -47,6 +52,8 @@ import org.gradle.api.tasks.bundling.Jar; import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.Layers; import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.layer.CustomLayers; import org.springframework.util.FileCopyUtils; /** @@ -54,6 +61,7 @@ import org.springframework.util.FileCopyUtils; * * @author Andy Wilkinson * @author Madhura Bhave + * @author Scott Frederick * @since 2.0.0 */ public class BootJar extends Jar implements BootArchive { @@ -75,6 +83,8 @@ public class BootJar extends Jar implements BootArchive { private final List dependencies = new ArrayList<>(); + private final Map coordinatesByFileName = new HashMap<>(); + /** * Creates a new {@code BootJar} task. */ @@ -177,12 +187,37 @@ public class BootJar extends Jar implements BootArchive { /** * Configures the archive to have layers. */ - public void layered() { + public void layers() { enableLayers(); - applyLayers(); + } + + public void layers(Action action) { + action.execute(enableLayers()); + } + + private LayerConfiguration enableLayers() { + if (this.layerConfiguration == null) { + this.layerConfiguration = new LayerConfiguration(); + } + + return this.layerConfiguration; } private void applyLayers() { + if (this.layerConfiguration == null) { + return; + } + + if (this.layerConfiguration.getLayers() == null || this.layerConfiguration.getLayers().isEmpty()) { + this.layers = Layers.IMPLICIT; + } + else { + List customLayers = this.layerConfiguration.getLayers().stream().map(Layer::new) + .collect(Collectors.toList()); + this.layers = new CustomLayers(customLayers, this.layerConfiguration.getClasses(), + this.layerConfiguration.getLibraries()); + } + if (this.layerConfiguration.isIncludeLayerTools()) { this.bootInf.into("lib", (spec) -> spec.from((Callable) () -> { String jarName = "spring-boot-jarmode-layertools.jar"; @@ -204,24 +239,13 @@ public class BootJar extends Jar implements BootArchive { this.bootInf.into("", (spec) -> spec.from(createLayersIndex())); } - public void layered(Action action) { - action.execute(enableLayers()); - applyLayers(); - } - - private LayerConfiguration enableLayers() { - this.layers = Layers.IMPLICIT; - if (this.layerConfiguration == null) { - this.layerConfiguration = new LayerConfiguration(); - } - - return this.layerConfiguration; - } - private Layer layerForFileDetails(FileCopyDetails details) { String path = details.getPath(); if (path.startsWith("BOOT-INF/lib/")) { - return this.layers.getLayer(new Library(details.getFile(), null)); + String coordinates = this.coordinatesByFileName.get(details.getName()); + LibraryCoordinates libraryCoordinates = (coordinates != null) ? new LibraryCoordinates(coordinates) + : new LibraryCoordinates("?:?:?"); + return this.layers.getLayer(new Library(null, details.getFile(), null, false, libraryCoordinates)); } if (path.startsWith("BOOT-INF/classes/")) { return this.layers.getLayer(details.getSourcePath()); @@ -248,9 +272,16 @@ public class BootJar extends Jar implements BootArchive { } } + private void resolveCoordinatesForFiles(ResolvableDependencies resolvableDependencies) { + ArtifactCollection resolvedArtifactResults = resolvableDependencies.getArtifacts(); + Set artifacts = resolvedArtifactResults.getArtifacts(); + artifacts.forEach((artifact) -> this.coordinatesByFileName.put(artifact.getFile().getName(), + artifact.getId().getComponentIdentifier().getDisplayName())); + } + @Input boolean isLayered() { - return this.layers != null; + return this.layerConfiguration != null; } @Override @@ -285,6 +316,13 @@ public class BootJar extends Jar implements BootArchive { this.support.setExcludeDevtools(excludeDevtools); } + public void resolvedDependencies(ResolvableDependencies resolvableDependencies) { + if (resolvableDependencies != null) { + resolveCoordinatesForFiles(resolvableDependencies); + } + applyLayers(); + } + /** * Returns a {@code CopySpec} that can be used to add content to the {@code BOOT-INF} * directory of the jar. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java index 1639a6f567..38eb7a8af2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java @@ -16,18 +16,42 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.gradle.api.Action; import org.gradle.api.tasks.Input; +import org.springframework.boot.loader.tools.layer.classes.FilteredResourceStrategy; +import org.springframework.boot.loader.tools.layer.classes.LocationFilter; +import org.springframework.boot.loader.tools.layer.classes.ResourceFilter; +import org.springframework.boot.loader.tools.layer.classes.ResourceStrategy; +import org.springframework.boot.loader.tools.layer.library.CoordinateFilter; +import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy; +import org.springframework.boot.loader.tools.layer.library.LibraryFilter; +import org.springframework.boot.loader.tools.layer.library.LibraryStrategy; +import org.springframework.util.Assert; + /** * Encapsulates the configuration for a layered jar. * * @author Madhura Bhave + * @author Scott Frederick * @since 2.3.0 */ public class LayerConfiguration { private boolean includeLayerTools = true; + private List layerNames = new ArrayList<>(); + + private List resourceStrategies = new ArrayList<>(); + + private List libraryStrategies = new ArrayList<>(); + + private StrategySpec strategySpec; + /** * Whether to include the layer tools jar. * @return true if layer tools is included @@ -41,4 +65,164 @@ public class LayerConfiguration { this.includeLayerTools = includeLayerTools; } + @Input + public List getLayers() { + return this.layerNames; + } + + public void layers(String... layers) { + this.layerNames = Arrays.asList(layers); + } + + public void layers(List layers) { + this.layerNames = layers; + } + + @Input + public List getClasses() { + return this.resourceStrategies; + } + + public void classes(ResourceStrategy... resourceStrategies) { + this.resourceStrategies = Arrays.asList(resourceStrategies); + } + + public void classes(Action config) { + this.strategySpec = StrategySpec.forResources(); + config.execute(this); + } + + @Input + public List getLibraries() { + return this.libraryStrategies; + } + + public void libraries(LibraryStrategy... strategies) { + this.libraryStrategies = Arrays.asList(strategies); + } + + public void libraries(Action configure) { + this.strategySpec = StrategySpec.forLibraries(); + configure.execute(this); + } + + public void layerContent(String layerName, Action config) { + this.strategySpec.newStrategy(); + config.execute(this); + if (this.strategySpec.isLibrariesStrategy()) { + this.libraryStrategies.add(new FilteredLibraryStrategy(layerName, this.strategySpec.libraryFilters())); + } + else { + this.resourceStrategies.add(new FilteredResourceStrategy(layerName, this.strategySpec.resourceFilters())); + } + } + + public void coordinates(Action config) { + Assert.state(this.strategySpec.isLibrariesStrategy(), + "The 'coordinates' filter must be used only with libraries"); + this.strategySpec.newFilter(); + config.execute(this); + this.strategySpec + .addLibraryFilter(new CoordinateFilter(this.strategySpec.includes(), this.strategySpec.excludes())); + } + + public void locations(Action config) { + Assert.state(this.strategySpec.isResourcesStrategy(), "The 'locations' filter must be used only with classes"); + this.strategySpec.newFilter(); + config.execute(this); + this.strategySpec + .addResourceFilter(new LocationFilter(this.strategySpec.includes(), this.strategySpec.excludes())); + } + + public void include(String... includes) { + this.strategySpec.include(includes); + } + + public void exclude(String... excludes) { + this.strategySpec.exclude(excludes); + } + + private static final class StrategySpec { + + private enum TYPE { + + LIBRARIES, RESOURCES; + + } + + private final TYPE type; + + private List libraryFilters; + + private List resourceFilters; + + private List filterIncludes; + + private List filterExcludes; + + private StrategySpec(TYPE type) { + this.type = type; + } + + private boolean isLibrariesStrategy() { + return this.type == TYPE.LIBRARIES; + } + + private boolean isResourcesStrategy() { + return this.type == TYPE.RESOURCES; + } + + private void newStrategy() { + this.libraryFilters = new ArrayList<>(); + this.resourceFilters = new ArrayList<>(); + newFilter(); + } + + private void newFilter() { + this.filterIncludes = new ArrayList<>(); + this.filterExcludes = new ArrayList<>(); + } + + private List libraryFilters() { + return this.libraryFilters; + } + + private void addLibraryFilter(LibraryFilter filter) { + this.libraryFilters.add(filter); + } + + private List resourceFilters() { + return this.resourceFilters; + } + + private void addResourceFilter(ResourceFilter filter) { + this.resourceFilters.add(filter); + } + + private List includes() { + return this.filterIncludes; + } + + private void include(String... includes) { + this.filterIncludes.addAll(Arrays.asList(includes)); + } + + private void exclude(String... excludes) { + this.filterIncludes.addAll(Arrays.asList(excludes)); + } + + private List excludes() { + return this.filterExcludes; + } + + private static StrategySpec forLibraries() { + return new StrategySpec(TYPE.LIBRARIES); + } + + private static StrategySpec forResources() { + return new StrategySpec(TYPE.RESOURCES); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java index 195b96ac07..c96a1ef842 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java @@ -16,7 +16,14 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.File; +import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.jar.JarFile; import org.gradle.testkit.runner.InvalidRunnerConfigurationException; import org.gradle.testkit.runner.TaskOutcome; @@ -38,8 +45,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { } @TestTemplate - void upToDateWhenBuiltTwiceWithLayers() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException { + void upToDateWhenBuiltTwiceWithLayers() throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome()) @@ -48,7 +54,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { @TestTemplate void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException { + throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); @@ -56,11 +62,76 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { @TestTemplate void notUpToDateWhenBuiltWithLayersAndToolsAndThenWithLayersAndWithoutTools() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException { + throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); assertThat(this.gradleBuild.build("-Playered=true", "-PexcludeTools=true", "bootJar").task(":bootJar") .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } + @TestTemplate + void implicitLayers() throws IOException { + writeMainClass(); + writeResource(); + assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar")) + .isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar")) + .isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/application/classes/example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/resources/classes/static/file.txt")).isNotNull(); + } + } + + @TestTemplate + void customLayers() throws IOException { + writeMainClass(); + writeResource(); + assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar")) + .isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/commons-dependencies/lib/commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar")) + .isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/app/classes/example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers/static/classes/static/file.txt")).isNotNull(); + } + } + + private void writeMainClass() { + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Main.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("import java.io.IOException;"); + writer.println(); + writer.println("public class Main {"); + writer.println(); + writer.println(" public static void main(String[] args) {"); + writer.println(" }"); + writer.println(); + writer.println("}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void writeResource() { + try { + Path path = this.gradleBuild.getProjectDir().toPath() + .resolve(Paths.get("src", "main", "resources", "static", "file.txt")); + Files.createDirectories(path.getParent()); + Files.createFile(path); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java index 7f1ff89adb..b6bfbaa33b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -20,21 +20,38 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import org.gradle.api.Action; +import org.gradle.api.artifacts.ArtifactCollection; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.junit.jupiter.api.Test; +import org.springframework.boot.loader.tools.layer.classes.FilteredResourceStrategy; +import org.springframework.boot.loader.tools.layer.classes.LocationFilter; +import org.springframework.boot.loader.tools.layer.library.CoordinateFilter; +import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link BootJar}. * * @author Andy Wilkinson * @author Madhura Bhave + * @author Scott Frederick */ class BootJarTests extends AbstractBootArchiveTests { @@ -102,6 +119,36 @@ class BootJarTests extends AbstractBootArchiveTests { .contains("BOOT-INF/layers/resources/classes/static/test.css"); } + @Test + void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException { + File jar = createLayeredJar((configuration) -> { + configuration.layers("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); + configuration.libraries(createLibraryStrategy("my-snapshot-deps", "com.example:*:*.SNAPSHOT"), + createLibraryStrategy("my-internal-deps", "com.example:*:*"), + createLibraryStrategy("my-deps", "*:*")); + configuration.classes(createResourceStrategy("resources", "static/**"), + createResourceStrategy("application", "**")); + }); + List entryNames = getEntryNames(jar); + assertThat(entryNames) + .containsSubsequence("BOOT-INF/layers/my-internal-deps/lib/first-library.jar", + "BOOT-INF/layers/my-internal-deps/lib/second-library.jar") + .contains("BOOT-INF/layers/my-snapshot-deps/lib/third-library-SNAPSHOT.jar") + .containsSubsequence("BOOT-INF/layers/application/classes/com/example/Application.class", + "BOOT-INF/layers/application/classes/application.properties") + .contains("BOOT-INF/layers/resources/classes/static/test.css"); + } + + private FilteredLibraryStrategy createLibraryStrategy(String layerName, String... includes) { + return new FilteredLibraryStrategy(layerName, + Collections.singletonList(new CoordinateFilter(Arrays.asList(includes), Collections.emptyList()))); + } + + private FilteredResourceStrategy createResourceStrategy(String layerName, String... includes) { + return new FilteredResourceStrategy(layerName, + Collections.singletonList(new LocationFilter(Arrays.asList(includes), Collections.emptyList()))); + } + @Test void whenJarIsLayeredJarsInLibAreStored() throws IOException { try (JarFile jarFile = new JarFile(createLayeredJar())) { @@ -154,13 +201,13 @@ class BootJarTests extends AbstractBootArchiveTests { } private File createLayeredJar(Action action) throws IOException { - addContent(); if (action != null) { - getTask().layered(action); + getTask().layers(action); } else { - getTask().layered(); + getTask().layers(); } + addContent(); executeTask(); return getTask().getArchiveFile().get().getAsFile(); } @@ -186,6 +233,33 @@ class BootJarTests extends AbstractBootArchiveTests { css.createNewFile(); bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"), jarFile("third-library-SNAPSHOT.jar")); + + Set resolvedArtifacts = new HashSet<>(); + resolvedArtifacts.add(mockLibraryArtifact("first-library.jar", "com.example:first-library:1.0.0")); + resolvedArtifacts.add(mockLibraryArtifact("second-library.jar", "com.example:second-library:1.0.0")); + resolvedArtifacts + .add(mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example:third-library:1.0.0.SNAPSHOT")); + + ArtifactCollection artifacts = mock(ArtifactCollection.class); + given(artifacts.getArtifacts()).willReturn(resolvedArtifacts); + + ResolvableDependencies deps = mock(ResolvableDependencies.class); + given(deps.getArtifacts()).willReturn(artifacts); + bootJar.resolvedDependencies(deps); + } + + private ResolvedArtifactResult mockLibraryArtifact(String fileName, String coordinates) { + ComponentIdentifier libraryId = mock(ComponentIdentifier.class); + given(libraryId.getDisplayName()).willReturn(coordinates); + + ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class); + given(libraryArtifactId.getComponentIdentifier()).willReturn(libraryId); + + ResolvedArtifactResult libraryArtifact = mock(ResolvedArtifactResult.class); + given(libraryArtifact.getFile()).willReturn(new File(fileName)); + given(libraryArtifact.getId()).willReturn(libraryArtifactId); + + return libraryArtifact; } private List entryLines(JarFile jarFile, String entryName) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle new file mode 100644 index 0000000000..92adc33456 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClassName = 'com.example.Application' + layers { + layers "dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app" + libraries { + layerContent("snapshot-dependencies") { coordinates { include "*:*:*SNAPSHOT" } } + layerContent("commons-dependencies") { coordinates { include "org.apache.commons:*" } } + layerContent("dependencies") { coordinates { include "*:*" } } + } + classes { + layerContent("static") { + locations { + include "META-INF/resources/**", "resources/**" + include "static/**", "public/**" + } + } + layerContent("app") { + locations { + include "**" + } + } + } + } +} + +repositories { + mavenCentral() + maven { url "https://repository.apache.org/content/repositories/snapshots" } +} + +dependencies { + implementation("org.apache.commons:commons-lang3:3.9") + implementation("commons-io:commons-io:2.7-SNAPSHOT") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle new file mode 100644 index 0000000000..0d12faa1f1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClassName = 'com.example.Application' + layers() +} + +repositories { + mavenCentral() + maven { url "https://repository.apache.org/content/repositories/snapshots" } +} + +dependencies { + implementation("org.apache.commons:commons-lang3:3.9") + implementation("commons-io:commons-io:2.7-SNAPSHOT") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle index fb56a68e55..8ad873d218 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle @@ -11,7 +11,7 @@ bootJar { } } if (project.hasProperty('layered') && project.getProperty('layered')) { - layered { + layers { includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java index 484bc2286c..8b813f448d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java @@ -16,6 +16,7 @@ package org.springframework.boot.loader.tools; +import java.io.Serializable; import java.util.regex.Pattern; import org.springframework.util.Assert; @@ -28,7 +29,7 @@ import org.springframework.util.Assert; * @since 2.3.0 * @see Layers */ -public class Layer { +public class Layer implements Serializable { private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java index b4847ead1b..963de32dbf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java @@ -57,6 +57,7 @@ public class CustomLayers implements Layers { for (ResourceStrategy strategy : this.resourceStrategies) { Layer matchingLayer = strategy.getMatchingLayer(resourceName); if (matchingLayer != null) { + validateLayerName(matchingLayer, "Resource '" + resourceName + "'"); return matchingLayer; } } @@ -68,10 +69,18 @@ public class CustomLayers implements Layers { for (LibraryStrategy strategy : this.libraryStrategies) { Layer matchingLayer = strategy.getMatchingLayer(library); if (matchingLayer != null) { + validateLayerName(matchingLayer, "Library '" + library.getName() + "'"); return matchingLayer; } } throw new IllegalStateException("Library '" + library.getName() + "' did not match any layer."); } + private void validateLayerName(Layer layer, String nameText) { + if (!this.layers.contains(layer)) { + throw new IllegalStateException(nameText + " matched a layer '" + layer + + "' that is not included in the configured layers " + this.layers + "."); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceFilter.java index cc87fa4549..2ad4cc85b8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceFilter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceFilter.java @@ -16,13 +16,15 @@ package org.springframework.boot.loader.tools.layer.classes; +import java.io.Serializable; + /** * A filter that can tell if a resource has been included or excluded. * * @author Madhura Bhave * @since 2.3.0 */ -public interface ResourceFilter { +public interface ResourceFilter extends Serializable { /** * Return true if the resource is included by the filter. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceStrategy.java index e041bb590d..40efc33fdd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceStrategy.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceStrategy.java @@ -16,6 +16,8 @@ package org.springframework.boot.loader.tools.layer.classes; +import java.io.Serializable; + import org.springframework.boot.loader.tools.Layer; /** @@ -24,7 +26,7 @@ import org.springframework.boot.loader.tools.Layer; * @author Madhura Bhave * @since 2.3.0 */ -public interface ResourceStrategy { +public interface ResourceStrategy extends Serializable { /** * Return a {@link Layer} for the given resource. If no matching layer is found, diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java index ea1ae8e465..7b21995f5b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java @@ -16,6 +16,8 @@ package org.springframework.boot.loader.tools.layer.library; +import java.io.Serializable; + import org.springframework.boot.loader.tools.Library; /** @@ -24,7 +26,7 @@ import org.springframework.boot.loader.tools.Library; * @author Madhura Bhave * @since 2.3.0 */ -public interface LibraryFilter { +public interface LibraryFilter extends Serializable { /** * Return true if the {@link Library} is included by the filter. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java index 93e9b3ff72..1d23d16032 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java @@ -16,6 +16,8 @@ package org.springframework.boot.loader.tools.layer.library; +import java.io.Serializable; + import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.Library; @@ -25,7 +27,7 @@ import org.springframework.boot.loader.tools.Library; * @author Madhura Bhave * @since 2.3.0 */ -public interface LibraryStrategy { +public interface LibraryStrategy extends Serializable { /** * Return a {@link Layer} for the given {@link Library}. If no matching layer is diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java index 09577065bf..37a2f6f33c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java @@ -73,6 +73,18 @@ class CustomLayersTests { assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer("com/acme")); } + @Test + void layerForResourceIsNotInListedLayers() { + FilteredResourceStrategy resourceStrategy = new FilteredResourceStrategy("test-not-listed", Collections + .singletonList(new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList()))); + Layer targetLayer = new Layer("test"); + CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer), + Collections.singletonList(resourceStrategy), Collections.emptyList()); + assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer("META-INF/manifest.mf")) + .withMessageContaining("META-INF/manifest.mf").withMessageContaining("test-not-listed") + .withMessageContaining("[test]"); + } + @Test void layerForLibraryIsFound() { FilteredLibraryStrategy libraryStrategy = new FilteredLibraryStrategy("test", Collections @@ -92,9 +104,22 @@ class CustomLayersTests { assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer(mockLibrary("org.another:test"))); } + @Test + void layerForLibraryIsNotInListedLayers() { + FilteredLibraryStrategy libraryStrategy = new FilteredLibraryStrategy("test-not-listed", Collections + .singletonList(new CoordinateFilter(Collections.singletonList("com.acme:*"), Collections.emptyList()))); + Layer targetLayer = new Layer("test"); + CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer), Collections.emptyList(), + Collections.singletonList(libraryStrategy)); + assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer(mockLibrary("com.acme:test"))) + .withMessageContaining("com.acme:test").withMessageContaining("test-not-listed") + .withMessageContaining("[test]"); + } + private Library mockLibrary(String coordinates) { Library library = mock(Library.class); given(library.getCoordinates()).willReturn(new LibraryCoordinates(coordinates)); + given(library.getName()).willReturn(coordinates); return library; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc index 8973f3b805..d7d637470e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc @@ -100,14 +100,14 @@ To use this feature, the layering feature must be enabled: By default, the following layers are created: -* `application` for any other classes and resources. -* `resources` for static resources at the default locations, i.e. `META-INF/resources/`, `resources/`, `static/`, `public/`. +* `dependencies` for any dependency whose version does not contain `SNAPSHOT`. * `snapshot-dependencies` for any dependency whose version contains `SNAPSHOT`. -* `dependencies` for any other dependency. +* `resources` for static resources at the default locations, i.e. `META-INF/resources/`, `resources/`, `static/`, `public/`. +* `application` for any other classes and resources. The layers order is important as it determines how likely previous layers can be cached when part of the application changes. -The default order is `application`, `resources`, `snapshot-dependencies` and `dependencies`. -Content that is likely to change should be added first, followed by layers that are less likely to change. +The default order is `dependencies`, `snapshot-dependencies`, `resources`, and `application`. +Content that is least likely to change should be added first, followed by layers that are more likely to change. @@ -147,11 +147,11 @@ The following example shows what the implicit layer configuration described abov xsi:schemaLocation="http://www.springframework.org/schema/boot/layers https://www.springframework.org/schema/boot/layers/layers-configuration.xsd"> - application - resources - snapshots dependencies - + snapshot-dependencies + resources + application + @@ -182,15 +182,15 @@ The following example shows what the implicit layer configuration described abov ---- -Each `layer-content` element defines a strategy to include an entry of the jar in a layer. +Each `layer-content` element defines a strategy to include or exclude an entry of the jar in a layer. When an entry matches a strategy, it is included in the layer and further strategies are ignored. This is illustrated by the `dependencies` and `application` layers that have a "catch-all" include filter used to add any libraries or classes that were not processed by previous strategies. -The content of a libraries layer can be customized using filters on the coordinates. +The content of a `libraries` layer can be customized using filters to `include` or `exclude` based on the dependency coordinates. The format is `groupId:artifactId[:version]`. In the example above, any artifact whose version ends with `SNAPSHOT` is going to be included in the `snapshot-dependencies` layer. -The content of a classes layer can be customized using filters on location of the entry using Ant-style pattern matching. +The content of a `classes` layer can be customized using filters to `include` or `exclude` based on location of the entry using Ant-style pattern matching.