Add support for creating layered war files with Gradle

See gh-22195
pull/25075/head
Madhura Bhave 4 years ago
parent 7f8ea33359
commit 1245e5eec9

@ -22,6 +22,7 @@ import java.util.function.Function;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ResolvableDependencies;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileCopyDetails;
@ -30,6 +31,8 @@ import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.provider.Property;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.bundling.War;
@ -50,12 +53,18 @@ public class BootWar extends War implements BootArchive {
private static final String LIB_DIRECTORY = "WEB-INF/lib/";
private static final String LAYERS_INDEX = "WEB-INF/layers.idx";
private final BootArchiveSupport support;
private final Property<String> mainClass;
private FileCollection providedClasspath;
private final ResolvedDependencies resolvedDependencies = new ResolvedDependencies();
private LayeredSpec layered = new LayeredSpec();
/**
* Creates a new {@code BootWar} task.
*/
@ -65,6 +74,14 @@ public class BootWar extends War implements BootArchive {
getWebInf().into("lib-provided", fromCallTo(this::getProvidedLibFiles));
this.support.moveModuleInfoToRoot(getRootSpec());
getRootSpec().eachFile(this.support::excludeNonZipLibraryFiles);
getProject().getConfigurations().all((configuration) -> {
ResolvableDependencies incoming = configuration.getIncoming();
incoming.afterResolve((resolvableDependencies) -> {
if (resolvableDependencies == incoming) {
this.resolvedDependencies.processConfiguration(configuration);
}
});
});
}
private Object getProvidedLibFiles() {
@ -74,12 +91,21 @@ public class BootWar extends War implements BootArchive {
@Override
public void copy() {
this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, null,
null);
(isLayeredDisabled()) ? null : LAYERS_INDEX);
super.copy();
}
private boolean isLayeredDisabled() {
return this.layered != null && !this.layered.isEnabled();
}
@Override
protected CopyAction createCopyAction() {
if (!isLayeredDisabled()) {
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
String layerToolsLocation = this.layered.isIncludeLayerTools() ? LIB_DIRECTORY : null;
return this.support.createCopyAction(this, layerResolver, layerToolsLocation);
}
return this.support.createCopyAction(this);
}
@ -181,6 +207,25 @@ public class BootWar extends War implements BootArchive {
return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
}
/**
* Returns the spec that describes the layers in a layered jar.
* @return the spec for the layers
* @since 2.5.0
*/
@Nested
public LayeredSpec getLayered() {
return this.layered;
}
/**
* Configures the war's layering using the given {@code action}.
* @param action the action to apply
* @since 2.5.0
*/
public void layered(Action<LayeredSpec> action) {
action.execute(this.layered);
}
/**
* Return if the {@link FileCopyDetails} are for a library. By default any file in
* {@code WEB-INF/lib} or {@code WEB-INF/lib-provided} is considered to be a library.
@ -201,6 +246,11 @@ public class BootWar extends War implements BootArchive {
return launchScript;
}
@Internal
ResolvedDependencies getResolvedDependencies() {
return this.resolvedDependencies;
}
/**
* Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read.
* @param <T> the result type

@ -41,7 +41,7 @@ import org.springframework.boot.loader.tools.layer.LibraryContentFilter;
import org.springframework.util.Assert;
/**
* Encapsulates the configuration for a layered jar.
* Encapsulates the configuration for a layered archive.
*
* @author Madhura Bhave
* @author Scott Frederick
@ -65,7 +65,7 @@ public class LayeredSpec {
/**
* Returns whether the layer tools should be included as a dependency in the layered
* jar.
* archive.
* @return whether the layer tools should be included
*/
@Input
@ -74,7 +74,8 @@ public class LayeredSpec {
}
/**
* Sets whether the layer tools should be included as a dependency in the layered jar.
* Sets whether the layer tools should be included as a dependency in the layered
* archive.
* @param includeLayerTools {@code true} if the layer tools should be included,
* otherwise {@code false}
*/
@ -83,7 +84,7 @@ public class LayeredSpec {
}
/**
* Returns whether the layers.idx should be included in the jar.
* Returns whether the layers.idx should be included in the archive.
* @return whether the layers.idx should be included
*/
@Input
@ -92,8 +93,8 @@ public class LayeredSpec {
}
/**
* Sets whether the layers.idx should be included in the jar.
* @param enabled {@code true} layers.idx should be included in the jar, otherwise
* Sets whether the layers.idx should be included in the archive.
* @param enabled {@code true} layers.idx should be included in the archive, otherwise
* {@code false}
*/
public void setEnabled(boolean enabled) {
@ -171,7 +172,8 @@ public class LayeredSpec {
}
/**
* Returns the order of the layers in the jar from least to most frequently changing.
* Returns the order of the layers in the archive from least to most frequently
* changing.
* @return the layer order
*/
@Input
@ -180,7 +182,7 @@ public class LayeredSpec {
}
/**
* Sets to order of the layers in the jar from least to most frequently changing.
* Sets the order of the layers in the archive from least to most frequently changing.
* @param layerOrder the layer order
*/
public void setLayerOrder(String... layerOrder) {
@ -188,7 +190,7 @@ public class LayeredSpec {
}
/**
* Sets to order of the layers in the jar from least to most frequently changing.
* Sets the order of the layers in the archive from least to most frequently changing.
* @param layerOrder the layer order
*/
public void setLayerOrder(List<String> layerOrder) {

@ -16,18 +16,36 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
import org.gradle.testkit.runner.TaskOutcome;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
@ -35,14 +53,17 @@ import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link BootJar}.
* Integration tests for {@link BootJar} and {@link BootWar}.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
abstract class AbstractBootArchiveIntegrationTests {
@ -52,12 +73,16 @@ abstract class AbstractBootArchiveIntegrationTests {
private final String classesPath;
private final String indexPath;
GradleBuild gradleBuild;
protected AbstractBootArchiveIntegrationTests(String taskName, String libPath, String classesPath) {
protected AbstractBootArchiveIntegrationTests(String taskName, String libPath, String classesPath,
String indexPath) {
this.taskName = taskName;
this.libPath = libPath;
this.classesPath = classesPath;
this.indexPath = indexPath;
}
@TestTemplate
@ -204,6 +229,235 @@ abstract class AbstractBootArchiveIntegrationTests {
.isEqualTo(TaskOutcome.UP_TO_DATE);
}
@TestTemplate
void upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
assertThat(this.gradleBuild.scriptProperty("layered", "").build("" + this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.scriptProperty("layered", "layered {}").build("" + this.taskName)
.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE);
}
@TestTemplate
void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = false").build(this.taskName)
.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = true").build(this.taskName)
.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
assertThat(this.gradleBuild.scriptProperty("layerTools", "").build(this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.scriptProperty("layerTools", "includeLayerTools = false").build(this.taskName)
.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void layersWithCustomSourceSet() throws IOException {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void implicitLayers() throws IOException {
writeMainClass();
writeResource();
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + 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(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull();
indexedLayers = readLayerIndex(jarFile);
}
List<String> layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies",
"application");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add(this.libPath + "commons-lang3-3.9.jar");
expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar");
expectedDependencies.add(this.libPath + "jul-to-slf4j-1.7.28.jar");
expectedDependencies.add(this.libPath + "log4j-api-2.12.1.jar");
expectedDependencies.add(this.libPath + "log4j-to-slf4j-2.12.1.jar");
expectedDependencies.add(this.libPath + "logback-classic-1.2.3.jar");
expectedDependencies.add(this.libPath + "logback-core-1.2.3.jar");
expectedDependencies.add(this.libPath + "slf4j-api-1.7.28.jar");
expectedDependencies.add(this.libPath + "spring-boot-starter-logging-2.2.0.RELEASE.jar");
Set<String> expectedSnapshotDependencies = new TreeSet<>();
expectedSnapshotDependencies.add(this.libPath + "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(getExpectedApplicationLayerContents(this.classesPath));
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);
}
abstract String[] getExpectedApplicationLayerContents(String... additionalFiles);
@TestTemplate
void multiModuleImplicitLayers() throws IOException {
writeSettingsGradle();
writeMainClass();
writeResource();
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + 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(this.libPath + "alpha-1.2.3.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull();
indexedLayers = readLayerIndex(jarFile);
}
List<String> layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies",
"application");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add(this.libPath + "commons-lang3-3.9.jar");
expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar");
Set<String> expectedSnapshotDependencies = new TreeSet<>();
expectedSnapshotDependencies.add(this.libPath + "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(getExpectedApplicationLayerContents(
this.classesPath, this.libPath + "alpha-1.2.3.jar", this.libPath + "bravo-1.2.3.jar"));
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();
writeResource();
BuildResult build = this.gradleBuild.build(this.taskName);
assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + 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(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull();
assertThat(jarFile.getEntry(this.indexPath + "layers.idx")).isNotNull();
indexedLayers = readLayerIndex(jarFile);
}
List<String> layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies",
"static", "app");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar");
List<String> expectedSnapshotDependencies = new ArrayList<>();
expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar");
(layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar);
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
assertThat(indexedLayers.get("commons-dependencies")).containsExactly(this.libPath + "commons-lang3-3.9.jar");
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
assertThat(indexedLayers.get("static")).containsExactly(this.classesPath + "static/");
List<String> appLayer = new ArrayList<>(indexedLayers.get("app"));
String[] appLayerContents = getExpectedApplicationLayerContents(this.classesPath + "example/");
assertThat(appLayer).containsSubsequence(appLayerContents);
appLayer.removeAll(Arrays.asList(appLayerContents));
assertThat(appLayer).containsExactly("org/");
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 multiModuleCustomLayers() throws IOException {
writeSettingsGradle();
writeMainClass();
writeResource();
BuildResult build = this.gradleBuild.build(this.taskName);
assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + 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(this.libPath + "alpha-1.2.3.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull();
assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull();
assertThat(jarFile.getEntry(this.indexPath + "layers.idx")).isNotNull();
indexedLayers = readLayerIndex(jarFile);
}
List<String> layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies",
"subproject-dependencies", "static", "app");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> expectedSubprojectDependencies = new TreeSet<>();
expectedSubprojectDependencies.add(this.libPath + "alpha-1.2.3.jar");
expectedSubprojectDependencies.add(this.libPath + "bravo-1.2.3.jar");
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar");
List<String> expectedSnapshotDependencies = new ArrayList<>();
expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar");
(layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar);
assertThat(indexedLayers.get("subproject-dependencies"))
.containsExactlyElementsOf(expectedSubprojectDependencies);
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
assertThat(indexedLayers.get("commons-dependencies")).containsExactly(this.libPath + "commons-lang3-3.9.jar");
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
assertThat(indexedLayers.get("static")).containsExactly(this.classesPath + "static/");
List<String> appLayer = new ArrayList<>(indexedLayers.get("app"));
String[] appLayerContents = getExpectedApplicationLayerContents(this.classesPath + "example/");
assertThat(appLayer).containsSubsequence(appLayerContents);
appLayer.removeAll(Arrays.asList(appLayerContents));
assertThat(appLayer).containsExactly("org/");
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);
}
private void copyMainClassApplication() throws IOException {
copyApplication("main");
}
@ -235,4 +489,103 @@ abstract class AbstractBootArchiveIntegrationTests {
new JarOutputStream(new FileOutputStream(location), manifest).close();
}
private void writeSettingsGradle() {
try (PrintWriter writer = new PrintWriter(
new FileWriter(new File(this.gradleBuild.getProjectDir(), "settings.gradle")))) {
writer.println("include 'alpha', 'bravo'");
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
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);
}
}
private Map<String, List<String>> readLayerIndex(JarFile jarFile) throws IOException {
Map<String, List<String>> index = new LinkedHashMap<>();
ZipEntry indexEntry = jarFile.getEntry(this.indexPath + "layers.idx");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) {
String line = reader.readLine();
String layer = null;
while (line != null) {
if (line.startsWith("- ")) {
layer = line.substring(3, line.length() - 2);
}
else if (line.startsWith(" - ")) {
index.computeIfAbsent(layer, (key) -> new ArrayList<>()).add(line.substring(5, line.length() - 1));
}
line = reader.readLine();
}
return index;
}
}
private Map<String, List<String>> readExtractedLayers(File root, List<String> layerNames) throws IOException {
Map<String, List<String>> extractedLayers = new LinkedHashMap<>();
for (String layerName : layerNames) {
File layer = new File(root, layerName);
assertThat(layer).isDirectory();
extractedLayers.put(layerName,
Files.walk(layer.toPath()).filter((path) -> path.toFile().isFile()).map(layer.toPath()::relativize)
.map(Path::toString).map(StringUtils::cleanPath).collect(Collectors.toList()));
}
return extractedLayers;
}
private void assertExtractedLayers(List<String> layerNames, Map<String, List<String>> indexedLayers)
throws IOException {
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
extractedLayers.forEach((name, contents) -> {
List<String> index = indexedLayers.get(name);
List<String> unexpected = new ArrayList<>();
for (String file : contents) {
if (!isInIndex(index, file)) {
unexpected.add(name);
}
}
assertThat(unexpected).isEmpty();
});
}
private boolean isInIndex(List<String> index, String file) {
for (String candidate : index) {
if (file.equals(candidate) || candidate.endsWith("/") && file.startsWith(candidate)) {
return true;
}
}
return false;
}
}

@ -16,10 +16,12 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
@ -27,19 +29,35 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.gradle.api.Action;
import org.gradle.api.DomainObjectSet;
import org.gradle.api.Project;
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;
import org.gradle.api.artifacts.ResolvedModuleVersion;
import org.gradle.api.artifacts.component.ComponentArtifactIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.internal.file.archive.ZipCopyAction;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar;
@ -49,8 +67,13 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.JarModeLibrary;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
* Abstract base class for testing {@link BootArchive} implementations.
@ -72,15 +95,19 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
private final String classesPath;
private final String indexPath;
private Project project;
private T task;
protected AbstractBootArchiveTests(Class<T> taskClass, String launcherClass, String libPath, String classesPath) {
protected AbstractBootArchiveTests(Class<T> taskClass, String launcherClass, String libPath, String classesPath,
String indexPath) {
this.taskClass = taskClass;
this.launcherClass = launcherClass;
this.libPath = libPath;
this.classesPath = classesPath;
this.indexPath = indexPath;
}
@BeforeEach
@ -407,6 +434,145 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
this.libPath + "third-library.jar");
}
@Test
void archiveShouldBeLayeredByDefault() throws IOException {
addContent();
executeTask();
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes"))
.isEqualTo(this.classesPath);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
.isEqualTo(this.indexPath + "layers.idx");
assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName());
}
}
@Test
void jarWhenLayersDisabledShouldNotContainLayersIndex() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar((configuration) -> configuration.setEnabled(false)));
assertThat(entryNames).doesNotContain(this.indexPath + "layers.idx");
}
@Test
void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes"))
.isEqualTo(this.classesPath);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
.isEqualTo(this.indexPath + "layers.idx");
}
}
@Test
void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
List<String> entryNames = getEntryNames(jarFile);
assertThat(entryNames).contains(this.libPath + "first-library.jar", this.libPath + "second-library.jar",
this.libPath + "third-library-SNAPSHOT.jar", this.libPath + "first-project-library.jar",
this.libPath + "second-project-library-SNAPSHOT.jar",
this.classesPath + "com/example/Application.class", this.classesPath + "application.properties",
this.classesPath + "static/test.css");
List<String> index = entryLines(jarFile, this.indexPath + "layers.idx");
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
"snapshot-dependencies", "application");
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"dependencies\":");
expected.add(" - \"" + this.libPath + "first-library.jar\"");
expected.add(" - \"" + this.libPath + "first-project-library.jar\"");
expected.add(" - \"" + this.libPath + "second-library.jar\"");
if (!layerToolsJar.contains("SNAPSHOT")) {
expected.add(" - \"" + layerToolsJar + "\"");
}
expected.add("- \"spring-boot-loader\":");
expected.add(" - \"org/\"");
expected.add("- \"snapshot-dependencies\":");
expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\"");
if (layerToolsJar.contains("SNAPSHOT")) {
expected.add(" - \"" + layerToolsJar + "\"");
}
expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\"");
expected.add("- \"application\":");
Set<String> applicationContents = new TreeSet<>();
applicationContents.add(" - \"" + this.classesPath + "\"");
if (archiveHasClasspathIndex()) {
applicationContents.add(" - \"" + this.indexPath + "classpath.idx\"");
}
applicationContents.add(" - \"" + this.indexPath + "layers.idx\"");
applicationContents.add(" - \"META-INF/\"");
expected.addAll(applicationContents);
assertThat(index).containsExactlyElementsOf(expected);
}
}
@Test
void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() throws IOException {
File jar = createLayeredJar((layered) -> {
layered.application((application) -> {
application.intoLayer("resources", (spec) -> spec.include("static/**"));
application.intoLayer("application");
});
layered.dependencies((dependencies) -> {
dependencies.intoLayer("my-snapshot-deps", (spec) -> spec.include("com.example:*:*.SNAPSHOT"));
dependencies.intoLayer("my-internal-deps", (spec) -> spec.include("com.example:*:*"));
dependencies.intoLayer("my-deps");
});
layered.setLayerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application");
});
try (JarFile jarFile = new JarFile(jar)) {
List<String> entryNames = getEntryNames(jar);
assertThat(entryNames).contains(this.libPath + "first-library.jar", this.libPath + "second-library.jar",
this.libPath + "third-library-SNAPSHOT.jar", this.libPath + "first-project-library.jar",
this.libPath + "second-project-library-SNAPSHOT.jar",
this.classesPath + "com/example/Application.class", this.classesPath + "application.properties",
this.classesPath + "static/test.css");
List<String> index = entryLines(jarFile, this.indexPath + "layers.idx");
assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps",
"resources", "application");
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"my-deps\":");
expected.add(" - \"" + layerToolsJar + "\"");
expected.add("- \"my-internal-deps\":");
expected.add(" - \"" + this.libPath + "first-library.jar\"");
expected.add(" - \"" + this.libPath + "first-project-library.jar\"");
expected.add(" - \"" + this.libPath + "second-library.jar\"");
expected.add("- \"my-snapshot-deps\":");
expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\"");
expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\"");
expected.add("- \"resources\":");
expected.add(" - \"" + this.classesPath + "static/\"");
expected.add("- \"application\":");
Set<String> applicationContents = new TreeSet<>();
applicationContents.add(" - \"" + this.classesPath + "application.properties\"");
applicationContents.add(" - \"" + this.classesPath + "com/\"");
if (archiveHasClasspathIndex()) {
applicationContents.add(" - \"" + this.indexPath + "classpath.idx\"");
}
applicationContents.add(" - \"" + this.indexPath + "layers.idx\"");
applicationContents.add(" - \"META-INF/\"");
applicationContents.add(" - \"org/\"");
expected.addAll(applicationContents);
assertThat(index).containsExactlyElementsOf(expected);
}
}
@Test
void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName());
}
@Test
void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(
createLayeredJar((configuration) -> configuration.setIncludeLayerTools(false)));
assertThat(entryNames)
.doesNotContain(this.indexPath + "layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
}
protected File jarFile(String name) throws IOException {
File file = newFile(name);
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) {
@ -453,4 +619,118 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
return file;
}
File createLayeredJar() throws IOException {
return createLayeredJar((spec) -> {
});
}
File createLayeredJar(Action<LayeredSpec> action) throws IOException {
applyLayered(action);
addContent();
executeTask();
return getTask().getArchiveFile().get().getAsFile();
}
abstract void applyLayered(Action<LayeredSpec> action);
boolean archiveHasClasspathIndex() {
return true;
}
@SuppressWarnings("unchecked")
void addContent() throws IOException {
this.task.getMainClass().set("com.example.Main");
File classesJavaMain = new File(this.temp, "classes/java/main");
File applicationClass = new File(classesJavaMain, "com/example/Application.class");
applicationClass.getParentFile().mkdirs();
applicationClass.createNewFile();
File resourcesMain = new File(this.temp, "resources/main");
File applicationProperties = new File(resourcesMain, "application.properties");
applicationProperties.getParentFile().mkdirs();
applicationProperties.createNewFile();
File staticResources = new File(resourcesMain, "static");
staticResources.mkdir();
File css = new File(staticResources, "test.css");
css.createNewFile();
this.task.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
jarFile("third-library-SNAPSHOT.jar"), jarFile("first-project-library.jar"),
jarFile("second-project-library-SNAPSHOT.jar"));
Set<ResolvedArtifact> artifacts = new LinkedHashSet<>();
artifacts.add(mockLibraryArtifact("first-library.jar", "com.example", "first-library", "1.0.0"));
artifacts.add(mockLibraryArtifact("second-library.jar", "com.example", "second-library", "1.0.0"));
artifacts.add(
mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example", "third-library", "1.0.0.SNAPSHOT"));
artifacts
.add(mockProjectArtifact("first-project-library.jar", "com.example", "first-project-library", "1.0.0"));
artifacts.add(mockProjectArtifact("second-project-library-SNAPSHOT.jar", "com.example",
"second-project-library", "1.0.0.SNAPSHOT"));
ResolvedConfiguration resolvedConfiguration = mock(ResolvedConfiguration.class);
given(resolvedConfiguration.getResolvedArtifacts()).willReturn(artifacts);
Configuration configuration = mock(Configuration.class);
given(configuration.getResolvedConfiguration()).willReturn(resolvedConfiguration);
ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class);
given(configuration.getIncoming()).willReturn(resolvableDependencies);
DependencySet dependencies = mock(DependencySet.class);
DomainObjectSet<ProjectDependency> 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;
}).given(resolvableDependencies).afterResolve(any(Action.class));
given(configuration.getIncoming()).willReturn(resolvableDependencies);
populateResolvedDependencies(configuration);
}
abstract void populateResolvedDependencies(Configuration configuration);
private ResolvedArtifact mockLibraryArtifact(String fileName, String group, String module, String version) {
ModuleComponentIdentifier moduleComponentIdentifier = mock(ModuleComponentIdentifier.class);
ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class);
given(libraryArtifactId.getComponentIdentifier()).willReturn(moduleComponentIdentifier);
ResolvedArtifact libraryArtifact = mockArtifact(fileName, group, module, version);
given(libraryArtifact.getId()).willReturn(libraryArtifactId);
return libraryArtifact;
}
private ResolvedArtifact mockProjectArtifact(String fileName, String group, String module, String version) {
ProjectComponentIdentifier projectComponentIdentifier = mock(ProjectComponentIdentifier.class);
ComponentArtifactIdentifier projectArtifactId = mock(ComponentArtifactIdentifier.class);
given(projectArtifactId.getComponentIdentifier()).willReturn(projectComponentIdentifier);
ResolvedArtifact projectArtifact = mockArtifact(fileName, group, module, version);
given(projectArtifact.getId()).willReturn(projectArtifactId);
return projectArtifact;
}
private ResolvedArtifact mockArtifact(String fileName, String group, String module, String version) {
ModuleVersionIdentifier moduleVersionIdentifier = mock(ModuleVersionIdentifier.class);
given(moduleVersionIdentifier.getGroup()).willReturn(group);
given(moduleVersionIdentifier.getName()).willReturn(module);
given(moduleVersionIdentifier.getVersion()).willReturn(version);
ResolvedModuleVersion moduleVersion = mock(ResolvedModuleVersion.class);
given(moduleVersion.getId()).willReturn(moduleVersionIdentifier);
ResolvedArtifact libraryArtifact = mock(ResolvedArtifact.class);
File file = new File(this.temp, fileName).getAbsoluteFile();
given(libraryArtifact.getFile()).willReturn(file);
given(libraryArtifact.getModuleVersion()).willReturn(moduleVersion);
return libraryArtifact;
}
List<String> entryLines(JarFile jarFile, String entryName) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(jarFile.getInputStream(jarFile.getEntry(entryName))))) {
return reader.lines().collect(Collectors.toList());
}
}
private Set<String> getLayerNames(List<String> index) {
Set<String> layerNames = new LinkedHashSet<>();
for (String line : index) {
if (line.startsWith("- ")) {
layerNames.add(line.substring(3, line.length() - 2));
}
}
return layerNames;
}
}

