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,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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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.
*/

View File

@@ -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

View File

@@ -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();
}
}

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);
}
}
}

View File

@@ -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() {