diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index 5145da575d..00e23b3fcd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -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 { + 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); - try { - writeEntry(entry, new InputStreamEntryWriter(inputStream)); - } - finally { - inputStream.close(); - } + 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(); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayeredLayout.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayeredLayout.java deleted file mode 100644 index b62e65d5d7..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayeredLayout.java +++ /dev/null @@ -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); - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayersIndex.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayersIndex.java new file mode 100644 index 0000000000..b12fce5b72 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayersIndex.java @@ -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 layers; + + private final MultiValueMap index = new LinkedMultiValueMap<>(); + + /** + * Create a new {@link LayersIndex} backed by the given layers. + * @param layers the layers in the index + */ + public LayersIndex(Iterable 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 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(); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java index 3a9cfe3327..02b1c3729b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java @@ -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; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 8747bca34a..13cca9eea5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -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,12 +168,16 @@ 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); - - writeLayerIndex(writer); + if (this.layers != null) { + writeLayerIndex(writer); + } } private void writeLoaderClasses(AbstractJarWriter writer) throws IOException { @@ -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 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) { - addLibrary(JarModeLibrary.LAYER_TOOLS); - } + 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,13 +500,19 @@ public abstract class Packager { writer.writeNestedLibrary(location, library); } if (getLayout() instanceof RepackagingLayout) { - String location = ((RepackagingLayout) getLayout()).getClasspathIndexFileLocation(); - List 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 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); + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java index e523889492..ae49e50b90 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java @@ -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; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index dfeeec57ff..6c39ef4777 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -238,7 +238,7 @@ abstract class AbstractPackagerTests

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

{ 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

{ }); assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue(); String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx"); - List 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 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 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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java index 49c2a48c83..44350aafb4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java @@ -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"))) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java index bf36fbec37..c10b28e6ea 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -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() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildInfoIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildInfoIntegrationTests.java index ac36d2eba2..1f4b731ca2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildInfoIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildInfoIntegrationTests.java @@ -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 buildInfo(Consumer> buildInfo) { + private ProjectCallback buildInfo(Consumer> buildInfo) { return buildInfo("target/classes/META-INF/build-info.properties", buildInfo); } - private Consumer buildInfo(String location, Consumer> buildInfo) { + private ProjectCallback buildInfo(String location, Consumer> buildInfo) { return (project) -> buildInfo.accept((buildInfo(project, location))); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index c522ac454a..94d6d707b3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -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 jarHash = new AtomicReference<>(); mavenBuild.project("jar-output-timestamp").execute((project) -> { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java index ad5485e8d5..b2ef0fa443 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java @@ -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 preparation; + private ProjectCallback preparation; private File projectDir; @@ -111,20 +109,20 @@ class MavenBuild { return this; } - MavenBuild prepare(Consumer callback) { + MavenBuild prepare(ProjectCallback callback) { this.preparation = callback; return this; } - void execute(Consumer callback) { + void execute(ProjectCallback callback) { execute(callback, 0); } - void executeAndFail(Consumer callback) { + void executeAndFail(ProjectCallback callback) { execute(callback, 1); } - private void execute(Consumer 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; + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml index 71581fb99f..b2e12e2c4a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml @@ -15,9 +15,9 @@ + my-dependencies-name + snapshot-dependencies configuration application - snapshot-dependencies - my-dependencies-name - \ No newline at end of file + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index 8acb33c91a..94b45034a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java index 6228c2c20c..a3d253863b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java @@ -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;