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,58 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Layers} that uses implicit rules.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ImplicitLayerResolver extends StandardLayers {
|
||||
|
||||
private static final String[] RESOURCE_LOCATIONS = { "META-INF/resources/", "resources/", "static/", "public/" };
|
||||
|
||||
@Override
|
||||
public Layer getLayer(String name) {
|
||||
if (!isClassFile(name) && isInResourceLocation(name)) {
|
||||
return RESOURCES;
|
||||
}
|
||||
return APPLICATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Layer getLayer(Library library) {
|
||||
if (library.getName().contains("SNAPSHOT.")) {
|
||||
return SNAPSHOT_DEPENDENCIES;
|
||||
}
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
private boolean isClassFile(String name) {
|
||||
return name.endsWith(".class");
|
||||
}
|
||||
|
||||
private boolean isInResourceLocation(String name) {
|
||||
for (String resourceLocation : RESOURCE_LOCATIONS) {
|
||||
if (name.startsWith(resourceLocation)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A named layer used to separate the jar when creating a Docker image.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @see Layers
|
||||
*/
|
||||
public class Layer {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");
|
||||
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Create a new {@link Layer} instance with the specified name.
|
||||
* @param name the name of the layer.
|
||||
*/
|
||||
public Layer(String name) {
|
||||
Assert.hasText(name, "Name must not be empty");
|
||||
Assert.isTrue(PATTERN.matcher(name).matches(), "Malformed layer name '" + name + "'");
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
return this.name.equals(((Layer) obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A specialization of {@link RepackagingLayout} that supports layers in the repackaged
|
||||
* archive.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public interface LayeredLayout extends RepackagingLayout {
|
||||
|
||||
/**
|
||||
* Returns the location of the layers index file that should be written or
|
||||
* {@code null} if not index is required. The result should include the filename and
|
||||
* is relative to the root of the jar.
|
||||
* @return the layers index file location
|
||||
*/
|
||||
String getLayersIndexFileLocation();
|
||||
|
||||
/**
|
||||
* Returns the location to which classes should be moved within the context of a
|
||||
* layer.
|
||||
* @param layer the destination layer for the content
|
||||
* @return the repackaged classes location
|
||||
*/
|
||||
String getRepackagedClassesLocation(Layer layer);
|
||||
|
||||
/**
|
||||
* Returns the destination path for a given library within the context of a layer.
|
||||
* @param libraryName the name of the library (excluding any path)
|
||||
* @param scope the scope of the library
|
||||
* @param layer the destination layer for the content
|
||||
* @return the location of the library relative to the root of the archive (should end
|
||||
* with '/') or {@code null} if the library should not be included.
|
||||
*/
|
||||
String getLibraryLocation(String libraryName, LibraryScope scope, Layer layer);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Interface to provide information about layers to the {@link Repackager}.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @see Layer
|
||||
*/
|
||||
public interface Layers extends Iterable<Layer> {
|
||||
|
||||
/**
|
||||
* The default layer resolver.
|
||||
*/
|
||||
Layers IMPLICIT = new ImplicitLayerResolver();
|
||||
|
||||
/**
|
||||
* Return the jar layers in the order that they should be added (starting with the
|
||||
* least frequently changed layer).
|
||||
*/
|
||||
@Override
|
||||
Iterator<Layer> iterator();
|
||||
|
||||
/**
|
||||
* Return the layer that contains the given resource name.
|
||||
* @param resourceName the name of the resource (for example a {@code .class} file).
|
||||
* @return the layer that contains the resource (must never be {@code null})
|
||||
*/
|
||||
Layer getLayer(String resourceName);
|
||||
|
||||
/**
|
||||
* Return the layer that contains the given library.
|
||||
* @param library the library to consider
|
||||
* @return the layer that contains the resource (must never be {@code null})
|
||||
*/
|
||||
Layer getLayer(Library library);
|
||||
|
||||
}
|
||||
@@ -101,6 +101,28 @@ public final class Layouts {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executable JAR layout with support for layers.
|
||||
*/
|
||||
public static class LayeredJar extends Jar implements LayeredLayout {
|
||||
|
||||
@Override
|
||||
public String getLayersIndexFileLocation() {
|
||||
return "BOOT-INF/layers.idx";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepackagedClassesLocation(Layer layer) {
|
||||
return "BOOT-INF/layers/" + layer + "/classes/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLibraryLocation(String libraryName, LibraryScope scope, Layer layer) {
|
||||
return "BOOT-INF/layers/" + layer + "/lib/";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executable expanded archive layout.
|
||||
*/
|
||||
|
||||
@@ -62,6 +62,8 @@ public class Repackager {
|
||||
|
||||
private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
|
||||
|
||||
private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index";
|
||||
|
||||
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
|
||||
|
||||
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||
@@ -80,6 +82,8 @@ public class Repackager {
|
||||
|
||||
private LayoutFactory layoutFactory;
|
||||
|
||||
private Layers layers = Layers.IMPLICIT;
|
||||
|
||||
public Repackager(File source) {
|
||||
this(source, null);
|
||||
}
|
||||
@@ -128,6 +132,16 @@ public class Repackager {
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layers that should be used in the jar.
|
||||
* @param layers the jar layers
|
||||
* @see LayeredLayout
|
||||
*/
|
||||
public void setLayers(Layers layers) {
|
||||
Assert.notNull(layers, "Layers must not be null");
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layout factory for the jar. The factory can be used when no specific
|
||||
* layout is specified.
|
||||
@@ -244,7 +258,7 @@ public class Repackager {
|
||||
|
||||
private EntryTransformer getEntityTransformer() {
|
||||
if (this.layout instanceof RepackagingLayout) {
|
||||
return new RepackagingEntryTransformer((RepackagingLayout) this.layout);
|
||||
return new RepackagingEntryTransformer((RepackagingLayout) this.layout, this.layers);
|
||||
}
|
||||
return EntryTransformer.NONE;
|
||||
}
|
||||
@@ -328,7 +342,10 @@ public class Repackager {
|
||||
|
||||
private void addBootAttributes(Attributes attributes) {
|
||||
attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion());
|
||||
if (this.layout instanceof RepackagingLayout) {
|
||||
if (this.layout instanceof LayeredLayout) {
|
||||
addBootBootAttributesForLayeredLayout(attributes, (LayeredLayout) this.layout);
|
||||
}
|
||||
else if (this.layout instanceof RepackagingLayout) {
|
||||
addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) this.layout);
|
||||
}
|
||||
else {
|
||||
@@ -336,6 +353,12 @@ public class Repackager {
|
||||
}
|
||||
}
|
||||
|
||||
private void addBootBootAttributesForLayeredLayout(Attributes attributes, LayeredLayout layout) {
|
||||
String layersIndexFileLocation = layout.getLayersIndexFileLocation();
|
||||
putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layersIndexFileLocation);
|
||||
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
|
||||
}
|
||||
|
||||
private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) {
|
||||
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation());
|
||||
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
|
||||
@@ -388,8 +411,11 @@ public class Repackager {
|
||||
|
||||
private final RepackagingLayout layout;
|
||||
|
||||
private RepackagingEntryTransformer(RepackagingLayout layout) {
|
||||
private final Layers layers;
|
||||
|
||||
private RepackagingEntryTransformer(RepackagingLayout layout, Layers layers) {
|
||||
this.layout = layout;
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -400,7 +426,7 @@ public class Repackager {
|
||||
if (!isTransformable(entry)) {
|
||||
return entry;
|
||||
}
|
||||
String transformedName = this.layout.getRepackagedClassesLocation() + entry.getName();
|
||||
String transformedName = transformName(entry.getName());
|
||||
JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName);
|
||||
transformedEntry.setTime(entry.getTime());
|
||||
transformedEntry.setSize(entry.getSize());
|
||||
@@ -425,6 +451,15 @@ public class Repackager {
|
||||
return transformedEntry;
|
||||
}
|
||||
|
||||
private String transformName(String name) {
|
||||
if (this.layout instanceof LayeredLayout) {
|
||||
Layer layer = this.layers.getLayer(name);
|
||||
Assert.state(layer != null, "Invalid 'null' layer from " + this.layers.getClass().getName());
|
||||
return ((LayeredLayout) this.layout).getRepackagedClassesLocation(layer) + name;
|
||||
}
|
||||
return this.layout.getRepackagedClassesLocation() + name;
|
||||
}
|
||||
|
||||
private boolean isTransformable(JarArchiveEntry entry) {
|
||||
String name = entry.getName();
|
||||
if (name.startsWith("META-INF/")) {
|
||||
@@ -456,7 +491,14 @@ public class Repackager {
|
||||
}
|
||||
|
||||
private String getLocation(Library library) {
|
||||
return Repackager.this.layout.getLibraryLocation(library.getName(), library.getScope());
|
||||
Layout layout = Repackager.this.layout;
|
||||
if (layout instanceof LayeredLayout) {
|
||||
Layers layers = Repackager.this.layers;
|
||||
Layer layer = layers.getLayer(library);
|
||||
Assert.state(layer != null, "Invalid 'null' library layer from " + layers.getClass().getName());
|
||||
return ((LayeredLayout) layout).getLibraryLocation(library.getName(), library.getScope(), layer);
|
||||
}
|
||||
return layout.getLibraryLocation(library.getName(), library.getScope());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base class for the standard set of {@link Layers}. Defines the following layers:
|
||||
* <ol>
|
||||
* <li>"dependencies" - For non snapshot dependencies</li>
|
||||
* <li>"snapshot-dependencies" - For snapshot dependencies</li>
|
||||
* <li>"resources" - For static resources such as HTML files</li>
|
||||
* <li>"application" - For application classes and resources</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public abstract class StandardLayers implements Layers {
|
||||
|
||||
/**
|
||||
* The dependencies layer.
|
||||
*/
|
||||
public static final Layer DEPENDENCIES = new Layer("dependencies");
|
||||
|
||||
/**
|
||||
* The snapshot dependencies layer.
|
||||
*/
|
||||
public static final Layer SNAPSHOT_DEPENDENCIES = new Layer("snapshot-dependencies");
|
||||
|
||||
/**
|
||||
* The resources layer.
|
||||
*/
|
||||
public static final Layer RESOURCES = new Layer("resources");
|
||||
|
||||
/**
|
||||
* The application layer.
|
||||
*/
|
||||
public static final Layer APPLICATION = new Layer("application");
|
||||
|
||||
private static final List<Layer> LAYERS;
|
||||
static {
|
||||
List<Layer> layers = new ArrayList<>();
|
||||
layers.add(DEPENDENCIES);
|
||||
layers.add(SNAPSHOT_DEPENDENCIES);
|
||||
layers.add(RESOURCES);
|
||||
layers.add(APPLICATION);
|
||||
LAYERS = Collections.unmodifiableList(layers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Layer> iterator() {
|
||||
return LAYERS.iterator();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.loader;
|
||||
import java.io.IOException;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.Archive.EntryFilter;
|
||||
@@ -36,13 +37,17 @@ import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||
*/
|
||||
public class JarLauncher extends ExecutableArchiveLauncher {
|
||||
|
||||
private static final Pattern CLASSES_PATTERN = Pattern.compile("BOOT-INF\\/(layers\\/.*\\/)?classes/");
|
||||
|
||||
private static final Pattern LIBS_PATTERN = Pattern.compile("BOOT-INF\\/(layers\\/.*\\/)?lib\\/.+");
|
||||
|
||||
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
|
||||
|
||||
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
|
||||
if (entry.isDirectory()) {
|
||||
return entry.getName().equals("BOOT-INF/classes/");
|
||||
return CLASSES_PATTERN.matcher(entry.getName()).matches();
|
||||
}
|
||||
return entry.getName().startsWith("BOOT-INF/lib/");
|
||||
return LIBS_PATTERN.matcher(entry.getName()).matches();
|
||||
};
|
||||
|
||||
public JarLauncher() {
|
||||
|
||||
Reference in New Issue
Block a user