Support flat jar layering with Maven

Update the Maven plugin so that layered jars now use the regular "flat"
format. The layers.idx file now describes which layer each file should
be placed.

See gh-20813

Co-authored-by: Phillip Webb <pwebb@pivotal.io>
pull/20830/head
Madhura Bhave 5 years ago committed by Phillip Webb
parent 3f806aa513
commit 4e3cdf936f

@ -86,7 +86,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());
setUpEntry(jarFile, entry);
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream);
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, false);
JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
if (transformedEntry != null) {
writeEntry(transformedEntry, entryWriter, unpackHandler);
@ -114,13 +114,18 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
*/
@Override
public void writeEntry(String entryName, InputStream inputStream) throws IOException {
JarArchiveEntry entry = new JarArchiveEntry(entryName);
try {
writeEntry(entry, new InputStreamEntryWriter(inputStream));
}
finally {
inputStream.close();
writeEntry(entryName, new InputStreamEntryWriter(inputStream, true));
}
/**
* Writes an entry. The {@code inputStream} is closed once the entry has been written
* @param entryName the name of the entry
* @param entryWriter the entry writer
* @throws IOException if the write fails
*/
public void writeEntry(String entryName, EntryWriter entryWriter) throws IOException {
JarArchiveEntry entry = new JarArchiveEntry(entryName);
writeEntry(entry, entryWriter);
}
/**
@ -133,9 +138,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
JarArchiveEntry entry = new JarArchiveEntry(location + library.getName());
entry.setTime(getNestedLibraryTime(library));
new CrcAndSize(library::openStream).setupStoredEntry(entry);
try (InputStream inputStream = library.openStream()) {
writeEntry(entry, new InputStreamEntryWriter(inputStream), new LibraryUnpackHandler(library));
}
writeEntry(entry, new InputStreamEntryWriter(library.openStream(), true), new LibraryUnpackHandler(library));
}
/**
@ -200,7 +203,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
JarEntry entry;
while ((entry = inputStream.getNextJarEntry()) != null) {
if (entry.getName().endsWith(".class")) {
writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream));
writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream, false));
}
}
}
@ -254,7 +257,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
ByteArrayOutputStream output = new ByteArrayOutputStream();
entryWriter.write(output);
entry.setComment("UNPACK:" + unpackHandler.sha1Hash(entry.getName()));
return new InputStreamEntryWriter(new ByteArrayInputStream(output.toByteArray()));
return new InputStreamEntryWriter(new ByteArrayInputStream(output.toByteArray()), true);
}
/**
@ -264,8 +267,11 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
private final InputStream inputStream;
InputStreamEntryWriter(InputStream inputStream) {
private final boolean close;
InputStreamEntryWriter(InputStream inputStream, boolean close) {
this.inputStream = inputStream;
this.close = close;
}
@Override
@ -276,6 +282,9 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
if (this.close) {
this.inputStream.close();
}
}
}

@ -1,55 +0,0 @@
/*
* 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,81 @@
/*
* 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.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Index describing the layer to which each entry in a jar belongs.
*
* @author Madhura Bhave
* @author Andy Wilkinson
* @since 2.3.0
*/
public class LayersIndex {
private final Iterable<Layer> layers;
private final MultiValueMap<Layer, String> index = new LinkedMultiValueMap<>();
/**
* Create a new {@link LayersIndex} backed by the given layers.
* @param layers the layers in the index
*/
public LayersIndex(Iterable<Layer> layers) {
this.layers = layers;
}
/**
* Add an item to the index.
* @param layer the layer of the item
* @param name the name of the item
*/
public void add(Layer layer, String name) {
this.index.add(layer, name);
}
/**
* Write the layer index to an output stream.
* @param out the destination stream
* @throws IOException on IO error
*/
public void writeTo(OutputStream out) throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
for (Layer layer : this.layers) {
List<String> names = this.index.get(layer);
if (names != null) {
for (String name : names) {
writer.write(layer.toString());
writer.write(" ");
writer.write(name);
writer.write("\n");
}
}
}
writer.flush();
}
}

@ -94,31 +94,14 @@ public final class Layouts {
return "BOOT-INF/classpath.idx";
}
@Override
public boolean isExecutable() {
return true;
}
}
/**
* 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/";
public boolean isExecutable() {
return true;
}
}

@ -29,6 +29,7 @@ import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
@ -79,7 +80,9 @@ public abstract class Packager {
private LayoutFactory layoutFactory;
private Layers layers = Layers.IMPLICIT;
private Layers layers;
private LayersIndex layersIndex;
private boolean includeRelevantJarModeJars = true;
@ -136,11 +139,11 @@ public abstract class Packager {
/**
* 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;
this.layersIndex = new LayersIndex(layers);
}
/**
@ -165,13 +168,17 @@ public abstract class Packager {
protected final void write(JarFile sourceJar, Libraries libraries, AbstractJarWriter writer) throws IOException {
Assert.notNull(libraries, "Libraries must not be null");
WritableLibraries writeableLibraries = new WritableLibraries(libraries);
if (this.layers != null) {
writer = new LayerTrackingEntryWriter(writer);
}
writer.writeManifest(buildManifest(sourceJar));
writeLoaderClasses(writer);
writer.writeEntries(sourceJar, getEntityTransformer(), writeableLibraries);
writeableLibraries.write(writer);
if (this.layers != null) {
writeLayerIndex(writer);
}
}
private void writeLoaderClasses(AbstractJarWriter writer) throws IOException {
Layout layout = getLayout();
@ -184,19 +191,17 @@ public abstract class Packager {
}
private void writeLayerIndex(AbstractJarWriter writer) throws IOException {
if (this.layers != null && getLayout() instanceof LayeredLayout) {
String location = ((LayeredLayout) this.layout).getLayersIndexFileLocation();
if (StringUtils.hasLength(location)) {
List<String> layerNames = new ArrayList<>();
this.layers.forEach((layer) -> layerNames.add(layer.toString()));
writer.writeIndexFile(location, layerNames);
}
String name = ((RepackagingLayout) this.layout).getLayersIndexFileLocation();
if (StringUtils.hasLength(name)) {
Layer layer = this.layers.getLayer(name);
this.layersIndex.add(layer, name);
writer.writeEntry(name, this.layersIndex::writeTo);
}
}
private EntryTransformer getEntityTransformer() {
if (getLayout() instanceof RepackagingLayout) {
return new RepackagingEntryTransformer((RepackagingLayout) getLayout(), this.layers);
return new RepackagingEntryTransformer((RepackagingLayout) getLayout());
}
return EntryTransformer.NONE;
}
@ -315,10 +320,7 @@ public abstract class Packager {
private void addBootAttributes(Attributes attributes) {
attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion());
Layout layout = getLayout();
if (layout instanceof LayeredLayout) {
addBootBootAttributesForLayeredLayout(attributes, (LayeredLayout) layout);
}
else if (layout instanceof RepackagingLayout) {
if (layout instanceof RepackagingLayout) {
addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) layout);
}
else {
@ -326,15 +328,13 @@ public abstract class Packager {
}
}
private void addBootBootAttributesForLayeredLayout(Attributes attributes, LayeredLayout layout) {
putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layout.getLayersIndexFileLocation());
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, getLayout().getLibraryLocation("", LibraryScope.COMPILE));
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
if (this.layers != null) {
putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layout.getLayersIndexFileLocation());
}
}
private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) {
@ -371,11 +371,8 @@ public abstract class Packager {
private final RepackagingLayout layout;
private final Layers layers;
private RepackagingEntryTransformer(RepackagingLayout layout, Layers layers) {
private RepackagingEntryTransformer(RepackagingLayout layout) {
this.layout = layout;
this.layers = layers;
}
@Override
@ -412,11 +409,6 @@ public abstract class Packager {
}
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;
}
@ -430,6 +422,35 @@ public abstract class Packager {
}
/**
* Decorator to track the layers as entries are written.
*/
private final class LayerTrackingEntryWriter extends AbstractJarWriter {
private final AbstractJarWriter writer;
private LayerTrackingEntryWriter(AbstractJarWriter writer) {
this.writer = writer;
}
@Override
public void writeNestedLibrary(String location, Library library) throws IOException {
this.writer.writeNestedLibrary(location, library);
Layer layer = Packager.this.layers.getLayer(library);
Packager.this.layersIndex.add(layer, location + library.getName());
}
@Override
protected void writeToArchive(ZipEntry entry, EntryWriter entryWriter) throws IOException {
this.writer.writeToArchive(entry, entryWriter);
if (!entry.getName().endsWith("/")) {
Layer layer = Packager.this.layers.getLayer(entry.getName());
Packager.this.layersIndex.add(layer, entry.getName());
}
}
}
/**
* An {@link UnpackHandler} that determines that an entry needs to be unpacked if a
* library that requires unpacking has a matching entry name.
@ -444,15 +465,13 @@ public abstract class Packager {
addLibrary(library);
}
});
if (Packager.this.includeRelevantJarModeJars) {
if (getLayout() instanceof LayeredLayout) {
if (Packager.this.layers != null && Packager.this.includeRelevantJarModeJars) {
addLibrary(JarModeLibrary.LAYER_TOOLS);
}
}
}
private void addLibrary(Library library) {
String location = getLocation(library);
String location = getLayout().getLibraryLocation(library.getName(), library.getScope());
if (location != null) {
String path = location + library.getName();
Library existing = this.libraries.putIfAbsent(path, library);
@ -460,17 +479,6 @@ public abstract class Packager {
}
}
private String getLocation(Library library) {
Layout layout = getLayout();
if (layout instanceof LayeredLayout) {
Layers layers = Packager.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
public boolean requiresUnpack(String name) {
Library library = this.libraries.get(name);
@ -492,11 +500,17 @@ public abstract class Packager {
writer.writeNestedLibrary(location, library);
}
if (getLayout() instanceof RepackagingLayout) {
String location = ((RepackagingLayout) getLayout()).getClasspathIndexFileLocation();
List<String> names = this.libraries.keySet().stream()
.map((key) -> key.substring(key.lastIndexOf('/') + 1)).collect(Collectors.toList());
writer.writeIndexFile(location, names);
writeClasspathIndex((RepackagingLayout) getLayout(), writer);
}
}
private void writeClasspathIndex(RepackagingLayout layout, AbstractJarWriter writer) throws IOException {
List<String> names = this.libraries.keySet().stream().map(this::getJarName).collect(Collectors.toList());
writer.writeIndexFile(layout.getClasspathIndexFileLocation(), names);
}
private String getJarName(String path) {
return path.substring(path.lastIndexOf('/') + 1);
}
}

@ -42,4 +42,15 @@ public interface RepackagingLayout extends Layout {
return null;
}
/**
* Returns the location of the layer 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 layer index file location
* @since 2.3.0
*/
default String getLayersIndexFileLocation() {
return null;
}
}

