From b5b4a02c6128240d46a2808dee08cfc9b12ae64d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 22 Jan 2020 20:41:05 -0800 Subject: [PATCH] Automatically add jarmode jars when packaging Update the `Packager` to automatically add the layertools jarmode jar when producing a layered jar. Closes gh-19865 --- .../spring-boot-loader-tools/build.gradle | 15 +++++ .../boot/loader/tools/AbstractJarWriter.java | 41 ++++++------ .../boot/loader/tools/Digest.java | 64 +++++++++++++++++++ .../boot/loader/tools/FileUtils.java | 26 +------- .../loader/tools/InputStreamSupplier.java | 48 ++++++++++++++ .../boot/loader/tools/JarModeLibrary.java | 57 +++++++++++++++++ .../boot/loader/tools/Library.java | 16 +++++ .../boot/loader/tools/Packager.java | 47 ++++++++++---- .../loader/tools/AbstractPackagerTests.java | 18 ++++++ 9 files changed, 272 insertions(+), 60 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Digest.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/InputStreamSupplier.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index 5c816b4d90..a76da94100 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -11,6 +11,7 @@ def generatedResources = "${buildDir}/generated-resources/main" configurations { loader + jarmode } dependencies { @@ -22,6 +23,8 @@ dependencies { loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools")) + testImplementation("org.assertj:assertj-core") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") @@ -45,6 +48,18 @@ task reproducibleLoaderJar(type: Jar) { destinationDirectory = file("${generatedResources}/META-INF/loader") } +task reproducibleJarModeLayerToolsJar(type: Jar) { + dependsOn configurations.jarmode + from { + zipTree(configurations.jarmode.incoming.files.filter {it.name.startsWith "spring-boot-jarmode-layertools" }.singleFile) + } + reproducibleFileOrder = true + preserveFileTimestamps = false + archiveFileName = "spring-boot-jarmode-layertools.jar" + destinationDirectory = file("${generatedResources}/META-INF/jarmode") +} + processResources { dependsOn reproducibleLoaderJar + dependsOn reproducibleJarModeLayerToolsJar } 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 016ad03321..5145da575d 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 @@ -20,17 +20,15 @@ import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -127,17 +125,16 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { /** * Write a nested library. - * @param destination the destination of the library + * @param location the destination of the library * @param library the library * @throws IOException if the write fails */ - public void writeNestedLibrary(String destination, Library library) throws IOException { - File file = library.getFile(); - JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName()); - entry.setTime(getNestedLibraryTime(file)); - new CrcAndSize(file).setupStoredEntry(entry); - try (FileInputStream input = new FileInputStream(file)) { - writeEntry(entry, new InputStreamEntryWriter(input), new LibraryUnpackHandler(library)); + public void writeNestedLibrary(String location, Library library) throws IOException { + 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)); } } @@ -148,7 +145,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { * @throws IOException if the write fails * @since 2.3.0 */ - public void writeIndexFile(String location, List lines) throws IOException { + public void writeIndexFile(String location, Collection lines) throws IOException { if (location != null) { JarArchiveEntry entry = new JarArchiveEntry(location); writeEntry(entry, (outputStream) -> { @@ -163,22 +160,22 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { } } - private long getNestedLibraryTime(File file) { + private long getNestedLibraryTime(Library library) { try { - try (JarFile jarFile = new JarFile(file)) { - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); + try (JarInputStream jarStream = new JarInputStream(library.openStream())) { + JarEntry entry = jarStream.getNextJarEntry(); + while (entry != null) { if (!entry.isDirectory()) { return entry.getTime(); } + entry = jarStream.getNextJarEntry(); } } } catch (Exception ex) { - // Ignore and just use the source file timestamp + // Ignore and just use the library timestamp } - return file.lastModified(); + return library.getLastModified(); } /** @@ -292,8 +289,8 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { private long size; - CrcAndSize(File file) throws IOException { - try (FileInputStream inputStream = new FileInputStream(file)) { + CrcAndSize(InputStreamSupplier supplier) throws IOException { + try (InputStream inputStream = supplier.openStream()) { load(inputStream); } } @@ -380,7 +377,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { @Override public String sha1Hash(String name) throws IOException { - return FileUtils.sha1Hash(this.library.getFile()); + return Digest.sha1(this.library::openStream); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Digest.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Digest.java new file mode 100644 index 0000000000..4da1899b09 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Digest.java @@ -0,0 +1,64 @@ +/* + * 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.IOException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Utility class used to calculate digests. + * + * @author Phillip Webb + */ +final class Digest { + + private Digest() { + } + + /** + * Return the SHA1 digest from the supplied stream. + * @param supplier the stream supplier + * @return the SHA1 digest + * @throws IOException on IO error + */ + static String sha1(InputStreamSupplier supplier) throws IOException { + try { + try (DigestInputStream inputStream = new DigestInputStream(supplier.openStream(), + MessageDigest.getInstance("SHA-1"))) { + byte[] buffer = new byte[4098]; + while (inputStream.read(buffer) != -1) { + // Read the entire stream + } + return bytesToHex(inputStream.getMessageDigest().digest()); + } + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder hex = new StringBuilder(); + for (byte b : bytes) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java index 5cf4582fab..f5964c0156 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java @@ -17,11 +17,7 @@ package org.springframework.boot.loader.tools; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; /** * Utilities for manipulating files and directories in Spring Boot tooling. @@ -62,27 +58,7 @@ public abstract class FileUtils { * @throws IOException if the file cannot be read */ public static String sha1Hash(File file) throws IOException { - try { - try (DigestInputStream inputStream = new DigestInputStream(new FileInputStream(file), - MessageDigest.getInstance("SHA-1"))) { - byte[] buffer = new byte[4098]; - while (inputStream.read(buffer) != -1) { - // Read the entire stream - } - return bytesToHex(inputStream.getMessageDigest().digest()); - } - } - catch (NoSuchAlgorithmException ex) { - throw new IllegalStateException(ex); - } - } - - private static String bytesToHex(byte[] bytes) { - StringBuilder hex = new StringBuilder(); - for (byte b : bytes) { - hex.append(String.format("%02x", b)); - } - return hex.toString(); + return Digest.sha1(InputStreamSupplier.forFile(file)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/InputStreamSupplier.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/InputStreamSupplier.java new file mode 100644 index 0000000000..093fb01cd0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/InputStreamSupplier.java @@ -0,0 +1,48 @@ +/* + * 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.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Supplier to provide an {@link InputStream}. + * + * @author Phillip Webb + */ +@FunctionalInterface +interface InputStreamSupplier { + + /** + * Returns a new open {@link InputStream} at the begining of the content. + * @return a new {@link InputStream} + * @throws IOException on IO error + */ + InputStream openStream() throws IOException; + + /** + * Factory method to create an {@link InputStreamSupplier} for the given {@link File}. + * @param file the source file + * @return a new {@link InputStreamSupplier} instance + */ + static InputStreamSupplier forFile(File file) { + return () -> new FileInputStream(file); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java new file mode 100644 index 0000000000..f6a588eb55 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.springframework.util.Assert; + +/** + * {@link Library} implementation for internal jarmode jars. + * + * @author Phillip Webb + */ +class JarModeLibrary extends Library { + + static final JarModeLibrary LAYER_TOOLS = new JarModeLibrary("spring-boot-jarmode-layertools.jar"); + + JarModeLibrary(String name) { + super(name, null, LibraryScope.RUNTIME, false); + } + + @Override + InputStream openStream() throws IOException { + String path = "META-INF/jarmode/" + getName(); + URL resource = getClass().getClassLoader().getResource(path); + Assert.state(resource != null, "Unable to find resource " + path); + return resource.openStream(); + } + + @Override + long getLastModified() { + return 0L; + } + + @Override + public File getFile() { + throw new UnsupportedOperationException("Unable to access jar mode library file"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java index b9ca6c75c5..0c17636c18 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java @@ -17,6 +17,9 @@ package org.springframework.boot.loader.tools; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; /** * Encapsulates information about a single library that may be packed into the archive. @@ -85,6 +88,15 @@ public class Library { return this.file; } + /** + * Open a stream that provides the content of the source file. + * @return the file content + * @throws IOException on error + */ + InputStream openStream() throws IOException { + return new FileInputStream(this.file); + } + /** * Return the scope of the library. * @return the scope @@ -102,4 +114,8 @@ public class Library { return this.unpackRequired; } + long getLastModified() { + return this.file.lastModified(); + } + } 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 d98978f94f..5b6cb96b5b 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 @@ -17,7 +17,6 @@ package org.springframework.boot.loader.tools; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -81,6 +80,8 @@ public abstract class Packager { private Layers layers = Layers.IMPLICIT; + private boolean includeRelevantJarModeJars = true; + /** * Create a new {@link Packager} instance. * @param source the source JAR file to package @@ -141,6 +142,14 @@ public abstract class Packager { this.layers = layers; } + /** + * Sets if jarmode jars relevant for the packaging should be automatcially included. + * @param includeRelevantJarModeJars if relevant jars are included + */ + public void setIncludeRelevantJarModeJars(boolean includeRelevantJarModeJars) { + this.includeRelevantJarModeJars = includeRelevantJarModeJars; + } + protected final boolean isAlreadyPackaged() throws IOException { return isAlreadyPackaged(this.source); } @@ -191,9 +200,9 @@ public abstract class Packager { return EntryTransformer.NONE; } - private boolean isZip(File file) { + private boolean isZip(InputStreamSupplier supplier) { try { - try (FileInputStream inputStream = new FileInputStream(file)) { + try (InputStream inputStream = supplier.openStream()) { return isZip(inputStream); } } @@ -430,14 +439,24 @@ public abstract class Packager { WritableLibraries(Libraries libraries) throws IOException { libraries.doWithLibraries((library) -> { - if (isZip(library.getFile())) { - String location = getLocation(library); - if (location != null) { - Library existing = this.libraries.putIfAbsent(location + library.getName(), library); - Assert.state(existing == null, "Duplicate library " + library.getName()); - } + if (isZip(library::openStream)) { + addLibrary(library); } }); + if (Packager.this.includeRelevantJarModeJars) { + if (getLayout() instanceof LayeredLayout) { + addLibrary(JarModeLibrary.LAYER_TOOLS); + } + } + } + + private void addLibrary(Library library) { + String location = getLocation(library); + if (location != null) { + String path = location + library.getName(); + Library existing = this.libraries.putIfAbsent(path, library); + Assert.state(existing == null, "Duplicate library " + library.getName()); + } } private String getLocation(Library library) { @@ -461,17 +480,19 @@ public abstract class Packager { public String sha1Hash(String name) throws IOException { Library library = this.libraries.get(name); Assert.notNull(library, "No library found for entry name '" + name + "'"); - return FileUtils.sha1Hash(library.getFile()); + return Digest.sha1(library::openStream); } private void write(AbstractJarWriter writer) throws IOException { for (Entry entry : this.libraries.entrySet()) { - writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1), - entry.getValue()); + String path = entry.getKey(); + Library library = entry.getValue(); + String location = path.substring(0, path.lastIndexOf('/') + 1); + writer.writeNestedLibrary(location, library); } if (getLayout() instanceof RepackagingLayout) { String location = ((RepackagingLayout) getLayout()).getClasspathIndexFileLocation(); - writer.writeIndexFile(location, new ArrayList<>(this.libraries.keySet())); + writer.writeIndexFile(location, this.libraries.keySet()); } } 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 c09e0d41bd..9641f4344c 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 @@ -254,6 +254,7 @@ abstract class AbstractPackagerTests

{ layers.addLibrary(libJarFile2, "0002"); 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)); @@ -277,6 +278,23 @@ abstract class AbstractPackagerTests

{ assertThat(Arrays.asList(layersIndex.split("\\n"))).containsExactly(expectedLayers.toArray(new String[0])); } + @Test + void layeredLayoutAddJarModeJar() 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("BOOT-INF/layers/default/lib/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"); + } + @Test void duplicateLibraries() throws Exception { TestJarFile libJar = new TestJarFile(this.tempDir);