@ -16,36 +16,16 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
import org.gradle.testkit.runner.TaskOutcome;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -60,233 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
BootJarIntegrationTests() {
super("bootJar", "BOOT-INF/lib/", "BOOT-INF/classes/");
}
@TestTemplate
void upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
assertThat(this.gradleBuild.scriptProperty("layered", "").build("bootJar").task(":bootJar").getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
assertThat(
this.gradleBuild.scriptProperty("layered", "layered {}").build("bootJar").task(":bootJar").getOutcome())
.isEqualTo(TaskOutcome.UP_TO_DATE);
}
@TestTemplate
void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = false").build("bootJar")
.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = true").build("bootJar")
.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
assertThat(this.gradleBuild.scriptProperty("layerTools", "").build("bootJar").task(":bootJar").getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.scriptProperty("layerTools", "includeLayerTools = false").build("bootJar")
.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void layersWithCustomSourceSet() throws IOException {
assertThat(this.gradleBuild.build("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);
Map<String, List<String>> 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/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<String> layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies",
"application");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> 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<String> 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", "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 multiModuleImplicitLayers() throws IOException {
writeSettingsGradle();
writeMainClass();
writeResource();
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> 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<String> layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies",
"application");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> 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<String> 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();
writeResource();
BuildResult build = this.gradleBuild.build("bootJar");
assertThat(build.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> 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/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();
assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNotNull();
indexedLayers = readLayerIndex(jarFile);
}
List<String> layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies",
"static", "app");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar");
List<String> expectedSnapshotDependencies = new ArrayList<>();
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("commons-dependencies")).containsExactly("BOOT-INF/lib/commons-lang3-3.9.jar");
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/");
List<String> appLayer = new ArrayList<>(indexedLayers.get("app"));
Set<String> nonLoaderEntries = new TreeSet<>();
nonLoaderEntries.add("BOOT-INF/classes/example/");
nonLoaderEntries.add("BOOT-INF/classpath.idx");
nonLoaderEntries.add("BOOT-INF/layers.idx");
nonLoaderEntries.add("META-INF/");
assertThat(appLayer).containsSubsequence(nonLoaderEntries);
appLayer.removeAll(nonLoaderEntries);
assertThat(appLayer).containsExactly("org/");
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 multiModuleCustomLayers() throws IOException {
writeSettingsGradle();
writeMainClass();
writeResource();
BuildResult build = this.gradleBuild.build("bootJar");
assertThat(build.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> 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();
assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNotNull();
indexedLayers = readLayerIndex(jarFile);
}
List<String> layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies",
"subproject-dependencies", "static", "app");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
Set<String> expectedSubprojectDependencies = new TreeSet<>();
expectedSubprojectDependencies.add("BOOT-INF/lib/alpha-1.2.3.jar");
expectedSubprojectDependencies.add("BOOT-INF/lib/bravo-1.2.3.jar");
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar");
List<String> expectedSnapshotDependencies = new ArrayList<>();
expectedSnapshotDependencies.add("BOOT-INF/lib/library-1.0-SNAPSHOT.jar");
(layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar);
assertThat(indexedLayers.get("subproject-dependencies"))
.containsExactlyElementsOf(expectedSubprojectDependencies);
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
assertThat(indexedLayers.get("commons-dependencies")).containsExactly("BOOT-INF/lib/commons-lang3-3.9.jar");
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/");
List<String> appLayer = new ArrayList<>(indexedLayers.get("app"));
Set<String> nonLoaderEntries = new TreeSet<>();
nonLoaderEntries.add("BOOT-INF/classes/example/");
nonLoaderEntries.add("BOOT-INF/classpath.idx");
nonLoaderEntries.add("BOOT-INF/layers.idx");
nonLoaderEntries.add("META-INF/");
assertThat(appLayer).containsSubsequence(nonLoaderEntries);
appLayer.removeAll(nonLoaderEntries);
assertThat(appLayer).containsExactly("org/");
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);
super("bootJar", "BOOT-INF/lib/", "BOOT-INF/classes/", "BOOT-INF/");
}
@TestTemplate
@ -320,107 +74,15 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
assertThat(output).doesNotContain("5. ");
}
private void assertExtractedLayers(List<String> layerNames, Map<String, List<String>> indexedLayers)
throws IOException {
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
extractedLayers.forEach((name, contents) -> {
List<String> index = indexedLayers.get(name);
List<String> unexpected = new ArrayList<>();
for (String file : contents) {
if (!isInIndex(index, file)) {
unexpected.add(name);
}
}
assertThat(unexpected).isEmpty();
});
}
private boolean isInIndex(List<String> index, String file) {
for (String candidate : index) {
if (file.equals(candidate) || candidate.endsWith("/") && file.startsWith(candidate)) {
return true;
}
}
return false;
}
private void writeSettingsGradle() {
try (PrintWriter writer = new PrintWriter(
new FileWriter(new File(this.gradleBuild.getProjectDir(), "settings.gradle")))) {
writer.println("include 'alpha', 'bravo'");
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
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);
}
}
private Map<String, List<String>> readLayerIndex(JarFile jarFile) throws IOException {
Map<String, List<String>> index = new LinkedHashMap<>();
ZipEntry indexEntry = jarFile.getEntry("BOOT-INF/layers.idx");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) {
String line = reader.readLine();
String layer = null;
while (line != null) {
if (line.startsWith("- ")) {
layer = line.substring(3, line.length() - 2);
}
else if (line.startsWith(" - ")) {
index.computeIfAbsent(layer, (key) -> new ArrayList<>()).add(line.substring(5, line.length() - 1));
}
line = reader.readLine();
}
return index;
}
}
private Map<String, List<String>> readExtractedLayers(File root, List<String> layerNames) throws IOException {
Map<String, List<String>> extractedLayers = new LinkedHashMap<>();
for (String layerName : layerNames) {
File layer = new File(root, layerName);
assertThat(layer).isDirectory();
extractedLayers.put(layerName,
Files.walk(layer.toPath()).filter((path) -> path.toFile().isFile()).map(layer.toPath()::relativize)
.map(Path::toString).map(StringUtils::cleanPath).collect(Collectors.toList()));
}
return extractedLayers;
}
private void copyClasspathApplication() throws IOException {
copyApplication("classpath");
}
@Override
String[] getExpectedApplicationLayerContents(String... additionalFiles) {
Set<String> contents = new TreeSet<>(Arrays.asList(additionalFiles));
contents.addAll(Arrays.asList("BOOT-INF/classpath.idx", "BOOT-INF/layers.idx", "META-INF/"));
return contents.toArray(new String[0]);
}
}