@ -238,7 +238,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
}
@Test
void layeredLayout() throws Exception {
void layersIndex() throws Exception {
TestJarFile libJar1 = new TestJarFile(this.tempDir);
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
File libJarFile1 = libJar1.getFile();
@ -256,7 +256,6 @@ abstract class AbstractPackagerTests<P extends Packager> {
layers.addLibrary(libJarFile3, "0003");
packager.setLayers(layers);
packager.setIncludeRelevantJarModeJars(false);
packager.setLayout(new Layouts.LayeredJar());
execute(packager, (callback) -> {
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
@ -264,36 +263,105 @@ abstract class AbstractPackagerTests<P extends Packager> {
});
assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue();
String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx");
List<String> expectedJars = new ArrayList<>();
expectedJars.add("BOOT-INF/layers/0001/lib/" + libJarFile1.getName());
expectedJars.add("BOOT-INF/layers/0002/lib/" + libJarFile2.getName());
expectedJars.add("BOOT-INF/layers/0003/lib/" + libJarFile3.getName());
assertThat(Arrays.asList(classpathIndex.split("\\n"))).containsExactly(libJarFile1.getName(),
libJarFile2.getName(), libJarFile3.getName());
assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue();
String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx");
List<String> expectedLayers = new ArrayList<>();
expectedLayers.add("default");
expectedLayers.add("0001");
expectedLayers.add("0002");
expectedLayers.add("0003");
getExpectedLayers(expectedLayers);
expectedLayers.add("default " + "BOOT-INF/classpath.idx");
expectedLayers.add("default " + "BOOT-INF/layers.idx");
expectedLayers.add("0001 " + "BOOT-INF/lib/" + libJarFile1.getName());
expectedLayers.add("0002 " + "BOOT-INF/lib/" + libJarFile2.getName());
expectedLayers.add("0003 " + "BOOT-INF/lib/" + libJarFile3.getName());
assertThat(Arrays.asList(layersIndex.split("\\n"))).containsExactly(expectedLayers.toArray(new String[0]));
}
@Test
void layeredLayoutAddJarModeJar() throws Exception {
private void getExpectedLayers(List<String> expectedLayers) {
expectedLayers.add("default META-INF/MANIFEST.MF");
expectedLayers.add("default org/springframework/boot/loader/ClassPathIndexFile.class");
expectedLayers.add("default org/springframework/boot/loader/ExecutableArchiveLauncher.class");
expectedLayers.add("default org/springframework/boot/loader/JarLauncher.class");
expectedLayers.add(
"default org/springframework/boot/loader/LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class");
expectedLayers.add("default org/springframework/boot/loader/LaunchedURLClassLoader.class");
expectedLayers.add("default org/springframework/boot/loader/Launcher.class");
expectedLayers.add("default org/springframework/boot/loader/MainMethodRunner.class");
expectedLayers.add("default org/springframework/boot/loader/PropertiesLauncher$1.class");
expectedLayers.add("default org/springframework/boot/loader/PropertiesLauncher$ArchiveEntryFilter.class");
expectedLayers
.add("default org/springframework/boot/loader/PropertiesLauncher$PrefixMatchingArchiveFilter.class");
expectedLayers.add("default org/springframework/boot/loader/PropertiesLauncher.class");
expectedLayers.add("default org/springframework/boot/loader/WarLauncher.class");
expectedLayers.add("default org/springframework/boot/loader/archive/Archive$Entry.class");
expectedLayers.add("default org/springframework/boot/loader/archive/Archive$EntryFilter.class");
expectedLayers.add("default org/springframework/boot/loader/archive/Archive.class");
expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$AbstractIterator.class");
expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$ArchiveIterator.class");
expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$EntryIterator.class");
expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$FileEntry.class");
expectedLayers
.add("default org/springframework/boot/loader/archive/ExplodedArchive$SimpleJarFileArchive.class");
expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive.class");
expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive$AbstractIterator.class");
expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive$EntryIterator.class");
expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive$JarFileEntry.class");
expectedLayers
.add("default org/springframework/boot/loader/archive/JarFileArchive$NestedArchiveIterator.class");
expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive.class");
expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessData.class");
expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile$1.class");
expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile$DataInputStream.class");
expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile$FileAccess.class");
expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile.class");
expectedLayers.add("default org/springframework/boot/loader/jar/AsciiBytes.class");
expectedLayers.add("default org/springframework/boot/loader/jar/Bytes.class");
expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord$1.class");
expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64End.class");
expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64Locator.class");
expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord.class");
expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryFileHeader.class");
expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryParser.class");
expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryVisitor.class");
expectedLayers.add("default org/springframework/boot/loader/jar/FileHeader.class");
expectedLayers.add("default org/springframework/boot/loader/jar/Handler.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarEntry.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarEntryFilter.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarFile$1.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarFile$2.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarFile$JarFileType.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarFile.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarFileEntries$1.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarFileEntries$EntryIterator.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarFileEntries.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$1.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$2.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$CloseAction.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$JarEntryName.class");
expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection.class");
expectedLayers.add("default org/springframework/boot/loader/jar/StringSequence.class");
expectedLayers.add("default org/springframework/boot/loader/jar/ZipInflaterInputStream.class");
expectedLayers.add("default org/springframework/boot/loader/jarmode/JarMode.class");
expectedLayers.add("default org/springframework/boot/loader/jarmode/JarModeLauncher.class");
expectedLayers.add("default org/springframework/boot/loader/jarmode/TestJarMode.class");
expectedLayers.add("default org/springframework/boot/loader/util/SystemPropertyUtils.class");
expectedLayers.add("default BOOT-INF/classes/a/b/C.class");
}
@Test
void layersEnabledAddJarModeJar() throws Exception {
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
P packager = createPackager();
TestLayers layers = new TestLayers();
packager.setLayers(layers);
packager.setLayout(new Layouts.LayeredJar());
execute(packager, Libraries.NONE);
assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue();
String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx");
assertThat(Arrays.asList(classpathIndex.split("\\n"))).containsExactly("spring-boot-jarmode-layertools.jar");
assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue();
String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx");
assertThat(Arrays.asList(layersIndex.split("\\n"))).containsExactly("default");
assertThat(Arrays.stream(layersIndex.split("\\n")).map((n) -> n.split(" ")[0]).distinct())
.containsExactly("default");
}
@Test

@ -60,6 +60,14 @@ class ImplicitLayerResolverTests {
assertThat(this.layers.getLayer("com/example/application.properties")).isEqualTo(StandardLayers.APPLICATION);
}
@Test
void getLayerWhenLoaderClassReturnsLoaderLayer() {
assertThat(this.layers.getLayer("org/springframework/boot/loader/Launcher.class"))
.isEqualTo(StandardLayers.SPRING_BOOT_LOADER);
assertThat(this.layers.getLayer("org/springframework/boot/loader/Utils.class"))
.isEqualTo(StandardLayers.SPRING_BOOT_LOADER);
}
@Test
void getLayerWhenLibraryIsSnapshotReturnsSnapshotLayer() {
assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.BUILD-SNAPSHOT.jar")))

@ -19,7 +19,6 @@ 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;
@ -37,17 +36,13 @@ 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 CLASSES_PATTERN.matcher(entry.getName()).matches();
return entry.getName().equals("BOOT-INF/classes/");
}
return LIBS_PATTERN.matcher(entry.getName()).matches();
return entry.getName().startsWith("BOOT-INF/lib/");
};
public JarLauncher() {

@ -26,6 +26,8 @@ import org.assertj.core.api.AssertProvider;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.maven.MavenBuild.ProjectCallback;
import static org.assertj.core.api.Assertions.assertThat;
/**
@ -77,11 +79,11 @@ public class BuildInfoIntegrationTests {
.doesNotContainBuildTime()));
}
private Consumer<File> buildInfo(Consumer<AssertProvider<BuildInfoAssert>> buildInfo) {
private ProjectCallback buildInfo(Consumer<AssertProvider<BuildInfoAssert>> buildInfo) {
return buildInfo("target/classes/META-INF/build-info.properties", buildInfo);
}
private Consumer<File> buildInfo(String location, Consumer<AssertProvider<BuildInfoAssert>> buildInfo) {
private ProjectCallback buildInfo(String location, Consumer<AssertProvider<BuildInfoAssert>> buildInfo) {
return (project) -> buildInfo.accept((buildInfo(project, location)));
}

@ -17,16 +17,21 @@ package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -291,35 +296,54 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
}
@TestTemplate
void whenJarIsRepackagedWithTheLayeredLayoutTheJarContainsLayers(MavenBuild mavenBuild) {
void whenJarIsRepackagedWithLayersEnabledTheJarContainsTheLayersIndex(MavenBuild mavenBuild) {
mavenBuild.project("jar-layered").execute((project) -> {
File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.hasEntryWithNameStartingWith(jarModeLayerTools());
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot").hasEntryWithNameStartingWith(
"BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId());
try {
try (JarFile jarFile = new JarFile(repackaged)) {
ZipEntry entry = jarFile.getEntry("BOOT-INF/layers.idx");
InputStream inputStream = jarFile.getInputStream(entry);
InputStreamReader reader = new InputStreamReader(inputStream);
String[] lines = FileCopyUtils.copyToString(reader).split("\\n");
assertThat(Arrays.stream(lines).map((n) -> n.split(" ")[0]).distinct()).containsExactly(
"dependencies", "spring-boot-loader", "snapshot-dependencies", "application");
}
}
catch (IOException ex) {
}
});
}
@TestTemplate
void whenJarIsRepackagedWithTheLayeredLayoutAndLayerToolsExcluded(MavenBuild mavenBuild) {
void whenJarIsRepackagedWithTheLayersEnabledAndLayerToolsExcluded(MavenBuild mavenBuild) {
mavenBuild.project("jar-layered-no-layer-tools").execute((project) -> {
File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.doesNotHaveEntryWithNameStartingWith(jarModeLayerTools());
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot")
.doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName());
});
}
@TestTemplate
void whenJarIsRepackagedWithTheCustomLayeredLayout(MavenBuild mavenBuild) {
void whenJarIsRepackagedWithTheCustomLayers(MavenBuild mavenBuild) {
mavenBuild.project("jar-layered-custom").execute((project) -> {
File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/my-dependencies-name/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.hasEntryWithNameStartingWith("BOOT-INF/layers/configuration/classes/application.yml");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot");
try (JarFile jarFile = new JarFile(repackaged)) {
ZipEntry entry = jarFile.getEntry("BOOT-INF/layers.idx");
InputStream inputStream = jarFile.getInputStream(entry);
InputStreamReader reader = new InputStreamReader(inputStream);
String[] lines = FileCopyUtils.copyToString(reader).split("\\n");
assertThat(Arrays.stream(lines).map((n) -> n.split(" ")[0]).distinct()).containsExactly(
"my-dependencies-name", "snapshot-dependencies", "configuration", "application");
}
});
}
@ -332,13 +356,6 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
assertThat(firstHash).isEqualTo(secondHash);
}
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
}
private String buildJarWithOutputTimestamp(MavenBuild mavenBuild) {
AtomicReference<String> jarHash = new AtomicReference<>();
mavenBuild.project("jar-output-timestamp").execute((project) -> {

@ -39,7 +39,6 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.function.Consumer;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
@ -56,7 +55,6 @@ import static org.assertj.core.api.Assertions.contentOf;
*
* @author Andy Wilkinson
* @author Scott Frederick
*
*/
class MavenBuild {
@ -70,7 +68,7 @@ class MavenBuild {
private final Properties properties = new Properties();
private Consumer<File> preparation;
private ProjectCallback preparation;
private File projectDir;
@ -111,20 +109,20 @@ class MavenBuild {
return this;
}
MavenBuild prepare(Consumer<File> callback) {
MavenBuild prepare(ProjectCallback callback) {
this.preparation = callback;
return this;
}
void execute(Consumer<File> callback) {
void execute(ProjectCallback callback) {
execute(callback, 0);
}
void executeAndFail(Consumer<File> callback) {
void executeAndFail(ProjectCallback callback) {
execute(callback, 1);
}
private void execute(Consumer<File> callback, int expectedExitCode) {
private void execute(ProjectCallback callback, int expectedExitCode) {
Invoker invoker = new DefaultInvoker();
invoker.setMavenHome(this.home);
InvocationRequest request = new DefaultInvocationRequest();
@ -175,7 +173,7 @@ class MavenBuild {
File target = new File(this.temp, "target");
target.mkdirs();
if (this.preparation != null) {
this.preparation.accept(this.temp);
this.preparation.doWith(this.temp);
}
File buildLogFile = new File(target, "build.log");
try (PrintWriter buildLog = new PrintWriter(new FileWriter(buildLogFile))) {
@ -191,7 +189,7 @@ class MavenBuild {
throw new RuntimeException(ex);
}
}
callback.accept(this.temp);
callback.doWith(this.temp);
}
catch (Exception ex) {
throw new RuntimeException(ex);
@ -213,4 +211,19 @@ class MavenBuild {
}
}
/**
* Action to take on a maven project folder.
*/
@FunctionalInterface
public interface ProjectCallback {
/**
* Take the action on the given project.
* @param project the project directory
* @throws Exception on error
*/
void doWith(File project) throws Exception;
}
}

@ -15,9 +15,9 @@
<into layer="my-dependencies-name" />
</dependencies>
<layerOrder>
<layer>my-dependencies-name</layer>
<layer>snapshot-dependencies</layer>
<layer>configuration</layer>
<layer>application</layer>
<layer>snapshot-dependencies</layer>
<layer>my-dependencies-name</layer>
</layerOrder>
</layers>

@ -43,7 +43,6 @@ import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.LayoutFactory;
import org.springframework.boot.loader.tools.Layouts.Expanded;
import org.springframework.boot.loader.tools.Layouts.Jar;
import org.springframework.boot.loader.tools.Layouts.LayeredJar;
import org.springframework.boot.loader.tools.Layouts.None;
import org.springframework.boot.loader.tools.Layouts.War;
import org.springframework.boot.loader.tools.Libraries;
@ -58,6 +57,8 @@ import org.springframework.boot.loader.tools.layer.CustomLayers;
*/
public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo {
private static final org.springframework.boot.loader.tools.Layers IMPLICIT_LAYERS = org.springframework.boot.loader.tools.Layers.IMPLICIT;
/**
* The Maven project.
* @since 1.0.0
@ -135,10 +136,8 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
packager.setLayout(this.layout.layout());
}
if (this.layers != null && this.layers.isEnabled()) {
if (this.layers.getConfiguration() != null) {
packager.setLayers(getCustomLayers(this.layers.getConfiguration()));
}
packager.setLayout(new LayeredJar());
packager.setLayers((this.layers.getConfiguration() != null)
? getCustomLayers(this.layers.getConfiguration()) : IMPLICIT_LAYERS);
packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools());
}
return packager;

@ -33,8 +33,8 @@ public class Layers {
private File configuration;
/**
* Whether layered jar layout is enabled.
* @return true if the layered layout is enabled.
* Whether a layers.idx file should be added to the jar.
* @return true if a layers.idx file should be added.
*/
public boolean isEnabled() {
return this.enabled;

Loading…
Cancel
Save