Commit e9d61bac authored by Madhura Bhave's avatar Madhura Bhave Committed by Phillip Webb

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: 's avatarPhillip Webb <pwebb@pivotal.io>
parent 45b1ab46
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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);
}
/*
* 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
......
/*
* 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();
}
}
/*
* 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;
}
}
/*
* 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() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment