Support flat jar layering with layertools

Update layertools to support the flat jar format. Layers are now
determined by reading the `layers.idx` file.

Closes gh-20813
pull/20830/head
Madhura Bhave 5 years ago committed by Phillip Webb
parent bfa04e6574
commit d61a79d90b

@ -20,59 +20,52 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
/** /**
* {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file. * {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave
*/ */
class IndexedLayers implements Layers { class IndexedLayers implements Layers {
private static final String APPLICATION_LAYER = "application"; private MultiValueMap<String, String> layers = new LinkedMultiValueMap<>();
private static final String SPRING_BOOT_APPLICATION_LAYER = "springbootapplication";
private static final Pattern LAYER_PATTERN = Pattern.compile("^BOOT-INF\\/layers\\/([a-zA-Z0-9-]+)\\/.*$");
private List<String> layers;
IndexedLayers(String indexFile) { IndexedLayers(String indexFile) {
String[] lines = indexFile.split("\n"); String[] lines = indexFile.split("\n");
this.layers = Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty()) Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty()).forEach((line) -> {
.collect(Collectors.toCollection(ArrayList::new)); String[] content = line.split(" ");
Assert.state(content.length == 2, "Layer index file is malformed");
this.layers.add(content[0], content[1]);
});
Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded"); Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded");
if (!this.layers.contains(APPLICATION_LAYER)) {
this.layers.add(0, SPRING_BOOT_APPLICATION_LAYER);
}
} }
@Override @Override
public Iterator<String> iterator() { public Iterator<String> iterator() {
return this.layers.iterator(); return this.layers.keySet().iterator();
} }
@Override @Override
public String getLayer(ZipEntry entry) { public String getLayer(ZipEntry entry) {
String name = entry.getName(); String name = entry.getName();
Matcher matcher = LAYER_PATTERN.matcher(name); for (Map.Entry<String, List<String>> indexEntry : this.layers.entrySet()) {
if (matcher.matches()) { if (indexEntry.getValue().contains(name)) {
String layer = matcher.group(1); return indexEntry.getKey();
Assert.state(this.layers.contains(layer), () -> "Unexpected layer '" + layer + "'"); }
return layer;
} }
return this.layers.contains(APPLICATION_LAYER) ? APPLICATION_LAYER : SPRING_BOOT_APPLICATION_LAYER; throw new IllegalStateException("No layer defined in index for file '" + name + "'");
} }
/** /**

@ -77,10 +77,10 @@ class HelpCommandTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
jarOutputStream.putNextEntry(indexEntry); jarOutputStream.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("a\n"); writer.write("0001 BOOT-INF/lib/a.jar\n");
writer.write("b\n"); writer.write("0001 BOOT-INF/lib/b.jar\n");
writer.write("c\n"); writer.write("0002 BOOT-INF/lib/c.jar\n");
writer.write("d\n"); writer.write("0003 BOOT-INF/lib/d.jar\n");
writer.flush(); writer.flush();
} }
return file; return file;

@ -16,10 +16,14 @@
package org.springframework.boot.jarmode.layertools; package org.springframework.boot.jarmode.layertools;
import java.io.InputStreamReader;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
@ -29,6 +33,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link IndexedLayers}. * Tests for {@link IndexedLayers}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave
*/ */
class IndexedLayersTests { class IndexedLayersTests {
@ -39,41 +44,35 @@ class IndexedLayersTests {
} }
@Test @Test
void createWhenIndexFileHasNoApplicationLayerAddSpringBootApplication() { void createWhenIndexFileIsMalformedThrowsException() throws Exception {
IndexedLayers layers = new IndexedLayers("test"); assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test"))
assertThat(layers).contains("springbootapplication"); .withMessage("Layer index file is malformed");
} }
@Test @Test
void iteratorReturnsLayers() { void iteratorReturnsLayers() throws Exception {
IndexedLayers layers = new IndexedLayers("test\napplication"); IndexedLayers layers = new IndexedLayers(getIndex());
assertThat(layers).containsExactly("test", "application"); assertThat(layers).containsExactly("test", "application");
} }
@Test @Test
void getLayerWhenMatchesLayerPatterReturnsLayer() { void getLayerWhenMatchesNameReturnsLayer() throws Exception {
IndexedLayers layers = new IndexedLayers("test"); IndexedLayers layers = new IndexedLayers(getIndex());
assertThat(layers.getLayer(mockEntry("BOOT-INF/layers/test/lib/file.jar"))).isEqualTo("test"); assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test");
assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application");
} }
@Test @Test
void getLayerWhenMatchesLayerPatterForMissingLayerThrowsException() { void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception {
IndexedLayers layers = new IndexedLayers("test"); IndexedLayers layers = new IndexedLayers(getIndex());
assertThatIllegalStateException() assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar")))
.isThrownBy(() -> layers.getLayer(mockEntry("BOOT-INF/layers/missing/lib/file.jar"))) .withMessage("No layer defined in index for file " + "'file.jar'");
.withMessage("Unexpected layer 'missing'");
} }
@Test private String getIndex() throws Exception {
void getLayerWhenDoesNotMatchLayerPatternReturnsApplication() { ClassPathResource resource = new ClassPathResource("test-layers.idx", getClass());
IndexedLayers layers = new IndexedLayers("test\napplication"); InputStreamReader reader = new InputStreamReader(resource.getInputStream());
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application"); return FileCopyUtils.copyToString(reader);
}
@Test
void getLayerWhenDoesNotMatchLayerPatternAndHasNoApplicationLayerReturnsSpringApplication() {
IndexedLayers layers = new IndexedLayers("test");
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("springbootapplication");
} }
private ZipEntry mockEntry(String name) { private ZipEntry mockEntry(String name) {

@ -85,10 +85,10 @@ class LayerToolsJarModeTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
jarOutputStream.putNextEntry(indexEntry); jarOutputStream.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("a\n"); writer.write("0001 BOOT-INF/lib/a.jar\n");
writer.write("b\n"); writer.write("0001 BOOT-INF/lib/b.jar\n");
writer.write("c\n"); writer.write("0002 BOOT-INF/lib/c.jar\n");
writer.write("d\n"); writer.write("0003 BOOT-INF/lib/d.jar\n");
writer.flush(); writer.flush();
} }
return file; return file;

@ -39,6 +39,7 @@ import static org.mockito.BDDMockito.given;
* Tests for {@link ListCommand}. * Tests for {@link ListCommand}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave
*/ */
class ListCommandTests { class ListCommandTests {
@ -74,7 +75,7 @@ class ListCommandTests {
File file = new File(this.temp, name); File file = new File(this.temp, name);
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) { try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
writeLayersIndex(jarOutputStream); writeLayersIndex(jarOutputStream);
String entryPrefix = "BOOT-INF/layers/"; String entryPrefix = "BOOT-INF/lib/";
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/")); jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/"));
jarOutputStream.closeEntry(); jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/a.jar")); jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/a.jar"));
@ -97,10 +98,10 @@ class ListCommandTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
out.putNextEntry(indexEntry); out.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
writer.write("a\n"); writer.write("0001 BOOT-INF/lib/a.jar\n");
writer.write("b\n"); writer.write("0001 BOOT-INF/lib/b.jar\n");
writer.write("c\n"); writer.write("0002 BOOT-INF/lib/c.jar\n");
writer.write("d\n"); writer.write("0003 BOOT-INF/lib/d.jar\n");
writer.flush(); writer.flush();
} }

Loading…
Cancel
Save