Add classpath index support for exploded archives
Update the `Repackager` class so that an additional `classpath.idx` file is written into the jar that provides the original order of the classpath. The `JarLauncher` class now uses this file when running as an exploded archive to ensure that the classpath order is the same as when running from the far jar. Closes gh-9128 Co-authored-by: Phillip Webb <pwebb@pivotal.io>
This commit is contained in:
committed by
Phillip Webb
parent
ad72f86bdb
commit
45b1ab46c3
@@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
@@ -27,13 +28,16 @@ import java.io.FilterInputStream;
|
||||
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.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.Arrays;
|
||||
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;
|
||||
@@ -52,6 +56,7 @@ import org.apache.commons.compress.archivers.zip.UnixStat;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
@@ -190,6 +195,28 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a simple index file containing the specified UTF-8 lines.
|
||||
* @param location the location of the index file
|
||||
* @param lines the lines to write
|
||||
* @throws IOException if the write fails
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public void writeIndexFile(String location, List<String> lines) throws IOException {
|
||||
if (location != null) {
|
||||
JarArchiveEntry entry = new JarArchiveEntry(location);
|
||||
writeEntry(entry, (outputStream) -> {
|
||||
BufferedWriter writer = new BufferedWriter(
|
||||
new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
for (String line : lines) {
|
||||
writer.write(line);
|
||||
writer.write("\n");
|
||||
}
|
||||
writer.flush();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private long getNestedLibraryTime(File file) {
|
||||
try {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.Map;
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public final class Layouts {
|
||||
@@ -88,6 +89,11 @@ public final class Layouts {
|
||||
return "BOOT-INF/classes/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClasspathIndexFileLocation() {
|
||||
return "BOOT-INF/classpath.idx";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecutable() {
|
||||
return true;
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @author Madhura Bhave
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class Repackager {
|
||||
@@ -59,6 +60,8 @@ public class Repackager {
|
||||
|
||||
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
|
||||
|
||||
private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
|
||||
|
||||
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
|
||||
|
||||
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||
@@ -336,6 +339,7 @@ public class Repackager {
|
||||
private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) {
|
||||
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation());
|
||||
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
|
||||
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
|
||||
}
|
||||
|
||||
private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) {
|
||||
@@ -473,6 +477,10 @@ public class Repackager {
|
||||
writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),
|
||||
entry.getValue());
|
||||
}
|
||||
if (Repackager.this.layout instanceof RepackagingLayout) {
|
||||
String location = ((RepackagingLayout) (Repackager.this.layout)).getClasspathIndexFileLocation();
|
||||
writer.writeIndexFile(location, new ArrayList<>(this.libraryEntryNames.keySet()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -31,4 +31,15 @@ public interface RepackagingLayout extends Layout {
|
||||
*/
|
||||
String getRepackagedClassesLocation();
|
||||
|
||||
/**
|
||||
* Returns the location of the classpath 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 classpath index file location
|
||||
* @since 2.3.0
|
||||
*/
|
||||
default String getClasspathIndexFileLocation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
@@ -45,6 +48,7 @@ import org.zeroturnaround.zip.ZipUtil;
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -59,6 +63,7 @@ import static org.mockito.Mockito.mock;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class RepackagerTests {
|
||||
|
||||
@@ -299,6 +304,34 @@ class RepackagerTests {
|
||||
assertThat(entry.getComment()).hasSize(47);
|
||||
}
|
||||
|
||||
@Test
|
||||
void index() throws Exception {
|
||||
TestJarFile libJar1 = new TestJarFile(this.tempDir);
|
||||
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile1 = libJar1.getFile();
|
||||
TestJarFile libJar2 = new TestJarFile(this.tempDir);
|
||||
libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile2 = libJar2.getFile();
|
||||
TestJarFile libJar3 = new TestJarFile(this.tempDir);
|
||||
libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile3 = libJar3.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> {
|
||||
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
|
||||
});
|
||||
assertThat(hasEntry(file, "BOOT-INF/classpath.idx")).isTrue();
|
||||
ZipUtil.unpack(file, new File(file.getParent()));
|
||||
FileInputStream inputStream = new FileInputStream(new File(file.getParent() + "/BOOT-INF/classpath.idx"));
|
||||
String index = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
|
||||
String[] libraries = index.split("\\r?\\n");
|
||||
assertThat(Arrays.asList(libraries)).contains("BOOT-INF/lib/" + libJarFile1.getName(),
|
||||
"BOOT-INF/lib/" + libJarFile2.getName(), "BOOT-INF/lib/" + libJarFile3.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateLibraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
|
||||
Reference in New Issue
Block a user