@ -16,41 +16,18 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedHashSet;
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.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;
import org.gradle.api.artifacts.ResolvedModuleVersion;
import org.gradle.api.artifacts.component.ComponentArtifactIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
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;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BootJar}.
@ -64,7 +41,8 @@ import static org.mockito.Mockito.mock;
class BootJarTests extends AbstractBootArchiveTests<BootJar> {
BootJarTests() {
super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/");
super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/",
"BOOT-INF/");
}
@Test
@ -89,130 +67,6 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
}
}
@Test
void jarShouldBeLayeredByDefault() throws IOException {
addContent();
executeTask();
BootJar bootJar = getTask();
try (JarFile jarFile = new JarFile(bootJar.getArchiveFile().get().getAsFile())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes"))
.isEqualTo("BOOT-INF/classes/");
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib"))
.isEqualTo("BOOT-INF/lib/");
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classpath-Index"))
.isEqualTo("BOOT-INF/classpath.idx");
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
.isEqualTo("BOOT-INF/layers.idx");
assertThat(getEntryNames(jarFile)).contains("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName());
}
}
@Test
void jarWhenLayersDisabledShouldNotContainLayersIndex() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar((configuration) -> configuration.setEnabled(false)));
assertThat(entryNames).doesNotContain("BOOT-INF/layers.idx");
}
@Test
void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes"))
.isEqualTo("BOOT-INF/classes/");
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib"))
.isEqualTo("BOOT-INF/lib/");
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classpath-Index"))
.isEqualTo("BOOT-INF/classpath.idx");
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
.isEqualTo("BOOT-INF/layers.idx");
}
}
@Test
void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
List<String> entryNames = getEntryNames(jarFile);
assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar",
"BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/lib/first-project-library.jar",
"BOOT-INF/lib/second-project-library-SNAPSHOT.jar",
"BOOT-INF/classes/com/example/Application.class", "BOOT-INF/classes/application.properties",
"BOOT-INF/classes/static/test.css");
List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
"snapshot-dependencies", "application");
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"dependencies\":");
expected.add(" - \"BOOT-INF/lib/first-library.jar\"");
expected.add(" - \"BOOT-INF/lib/first-project-library.jar\"");
expected.add(" - \"BOOT-INF/lib/second-library.jar\"");
if (!layerToolsJar.contains("SNAPSHOT")) {
expected.add(" - \"" + layerToolsJar + "\"");
}
expected.add("- \"spring-boot-loader\":");
expected.add(" - \"org/\"");
expected.add("- \"snapshot-dependencies\":");
expected.add(" - \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\"");
if (layerToolsJar.contains("SNAPSHOT")) {
expected.add(" - \"" + layerToolsJar + "\"");
}
expected.add(" - \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"");
expected.add("- \"application\":");
expected.add(" - \"BOOT-INF/classes/\"");
expected.add(" - \"BOOT-INF/classpath.idx\"");
expected.add(" - \"BOOT-INF/layers.idx\"");
expected.add(" - \"META-INF/\"");
assertThat(index).containsExactlyElementsOf(expected);
}
}
@Test
void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() throws IOException {
File jar = createLayeredJar((layered) -> {
layered.application((application) -> {
application.intoLayer("resources", (spec) -> spec.include("static/**"));
application.intoLayer("application");
});
layered.dependencies((dependencies) -> {
dependencies.intoLayer("my-snapshot-deps", (spec) -> spec.include("com.example:*:*.SNAPSHOT"));
dependencies.intoLayer("my-internal-deps", (spec) -> spec.include("com.example:*:*"));
dependencies.intoLayer("my-deps");
});
layered.setLayerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application");
});
try (JarFile jarFile = new JarFile(jar)) {
List<String> entryNames = getEntryNames(jar);
assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar",
"BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/lib/first-project-library.jar",
"BOOT-INF/lib/second-project-library-SNAPSHOT.jar",
"BOOT-INF/classes/com/example/Application.class", "BOOT-INF/classes/application.properties",
"BOOT-INF/classes/static/test.css");
List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps",
"resources", "application");
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"my-deps\":");
expected.add(" - \"" + layerToolsJar + "\"");
expected.add("- \"my-internal-deps\":");
expected.add(" - \"BOOT-INF/lib/first-library.jar\"");
expected.add(" - \"BOOT-INF/lib/first-project-library.jar\"");
expected.add(" - \"BOOT-INF/lib/second-library.jar\"");
expected.add("- \"my-snapshot-deps\":");
expected.add(" - \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\"");
expected.add(" - \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"");
expected.add("- \"resources\":");
expected.add(" - \"BOOT-INF/classes/static/\"");
expected.add("- \"application\":");
expected.add(" - \"BOOT-INF/classes/application.properties\"");
expected.add(" - \"BOOT-INF/classes/com/\"");
expected.add(" - \"BOOT-INF/classpath.idx\"");
expected.add(" - \"BOOT-INF/layers.idx\"");
expected.add(" - \"META-INF/\"");
expected.add(" - \"org/\"");
assertThat(index).containsExactlyElementsOf(expected);
}
}
@Test
void jarsInLibAreStored() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
@ -237,19 +91,6 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
}
}
@Test
void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames).contains("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName());
}
@Test
void whenJarIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(
createLayeredJar((configuration) -> configuration.setIncludeLayerTools(false)));
assertThat(entryNames).doesNotContain("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
}
@Test
void classpathIndexPointsToBootInfLibs() throws IOException {
try (JarFile jarFile = new JarFile(createPopulatedJar())) {
@ -268,111 +109,14 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
return getTask().getArchiveFile().get().getAsFile();
}
private File createLayeredJar() throws IOException {
return createLayeredJar((spec) -> {
});
}
private File createLayeredJar(Action<LayeredSpec> action) throws IOException {
@Override
void applyLayered(Action<LayeredSpec> action) {
getTask().layered(action);
addContent();
executeTask();
return getTask().getArchiveFile().get().getAsFile();
}
@SuppressWarnings("unchecked")
private void addContent() throws IOException {
BootJar bootJar = getTask();
bootJar.getMainClass().set("com.example.Main");
File classesJavaMain = new File(this.temp, "classes/java/main");
File applicationClass = new File(classesJavaMain, "com/example/Application.class");
applicationClass.getParentFile().mkdirs();
applicationClass.createNewFile();
File resourcesMain = new File(this.temp, "resources/main");
File applicationProperties = new File(resourcesMain, "application.properties");
applicationProperties.getParentFile().mkdirs();
applicationProperties.createNewFile();
File staticResources = new File(resourcesMain, "static");
staticResources.mkdir();
File css = new File(staticResources, "test.css");
css.createNewFile();
bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
jarFile("third-library-SNAPSHOT.jar"), jarFile("first-project-library.jar"),
jarFile("second-project-library-SNAPSHOT.jar"));
Set<ResolvedArtifact> artifacts = new LinkedHashSet<>();
artifacts.add(mockLibraryArtifact("first-library.jar", "com.example", "first-library", "1.0.0"));
artifacts.add(mockLibraryArtifact("second-library.jar", "com.example", "second-library", "1.0.0"));
artifacts.add(
mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example", "third-library", "1.0.0.SNAPSHOT"));
artifacts
.add(mockProjectArtifact("first-project-library.jar", "com.example", "first-project-library", "1.0.0"));
artifacts.add(mockProjectArtifact("second-project-library-SNAPSHOT.jar", "com.example",
"second-project-library", "1.0.0.SNAPSHOT"));
ResolvedConfiguration resolvedConfiguration = mock(ResolvedConfiguration.class);
given(resolvedConfiguration.getResolvedArtifacts()).willReturn(artifacts);
Configuration configuration = mock(Configuration.class);
given(configuration.getResolvedConfiguration()).willReturn(resolvedConfiguration);
ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class);
given(configuration.getIncoming()).willReturn(resolvableDependencies);
DependencySet dependencies = mock(DependencySet.class);
DomainObjectSet<ProjectDependency> 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;
}).given(resolvableDependencies).afterResolve(any(Action.class));
given(configuration.getIncoming()).willReturn(resolvableDependencies);
bootJar.getResolvedDependencies().processConfiguration(configuration);
}
private ResolvedArtifact mockLibraryArtifact(String fileName, String group, String module, String version) {
ModuleComponentIdentifier moduleComponentIdentifier = mock(ModuleComponentIdentifier.class);
ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class);
given(libraryArtifactId.getComponentIdentifier()).willReturn(moduleComponentIdentifier);
ResolvedArtifact libraryArtifact = mockArtifact(fileName, group, module, version);
given(libraryArtifact.getId()).willReturn(libraryArtifactId);
return libraryArtifact;
}
private ResolvedArtifact mockProjectArtifact(String fileName, String group, String module, String version) {
ProjectComponentIdentifier projectComponentIdentifier = mock(ProjectComponentIdentifier.class);
ComponentArtifactIdentifier projectArtifactId = mock(ComponentArtifactIdentifier.class);
given(projectArtifactId.getComponentIdentifier()).willReturn(projectComponentIdentifier);
ResolvedArtifact projectArtifact = mockArtifact(fileName, group, module, version);
given(projectArtifact.getId()).willReturn(projectArtifactId);
return projectArtifact;
}
private ResolvedArtifact mockArtifact(String fileName, String group, String module, String version) {
ModuleVersionIdentifier moduleVersionIdentifier = mock(ModuleVersionIdentifier.class);
given(moduleVersionIdentifier.getGroup()).willReturn(group);
given(moduleVersionIdentifier.getName()).willReturn(module);
given(moduleVersionIdentifier.getVersion()).willReturn(version);
ResolvedModuleVersion moduleVersion = mock(ResolvedModuleVersion.class);
given(moduleVersion.getId()).willReturn(moduleVersionIdentifier);
ResolvedArtifact libraryArtifact = mock(ResolvedArtifact.class);
File file = new File(this.temp, fileName).getAbsoluteFile();
given(libraryArtifact.getFile()).willReturn(file);
given(libraryArtifact.getModuleVersion()).willReturn(moduleVersion);
return libraryArtifact;
}
private List<String> entryLines(JarFile jarFile, String entryName) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(jarFile.getInputStream(jarFile.getEntry(entryName))))) {
return reader.lines().collect(Collectors.toList());
}
}
private Set<String> getLayerNames(List<String> index) {
Set<String> layerNames = new LinkedHashSet<>();
for (String line : index) {
if (line.startsWith("- ")) {
layerNames.add(line.substring(3, line.length() - 2));
}
}
return layerNames;
@Override
void populateResolvedDependencies(Configuration configuration) {
getTask().getResolvedDependencies().processConfiguration(configuration);
}
@Override

@ -16,10 +16,14 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.boot.gradle.junit.GradleCompatibility;
/**
* Integration tests for {@link BootJar}.
* Integration tests for {@link BootWar}.
*
* @author Andy Wilkinson
*/
@ -27,7 +31,14 @@ import org.springframework.boot.gradle.junit.GradleCompatibility;
class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests {
BootWarIntegrationTests() {
super("bootWar", "WEB-INF/lib/", "WEB-INF/classes/");
super("bootWar", "WEB-INF/lib/", "WEB-INF/classes/", "WEB-INF/");
}
@Override
String[] getExpectedApplicationLayerContents(String... additionalFiles) {
Set<String> contents = new TreeSet<>(Arrays.asList(additionalFiles));
contents.addAll(Arrays.asList("WEB-INF/layers.idx", "META-INF/"));
return contents.toArray(new String[0]);
}
}

@ -20,6 +20,8 @@ import java.io.File;
import java.io.IOException;
import java.util.jar.JarFile;
import org.gradle.api.Action;
import org.gradle.api.artifacts.Configuration;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@ -32,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat;
class BootWarTests extends AbstractBootArchiveTests<BootWar> {
BootWarTests() {
super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/");
super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/",
"WEB-INF/");
}
@Test
@ -111,4 +114,19 @@ class BootWarTests extends AbstractBootArchiveTests<BootWar> {
getTask().copy();
}
@Override
void populateResolvedDependencies(Configuration configuration) {
getTask().getResolvedDependencies().processConfiguration(configuration);
}
@Override
void applyLayered(Action<LayeredSpec> action) {
getTask().layered(action);
}
@Override
boolean archiveHasClasspathIndex() {
return false;
}
}

