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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user