Support generation and loading of layered jars

Support an alternative fat jar format that is more amenable to Docker
image layers.

The new format arranges files in the following structure:

	BOOT-INF/
	  layers/
	    <layer-name #1>
	      /classes
	      /lib
	    <layer-name #2>
	      /classes
	      /lib

The `BOOT-INF/layers.idx` file provides the names of the layers and the
order in which they should be added (starting with the least changed).

The `JarLauncher` class can load layered jars in both fat and exploded
forms.

Closes gh-19767

Co-authored-by: Phillip Webb <pwebb@pivotal.io>
This commit is contained in:
Madhura Bhave
2020-01-15 21:21:56 -08:00
committed by Phillip Webb
parent 45b1ab46c3
commit e9d61bac75
11 changed files with 614 additions and 7 deletions

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ImplicitLayerResolver}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class ImplicitLayerResolverTests {
private Layers layers = Layers.IMPLICIT;
@Test
void iteratorReturnsLayers() {
assertThat(this.layers).containsExactly(StandardLayers.DEPENDENCIES, StandardLayers.SNAPSHOT_DEPENDENCIES,
StandardLayers.RESOURCES, StandardLayers.APPLICATION);
}
@Test
void getLayerWhenNameInResourceLocationReturnsResourceLayer() {
assertThat(this.layers.getLayer("META-INF/resources/logo.gif")).isEqualTo(StandardLayers.RESOURCES);
assertThat(this.layers.getLayer("resources/logo.gif")).isEqualTo(StandardLayers.RESOURCES);
assertThat(this.layers.getLayer("static/logo.gif")).isEqualTo(StandardLayers.RESOURCES);
assertThat(this.layers.getLayer("public/logo.gif")).isEqualTo(StandardLayers.RESOURCES);
}
@Test
void getLayerWhenNameIsClassInResourceLocationReturnsApplicationLayer() {
assertThat(this.layers.getLayer("META-INF/resources/Logo.class")).isEqualTo(StandardLayers.APPLICATION);
assertThat(this.layers.getLayer("resources/Logo.class")).isEqualTo(StandardLayers.APPLICATION);
assertThat(this.layers.getLayer("static/Logo.class")).isEqualTo(StandardLayers.APPLICATION);
assertThat(this.layers.getLayer("public/Logo.class")).isEqualTo(StandardLayers.APPLICATION);
}
@Test
void getLayerWhenNameNotInResourceLocationReturnsApplicationLayer() {
assertThat(this.layers.getLayer("com/example/Application.class")).isEqualTo(StandardLayers.APPLICATION);
assertThat(this.layers.getLayer("com/example/application.properties")).isEqualTo(StandardLayers.APPLICATION);
}
@Test
void getLayerWhenLibraryIsSnapshotReturnsSnapshotLayer() {
assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.BUILD-SNAPSHOT.jar")))
.isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES);
assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0-SNAPSHOT.jar")))
.isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES);
assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.SNAPSHOT.jar")))
.isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES);
}
@Test
void getLayerWhenLibraryIsNotSnapshotReturnsDependenciesLayer() {
assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.jar"))).isEqualTo(StandardLayers.DEPENDENCIES);
assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0-classified.jar")))
.isEqualTo(StandardLayers.DEPENDENCIES);
}
private Library mockLibrary(String name) {
Library library = mock(Library.class);
given(library.getName()).willReturn(name);
return library;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link Layer}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class LayerTests {
@Test
void createWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new Layer(null)).withMessage("Name must not be empty");
}
@Test
void createWhenNameIsEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new Layer("")).withMessage("Name must not be empty");
}
@Test
void createWhenNameContainsBadCharsThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new Layer("bad!name"))
.withMessage("Malformed layer name 'bad!name'");
}
@Test
void equalsAndHashCode() {
Layer layer1 = new Layer("testa");
Layer layer2 = new Layer("testa");
Layer layer3 = new Layer("testb");
assertThat(layer1.hashCode()).isEqualTo(layer2.hashCode());
assertThat(layer1).isEqualTo(layer1).isEqualTo(layer2).isNotEqualTo(layer3);
}
@Test
void toStringReturnsName() {
assertThat(new Layer("test")).hasToString("test");
}
}

View File

@@ -28,8 +28,13 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@@ -332,6 +337,43 @@ class RepackagerTests {
"BOOT-INF/lib/" + libJarFile2.getName(), "BOOT-INF/lib/" + libJarFile3.getName());
}
@Test
void layeredLayout() throws Exception {
TestJarFile libJar1 = new TestJarFile(this.tempDir);
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
File libJarFile1 = libJar1.getFile();
TestJarFile libJar2 = new TestJarFile(this.tempDir);
libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
File libJarFile2 = libJar2.getFile();
TestJarFile libJar3 = new TestJarFile(this.tempDir);
libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
File libJarFile3 = libJar3.getFile();
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
File file = this.testJarFile.getFile();
Repackager repackager = new Repackager(file);
TestLayers layers = new TestLayers();
layers.addLibrary(libJarFile1, "0001");
layers.addLibrary(libJarFile2, "0002");
layers.addLibrary(libJarFile3, "0003");
repackager.setLayers(layers);
repackager.setLayout(new Layouts.LayeredJar());
repackager.repackage((callback) -> {
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
});
assertThat(hasEntry(file, "BOOT-INF/classpath.idx")).isTrue();
ZipUtil.unpack(file, new File(file.getParent()));
FileInputStream inputStream = new FileInputStream(new File(file.getParent() + "/BOOT-INF/classpath.idx"));
String index = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
String[] libraries = index.split("\\r?\\n");
List<String> expected = new ArrayList<>();
expected.add("BOOT-INF/layers/0001/lib/" + libJarFile1.getName());
expected.add("BOOT-INF/layers/0002/lib/" + libJarFile2.getName());
expected.add("BOOT-INF/layers/0003/lib/" + libJarFile3.getName());
assertThat(Arrays.asList(libraries)).containsExactly(expected.toArray(new String[0]));
}
@Test
void duplicateLibraries() throws Exception {
TestJarFile libJar = new TestJarFile(this.tempDir);
@@ -746,4 +788,40 @@ class RepackagerTests {
}
static class TestLayers implements Layers {
private static final Layer DEFAULT_LAYER = new Layer("default");
private Set<Layer> layers = new LinkedHashSet<Layer>();
private Map<String, Layer> libraries = new HashMap<>();
TestLayers() {
this.layers.add(DEFAULT_LAYER);
}
void addLibrary(File jarFile, String layerName) {
Layer layer = new Layer(layerName);
this.layers.add(layer);
this.libraries.put(jarFile.getName(), layer);
}
@Override
public Iterator<Layer> iterator() {
return this.layers.iterator();
}
@Override
public Layer getLayer(String name) {
return DEFAULT_LAYER;
}
@Override
public Layer getLayer(Library library) {
String name = new File(library.getName()).getName();
return this.libraries.getOrDefault(name, DEFAULT_LAYER);
}
}
}