@ -16,6 +16,7 @@ dependencies {
implementation("com.example:library:1.0-SNAPSHOT")
implementation("org.apache.commons:commons-lang3:3.9")
implementation("org.springframework:spring-core:5.2.5.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-logging:2.2.0.RELEASE")
}
task listLayers(type: JavaExec) {

@ -0,0 +1,50 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
bootWar {
mainClass = 'com.example.Application'
layered {
application {
intoLayer("static") {
include "META-INF/resources/**", "resources/**", "static/**", "public/**"
}
intoLayer("app")
}
dependencies {
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
}
intoLayer("commons-dependencies") {
include "org.apache.commons:*"
}
intoLayer("dependencies")
}
layerOrder = ["dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"]
}
}
repositories {
mavenCentral()
maven { url "file:repository" }
}
dependencies {
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 = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
}

@ -16,3 +16,9 @@ dependencies {
developmentOnly("commons-io:commons-io:2.6")
implementation("commons-io:commons-io:2.6")
}
bootWar {
layered {
enabled = false
}
}

@ -0,0 +1,33 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
bootWar {
mainClass = 'com.example.Application'
}
repositories {
mavenCentral()
maven { url "file:repository" }
}
dependencies {
implementation("com.example:library:1.0-SNAPSHOT")
implementation("org.apache.commons:commons-lang3:3.9")
implementation("org.springframework:spring-core:5.2.5.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-logging:2.2.0.RELEASE")
}
task listLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
}

