Support flat jar layering with Gradle

Update the Gralde plugin so that layered jars now use the regular "flat"
format. The layers.idx file now describes which layer each file should
be placed.

See gh-20813

Co-authored-by: Phillip Webb <pwebb@pivotal.io>
This commit is contained in:
Andy Wilkinson
2020-04-02 22:44:56 -07:00
committed by Phillip Webb
parent 4e3cdf936f
commit bfa04e6574
10 changed files with 334 additions and 188 deletions

View File

@@ -437,12 +437,16 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
}
protected List<String> getEntryNames(File file) throws IOException {
List<String> entryNames = new ArrayList<>();
try (JarFile jarFile = new JarFile(file)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
entryNames.add(entries.nextElement().getName());
}
return getEntryNames(jarFile);
}
}
protected List<String> getEntryNames(JarFile jarFile) {
List<String> entryNames = new ArrayList<>();
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
entryNames.add(entries.nextElement().getName());
}
return entryNames;
}

View File

@@ -16,14 +16,25 @@
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.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
@@ -77,14 +88,45 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
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(jarModeLayerTools())).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/application/classes/static/file.txt")).isNotNull();
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/commons-io-2.7-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);
List<String> expectedDependencies = new ArrayList<>();
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");
List<String> expectedSnapshotDependencies = new ArrayList<>();
expectedSnapshotDependencies.add("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar");
(layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar);
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
assertThat(indexedLayers.get("spring-boot-loader"))
.allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate());
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
assertThat(indexedLayers.get("application")).containsExactly("META-INF/MANIFEST.MF",
"BOOT-INF/classes/example/Main.class", "BOOT-INF/classes/static/file.txt", "BOOT-INF/classpath.idx",
"BOOT-INF/layers.idx");
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);
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
extractedLayers.forEach(
(name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name)));
}
@TestTemplate
@@ -92,23 +134,49 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
writeMainClass();
writeResource();
BuildResult build = this.gradleBuild.build("bootJar");
System.out.println(build.getOutput());
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(jarModeLayerTools())).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();
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/commons-io-2.7-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);
}
}
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
List<String> layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies",
"static", "app");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
List<String> expectedDependencies = new ArrayList<>();
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/commons-io-2.7-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/file.txt");
List<String> appLayer = new ArrayList<>(indexedLayers.get("app"));
List<String> nonLoaderEntries = Arrays.asList("META-INF/MANIFEST.MF", "BOOT-INF/classes/example/Main.class",
"BOOT-INF/classpath.idx", "BOOT-INF/layers.idx");
assertThat(appLayer).containsSubsequence(nonLoaderEntries);
appLayer.removeAll(nonLoaderEntries);
assertThat(appLayer).allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate());
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);
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
extractedLayers.forEach(
(name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name)));
}
private void writeMainClass() {
@@ -144,4 +212,24 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
}
}
private Map<String, List<String>> readLayerIndex(JarFile jarFile) throws IOException {
ZipEntry indexEntry = jarFile.getEntry("BOOT-INF/layers.idx");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) {
return reader.lines().map((line) -> line.split(" "))
.collect(Collectors.groupingBy((layerAndPath) -> layerAndPath[0], LinkedHashMap::new,
Collectors.mapping((layerAndPath) -> layerAndPath[1], Collectors.toList())));
}
}
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).collect(Collectors.toList()));
}
return extractedLayers;
}
}

View File

@@ -79,46 +79,41 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
}
}
@Test
void whenJarIsLayeredThenBootInfContainsOnlyLayersAndIndexFiles() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames.stream().filter((name) -> name.startsWith("BOOT-INF/"))
.filter((name) -> !name.startsWith("BOOT-INF/layers/"))).contains("BOOT-INF/layers.idx",
"BOOT-INF/classpath.idx");
}
@Test
void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")).isEqualTo(null);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(null);
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 whenJarIsLayeredThenLayersIndexIsPresentAndListsLayersInOrder() throws IOException {
void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
assertThat(entryLines(jarFile, "BOOT-INF/layers.idx")).containsExactly("dependencies", "spring-boot-loader",
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/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");
assertThat(index).contains("dependencies BOOT-INF/lib/first-library.jar",
"dependencies BOOT-INF/lib/second-library.jar",
"snapshot-dependencies BOOT-INF/lib/third-library-SNAPSHOT.jar",
"application BOOT-INF/classes/com/example/Application.class",
"application BOOT-INF/classes/application.properties",
"application BOOT-INF/classes/static/test.css");
}
}
@Test
void whenJarIsLayeredThenContentsAreMovedToLayerDirectories() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames)
.containsSubsequence("BOOT-INF/layers/dependencies/lib/first-library.jar",
"BOOT-INF/layers/dependencies/lib/second-library.jar")
.contains("BOOT-INF/layers/snapshot-dependencies/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/application/classes/static/test.css");
}
@Test
void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException {
void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrent() throws IOException {
File jar = createLayeredJar((layered) -> {
layered.application((application) -> {
application.intoLayer("resources", (spec) -> spec.include("static/**"));
@@ -131,25 +126,30 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
});
layered.layerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "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");
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/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");
assertThat(index).contains("my-internal-deps BOOT-INF/lib/first-library.jar",
"my-internal-deps BOOT-INF/lib/second-library.jar",
"my-snapshot-deps BOOT-INF/lib/third-library-SNAPSHOT.jar",
"application BOOT-INF/classes/com/example/Application.class",
"application BOOT-INF/classes/application.properties",
"resources BOOT-INF/classes/static/test.css");
}
}
@Test
void whenJarIsLayeredJarsInLibAreStored() throws IOException {
void jarsInLibAreStored() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/first-library.jar").getMethod())
assertThat(jarFile.getEntry("BOOT-INF/lib/first-library.jar").getMethod()).isEqualTo(ZipEntry.STORED);
assertThat(jarFile.getEntry("BOOT-INF/lib/second-library.jar").getMethod()).isEqualTo(ZipEntry.STORED);
assertThat(jarFile.getEntry("BOOT-INF/lib/third-library-SNAPSHOT.jar").getMethod())
.isEqualTo(ZipEntry.STORED);
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/second-library.jar").getMethod())
.isEqualTo(ZipEntry.STORED);
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/third-library-SNAPSHOT.jar")
.getMethod()).isEqualTo(ZipEntry.STORED);
}
}
@@ -164,14 +164,7 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
@Test
void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames).contains(jarModeLayerTools());
}
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
assertThat(entryNames).contains("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName());
}
@Test
@@ -267,6 +260,14 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
}
}
private Set<String> getLayerNames(List<String> index) {
return index.stream().map(this::getLayerName).collect(Collectors.toCollection(LinkedHashSet::new));
}
private String getLayerName(String indexLine) {
return indexLine.substring(0, indexLine.indexOf(" "));
}
@Override
protected void executeTask() {
getTask().copy();