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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user