@ -17,3 +17,9 @@ dependencies {
implementation(name: "standard")
implementation(name: "starter")
}
bootWar {
layered {
enabled = false
}
}

@ -0,0 +1,36 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
sourceSets {
custom
}
bootWar {
mainClass = 'com.example.Application'
}
repositories {
mavenCentral()
maven { url "file:repository" }
}
dependencies {
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 = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
}

@ -0,0 +1,62 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
subprojects {
apply plugin: 'java'
group = 'org.example.projects'
version = '1.2.3'
}
bootWar {
mainClass = 'com.example.Application'
layered {
application {
intoLayer("static") {
include "META-INF/resources/**", "resources/**", "static/**", "public/**"
}
intoLayer("app")
}
dependencies {
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
excludeProjectDependencies()
}
intoLayer("subproject-dependencies") {
includeProjectDependencies()
}
intoLayer("commons-dependencies") {
include "org.apache.commons:*"
}
intoLayer("dependencies")
}
layerOrder = ["dependencies", "commons-dependencies", "snapshot-dependencies", "subproject-dependencies", "static", "app"]
}
}
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 = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
}

@ -0,0 +1,40 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
subprojects {
apply plugin: 'java'
group = 'org.example.projects'
version = '1.2.3'
}
bootWar {
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 = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
}

@ -0,0 +1,12 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
bootWar {
mainClass = 'com.example.Application'
layered {
{layerTools}
}
}

@ -0,0 +1,12 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
bootWar {
mainClass = 'com.example.Application'
layered {
{layerEnablement}
}
}

@ -0,0 +1,10 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
bootWar {
mainClass = 'com.example.Application'
{layered}
}
Loading…
Cancel
Save