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:
Scott Frederick
2020-03-10 16:09:44 -07:00
parent c4a55a5fb4
commit f2dadf5a87
23 changed files with 643 additions and 59 deletions

View File

@@ -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);
}
}
}

View File

@@ -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 {