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:
committed by
Phillip Webb
parent
45b1ab46c3
commit
e9d61bac75
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user