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 28d8a8482ea..64bf1c5e6e9 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 @@ -282,10 +282,10 @@ Layered jars use the same layout as regular boot packaged jars, but include an a By default, the following layers are defined: -* `dependencies` for any dependency whose version does not contain `SNAPSHOT`. +* `dependencies` for any non-project dependency whose version does not contain `SNAPSHOT`. * `spring-boot-loader` for the jar loader classes. -* `snapshot-dependencies` for any dependency whose version contains `SNAPSHOT`. -* `application` for application classes and resources. +* `snapshot-dependencies` for any non-project dependency whose version contains `SNAPSHOT`. +* `application` for project dependencies, application 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`, `spring-boot-loader`, `snapshot-dependencies`, `application`. @@ -355,13 +355,15 @@ Any content not claimed by an earlier `intoLayer` closure remains available for The `intoLayer` closure claims content using nested `include` and `exclude` calls. The `application` closure uses Ant-style patch matching for include/exclude parameters. The `dependencies` section uses `group:artifact[:version]` patterns. +It also provides `includeProjectDependencies()` and `excludeProjectDependencies()` methods that can be used to include or exclude project dependencies. If no `include` call is made, then all content (not claimed by an earlier closure) is considered. If no `exclude` call is made, then no exclusions are applied. -Looking at the `dependencies` closure in the example above, we can see that the first `intoLayer` will claim all SNAPSHOT dependencies for the `snapshot-dependencies` layer. -The subsequent `intoLayer` will claim anything left (in this case, any dependency that is not a SNAPSHOT) for the `dependencies` layer. +Looking at the `dependencies` closure in the example above, we can see that the first `intoLayer` will claim all project dependencies for the `application` layer. +The next `intoLayer` will claim all SNAPSHOT dependencies for the `snapshot-dependencies` layer. +The third and final `intoLayer` will claim anything left (in this case, any dependency that is not a project dependency or a SNAPSHOT) for the `dependencies` layer. The `application` closure has similar rules. First claiming `org/springframework/boot/loader/**` content for the `spring-boot-loader` layer. 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 index bab1c0f47f3..2e89d340902 100644 --- 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 @@ -17,6 +17,9 @@ bootJar { intoLayer("application") } dependencies { + intoLayer("application") { + includeProjectDependencies() + } intoLayer("snapshot-dependencies") { include "*:*:*SNAPSHOT" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java index 0d414582da0..8efa6e6a3e6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java @@ -21,9 +21,9 @@ import java.io.File; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.specs.Spec; +import org.springframework.boot.gradle.tasks.bundling.ResolvedDependencies.DependencyDescriptor; import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.Library; -import org.springframework.boot.loader.tools.LibraryCoordinates; /** * Resolver backed by a {@link LayeredSpec} that provides the destination {@link Layer} @@ -76,8 +76,10 @@ class LayerResolver { private Library asLibrary(FileCopyDetails details) { File file = details.getFile(); - LibraryCoordinates coordinates = this.resolvedDependencies.find(file); - return new Library(null, file, null, coordinates, false); + DependencyDescriptor dependency = this.resolvedDependencies.find(file); + return (dependency != null) + ? new Library(null, file, null, dependency.getCoordinates(), false, dependency.isProjectDependency()) + : new Library(file, null); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java index 1aa9d1753c3..e5bfe5f1a72 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java @@ -227,16 +227,19 @@ public class LayeredSpec { private final List intoLayers; + private final Function specFactory; + boolean isEmpty() { return this.intoLayers.isEmpty(); } - IntoLayersSpec(IntoLayerSpec... spec) { + IntoLayersSpec(Function specFactory, IntoLayerSpec... spec) { this.intoLayers = new ArrayList<>(Arrays.asList(spec)); + this.specFactory = specFactory; } public void intoLayer(String layer) { - this.intoLayers.add(new IntoLayerSpec(layer)); + this.intoLayers.add(this.specFactory.apply(layer)); } public void intoLayer(String layer, Closure closure) { @@ -244,14 +247,13 @@ public class LayeredSpec { } public void intoLayer(String layer, Action action) { - IntoLayerSpec spec = new IntoLayerSpec(layer); + IntoLayerSpec spec = this.specFactory.apply(layer); action.execute(spec); this.intoLayers.add(spec); } - List> asSelectors(Function> filterFactory) { - return this.intoLayers.stream().map((content) -> content.asSelector(filterFactory)) - .collect(Collectors.toList()); + List> asSelectors(Function> selectorFactory) { + return this.intoLayers.stream().map(selectorFactory).collect(Collectors.toList()); } } @@ -279,8 +281,8 @@ public class LayeredSpec { /** * Adds patterns that control the content that is included in the layer. If no * includes are specified then all content is included. If includes are specified - * then content must match an inclusion pattern and not match any exclusion - * patterns to be included. + * then content must match an inclusion and not match any exclusions to be + * included. * @param patterns the patterns to be included */ public void include(String... patterns) { @@ -291,7 +293,7 @@ public class LayeredSpec { * Adds patterns that control the content that is excluded from the layer. If no * excludes a specified no content is excluded. If exclusions are specified then * any content that matches an exclusion will be excluded irrespective of whether - * it matches an include pattern. + * it matches an include. * @param patterns the patterns to be excluded */ public void exclude(String... patterns) { @@ -303,6 +305,76 @@ public class LayeredSpec { return new IncludeExcludeContentSelector<>(layer, this.includes, this.excludes, filterFactory); } + String getIntoLayer() { + return this.intoLayer; + } + + List getIncludes() { + return this.includes; + } + + List getExcludes() { + return this.excludes; + } + + } + + /** + * Spec that controls the dependencies that should be part of a particular layer. + * + * @since 2.4.0 + */ + public static class DependenciesIntoLayerSpec extends IntoLayerSpec { + + private boolean includeProjectDependencies; + + private boolean excludeProjectDependencies; + + /** + * Creates a new {@code IntoLayerSpec} that will control the content of the given + * layer. + * @param intoLayer the layer + */ + public DependenciesIntoLayerSpec(String intoLayer) { + super(intoLayer); + } + + /** + * Configures the layer to include project dependencies. If no includes are + * specified then all content is included. If includes are specified then content + * must match an inclusion and not match any exclusions to be included. + */ + public void includeProjectDependencies() { + this.includeProjectDependencies = true; + } + + /** + * Configures the layer to exclude project dependencies. If no excludes a + * specified no content is excluded. If exclusions are specified then any content + * that matches an exclusion will be excluded irrespective of whether it matches + * an include. + */ + public void excludeProjectDependencies() { + this.excludeProjectDependencies = true; + } + + ContentSelector asLibrarySelector(Function> filterFactory) { + Layer layer = new Layer(getIntoLayer()); + List> includeFilters = getIncludes().stream().map(filterFactory) + .collect(Collectors.toList()); + if (this.includeProjectDependencies) { + includeFilters = new ArrayList<>(includeFilters); + includeFilters.add(Library::isLocal); + } + List> excludeFilters = getExcludes().stream().map(filterFactory) + .collect(Collectors.toList()); + if (this.excludeProjectDependencies) { + excludeFilters = new ArrayList<>(includeFilters); + excludeFilters.add(Library::isLocal); + } + return new IncludeExcludeContentSelector<>(layer, includeFilters, excludeFilters); + } + } /** @@ -317,11 +389,20 @@ public class LayeredSpec { * included */ public ApplicationSpec(IntoLayerSpec... contents) { - super(contents); + super(new IntoLayerSpecFactory(), contents); } List> asSelectors() { - return asSelectors(ApplicationContentFilter::new); + return asSelectors((spec) -> spec.asSelector(ApplicationContentFilter::new)); + } + + private static final class IntoLayerSpecFactory implements Function, Serializable { + + @Override + public IntoLayerSpec apply(String layer) { + return new IntoLayerSpec(layer); + } + } } @@ -329,18 +410,28 @@ public class LayeredSpec { /** * An {@link IntoLayersSpec} that controls the layers to which dependencies belong. */ - public static class DependenciesSpec extends IntoLayersSpec { + public static class DependenciesSpec extends IntoLayersSpec implements Serializable { /** * Creates a new {@code DependenciesSpec} with the given {@code contents}. * @param contents specs for the layers in which dependencies should be included */ - public DependenciesSpec(IntoLayerSpec... contents) { - super(contents); + public DependenciesSpec(DependenciesIntoLayerSpec... contents) { + super(new IntoLayerSpecFactory(), contents); } List> asSelectors() { - return asSelectors(LibraryContentFilter::new); + return asSelectors( + (spec) -> ((DependenciesIntoLayerSpec) spec).asLibrarySelector(LibraryContentFilter::new)); + } + + private static final class IntoLayerSpecFactory implements Function, Serializable { + + @Override + public IntoLayerSpec apply(String layer) { + return new DependenciesIntoLayerSpec(layer); + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/ResolvedDependencies.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/ResolvedDependencies.java index 45ee221d2f7..62548749b92 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/ResolvedDependencies.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/ResolvedDependencies.java @@ -19,9 +19,12 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.File; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedConfiguration; @@ -42,15 +45,19 @@ class ResolvedDependencies { private final Map configurationDependencies = new LinkedHashMap<>(); void processConfiguration(Configuration configuration) { + Set projectDependencyIds = configuration.getAllDependencies().withType(ProjectDependency.class).stream() + .map((projectDependency) -> projectDependency.getGroup() + ":" + projectDependency.getName() + ":" + + projectDependency.getVersion()) + .collect(Collectors.toSet()); this.configurationDependencies.put(configuration, - new ResolvedConfigurationDependencies(configuration.getResolvedConfiguration())); + new ResolvedConfigurationDependencies(projectDependencyIds, configuration.getResolvedConfiguration())); } - LibraryCoordinates find(File file) { + DependencyDescriptor find(File file) { for (ResolvedConfigurationDependencies dependencies : this.configurationDependencies.values()) { - LibraryCoordinates coordinates = dependencies.find(file); - if (coordinates != null) { - return coordinates; + DependencyDescriptor dependency = dependencies.find(file); + if (dependency != null) { + return dependency; } } return null; @@ -61,19 +68,23 @@ class ResolvedDependencies { */ private static class ResolvedConfigurationDependencies { - private final Map artifactCoordinates = new LinkedHashMap<>(); + private final Map dependencies = new LinkedHashMap<>(); - ResolvedConfigurationDependencies(ResolvedConfiguration resolvedConfiguration) { + ResolvedConfigurationDependencies(Set projectDependencyIds, + ResolvedConfiguration resolvedConfiguration) { if (!resolvedConfiguration.hasError()) { for (ResolvedArtifact resolvedArtifact : resolvedConfiguration.getResolvedArtifacts()) { - this.artifactCoordinates.put(resolvedArtifact.getFile(), - new ModuleVersionIdentifierLibraryCoordinates(resolvedArtifact.getModuleVersion().getId())); + ModuleVersionIdentifier id = resolvedArtifact.getModuleVersion().getId(); + boolean projectDependency = projectDependencyIds + .contains(id.getGroup() + ":" + id.getName() + ":" + id.getVersion()); + this.dependencies.put(resolvedArtifact.getFile(), new DependencyDescriptor( + new ModuleVersionIdentifierLibraryCoordinates(id), projectDependency)); } } } - LibraryCoordinates find(File file) { - return this.artifactCoordinates.get(file); + DependencyDescriptor find(File file) { + return this.dependencies.get(file); } } @@ -111,4 +122,28 @@ class ResolvedDependencies { } + /** + * Describes a dependency in a {@link ResolvedConfiguration}. + */ + static final class DependencyDescriptor { + + private final LibraryCoordinates coordinates; + + private final boolean projectDependency; + + private DependencyDescriptor(LibraryCoordinates coordinates, boolean projectDependency) { + this.coordinates = coordinates; + this.projectDependency = projectDependency; + } + + LibraryCoordinates getCoordinates() { + return this.coordinates; + } + + boolean isProjectDependency() { + return this.projectDependency; + } + + } + } 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 6c3a8c273c4..476af25b15f 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 @@ -140,6 +140,50 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { assertExtractedLayers(layerNames, indexedLayers); } + @TestTemplate + void multiModuleImplicitLayers() throws IOException { + writeSettingsGradle(); + writeMainClass(); + writeResource(); + assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/alpha-1.2.3.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/bravo-1.2.3.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", + "application"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add("BOOT-INF/lib/commons-lang3-3.9.jar"); + expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar"); + Set expectedSnapshotDependencies = new TreeSet<>(); + expectedSnapshotDependencies.add("BOOT-INF/lib/library-1.0-SNAPSHOT.jar"); + (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("application")).containsExactly("BOOT-INF/classes/", "BOOT-INF/classpath.idx", + "BOOT-INF/layers.idx", "BOOT-INF/lib/alpha-1.2.3.jar", "BOOT-INF/lib/bravo-1.2.3.jar", "META-INF/"); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + @TestTemplate void customLayers() throws IOException { writeMainClass(); 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 2a95643d87e..9496b5b7801 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 @@ -29,8 +29,11 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import org.gradle.api.Action; +import org.gradle.api.DomainObjectSet; import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedConfiguration; @@ -41,6 +44,7 @@ import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.junit.jupiter.api.Test; import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -56,6 +60,7 @@ import static org.mockito.Mockito.mock; * @author Scott Frederick * @author Paddy Drury */ +@ClassPathExclusions("kotlin-daemon-client-*") class BootJarTests extends AbstractBootArchiveTests { BootJarTests() { @@ -307,6 +312,10 @@ class BootJarTests extends AbstractBootArchiveTests { given(configuration.getResolvedConfiguration()).willReturn(resolvedConfiguration); ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class); given(configuration.getIncoming()).willReturn(resolvableDependencies); + DependencySet dependencies = mock(DependencySet.class); + DomainObjectSet projectDependencies = mock(DomainObjectSet.class); + given(dependencies.withType(ProjectDependency.class)).willReturn(projectDependencies); + given(configuration.getAllDependencies()).willReturn(dependencies); willAnswer((invocation) -> { invocation.getArgument(0, Action.class).execute(resolvableDependencies); return null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle index ef06bc2edb6..56213e23699 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle @@ -20,7 +20,7 @@ bootJar { } dependencies { intoLayer("subproject-dependencies") { - include "org.example.projects:*" + includeProjectDependencies() } intoLayer("snapshot-dependencies") { include "*:*:*SNAPSHOT" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle new file mode 100644 index 00000000000..bd78e7ce97d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +subprojects { + apply plugin: 'java' + group = 'org.example.projects' + version = '1.2.3' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation(project(':alpha')) + implementation(project(':bravo')) + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ImplicitLayerResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ImplicitLayerResolver.java index ca97b6f3bbd..84c19f10d40 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ImplicitLayerResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ImplicitLayerResolver.java @@ -36,6 +36,9 @@ class ImplicitLayerResolver extends StandardLayers { @Override public Layer getLayer(Library library) { + if (library.isLocal()) { + return APPLICATION; + } if (library.getName().contains("SNAPSHOT.")) { return SNAPSHOT_DEPENDENCIES; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java index e8398fd9cf3..9c82bae24d9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java @@ -41,6 +41,8 @@ public class Library { private final boolean unpackRequired; + private final boolean local; + /** * Create a new {@link Library}. * @param file the source file @@ -82,11 +84,29 @@ public class Library { * @param unpackRequired if the library needs to be unpacked before it can be used */ public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired) { + this(name, file, scope, coordinates, unpackRequired, false); + } + + /** + * Create a new {@link Library}. + * @param name the name of the library as it should be written or {@code null} to use + * the file name + * @param file the source file + * @param scope the scope of the library + * @param coordinates the library coordinates or {@code null} + * @param unpackRequired if the library needs to be unpacked before it can be used + * @param local if the library is local (part of the same build) to the application + * that is being packaged + * @since 2.4.0 + */ + public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired, + boolean local) { this.name = (name != null) ? name : file.getName(); this.file = file; this.scope = scope; this.coordinates = coordinates; this.unpackRequired = unpackRequired; + this.local = local; } /** @@ -143,4 +163,13 @@ public class Library { return this.file.lastModified(); } + /** + * Return if the library is local (part of the same build) to the application that is + * being packaged. + * @return if the library is local + */ + public boolean isLocal() { + return this.local; + } + }