Add support for customizing layers in Gradle
This commit adds configuration to the Spring Boot Gradle plugin that allows the names and contents of layers to be customized in the build configuration. Fixes gh-20296
This commit is contained in:
@@ -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]
|
||||
----
|
||||
----
|
||||
|
||||
[[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.
|
||||
@@ -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[]
|
||||
@@ -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>("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[]
|
||||
@@ -9,7 +9,7 @@ bootJar {
|
||||
|
||||
// tag::layered[]
|
||||
bootJar {
|
||||
layered {
|
||||
layers {
|
||||
includeLayerTools = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ tasks.getByName<BootJar>("bootJar") {
|
||||
|
||||
// tag::layered[]
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
layered {
|
||||
layers {
|
||||
includeLayerTools = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ bootJar {
|
||||
|
||||
// tag::layered[]
|
||||
bootJar {
|
||||
layered()
|
||||
layers()
|
||||
}
|
||||
// end::layered[]
|
||||
|
||||
@@ -11,6 +11,6 @@ tasks.getByName<BootJar>("bootJar") {
|
||||
|
||||
// tag::layered[]
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
layered()
|
||||
layers()
|
||||
}
|
||||
// end::layered[]
|
||||
|
||||
@@ -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<FileCollection>) () -> {
|
||||
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<FileCollection>) () -> mainSourceSet.getRuntimeClasspath());
|
||||
Configuration runtimeClasspathConfiguration = project.getConfigurations()
|
||||
.getByName(mainSourceSet.getRuntimeClasspathConfigurationName());
|
||||
runtimeClasspathConfiguration.getIncoming().afterResolve(bootJar::resolvedDependencies);
|
||||
bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath));
|
||||
return bootJar;
|
||||
}
|
||||
|
||||
@@ -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<String> dependencies = new ArrayList<>();
|
||||
|
||||
private final Map<String, String> 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<LayerConfiguration> 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<Layer> 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<File>) () -> {
|
||||
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<LayerConfiguration> 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<ResolvedArtifactResult> 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.
|
||||
|
||||
@@ -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<String> layerNames = new ArrayList<>();
|
||||
|
||||
private List<ResourceStrategy> resourceStrategies = new ArrayList<>();
|
||||
|
||||
private List<LibraryStrategy> 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<String> getLayers() {
|
||||
return this.layerNames;
|
||||
}
|
||||
|
||||
public void layers(String... layers) {
|
||||
this.layerNames = Arrays.asList(layers);
|
||||
}
|
||||
|
||||
public void layers(List<String> layers) {
|
||||
this.layerNames = layers;
|
||||
}
|
||||
|
||||
@Input
|
||||
public List<ResourceStrategy> getClasses() {
|
||||
return this.resourceStrategies;
|
||||
}
|
||||
|
||||
public void classes(ResourceStrategy... resourceStrategies) {
|
||||
this.resourceStrategies = Arrays.asList(resourceStrategies);
|
||||
}
|
||||
|
||||
public void classes(Action<LayerConfiguration> config) {
|
||||
this.strategySpec = StrategySpec.forResources();
|
||||
config.execute(this);
|
||||
}
|
||||
|
||||
@Input
|
||||
public List<LibraryStrategy> getLibraries() {
|
||||
return this.libraryStrategies;
|
||||
}
|
||||
|
||||
public void libraries(LibraryStrategy... strategies) {
|
||||
this.libraryStrategies = Arrays.asList(strategies);
|
||||
}
|
||||
|
||||
public void libraries(Action<LayerConfiguration> configure) {
|
||||
this.strategySpec = StrategySpec.forLibraries();
|
||||
configure.execute(this);
|
||||
}
|
||||
|
||||
public void layerContent(String layerName, Action<LayerConfiguration> 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<LayerConfiguration> 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<LayerConfiguration> 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<LibraryFilter> libraryFilters;
|
||||
|
||||
private List<ResourceFilter> resourceFilters;
|
||||
|
||||
private List<String> filterIncludes;
|
||||
|
||||
private List<String> 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<LibraryFilter> libraryFilters() {
|
||||
return this.libraryFilters;
|
||||
}
|
||||
|
||||
private void addLibraryFilter(LibraryFilter filter) {
|
||||
this.libraryFilters.add(filter);
|
||||
}
|
||||
|
||||
private List<ResourceFilter> resourceFilters() {
|
||||
return this.resourceFilters;
|
||||
}
|
||||
|
||||
private void addResourceFilter(ResourceFilter filter) {
|
||||
this.resourceFilters.add(filter);
|
||||
}
|
||||
|
||||
private List<String> 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<String> excludes() {
|
||||
return this.filterExcludes;
|
||||
}
|
||||
|
||||
private static StrategySpec forLibraries() {
|
||||
return new StrategySpec(TYPE.LIBRARIES);
|
||||
}
|
||||
|
||||
private static StrategySpec forResources() {
|
||||
return new StrategySpec(TYPE.RESOURCES);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<BootJar> {
|
||||
|
||||
@@ -102,6 +119,36 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
||||
.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<String> 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<BootJar> {
|
||||
}
|
||||
|
||||
private File createLayeredJar(Action<LayerConfiguration> 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<BootJar> {
|
||||
css.createNewFile();
|
||||
bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
|
||||
jarFile("third-library-SNAPSHOT.jar"));
|
||||
|
||||
Set<ResolvedArtifactResult> 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<String> entryLines(JarFile jarFile, String entryName) throws IOException {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -11,7 +11,7 @@ bootJar {
|
||||
}
|
||||
}
|
||||
if (project.hasProperty('layered') && project.getProperty('layered')) {
|
||||
layered {
|
||||
layers {
|
||||
includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-]+$");
|
||||
|
||||
|
||||
@@ -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 + ".");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
<layers>
|
||||
<layer>application</layer>
|
||||
<layer>resources</layer>
|
||||
<layer>snapshots</layer>
|
||||
<layer>dependencies</layer>
|
||||
</layers>
|
||||
<layer>snapshot-dependencies</layer>
|
||||
<layer>resources</layer>
|
||||
<layer>application</layer>
|
||||
</layers>
|
||||
<libraries>
|
||||
<layer-content layer="snapshot-dependencies">
|
||||
<coordinates>
|
||||
@@ -182,15 +182,15 @@ The following example shows what the implicit layer configuration described abov
|
||||
</layers-configuration>
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user