Use classpath index when building classpath in PropertiesLauncher

Fixes gh-41719
This commit is contained in:
Scott Frederick 2024-08-20 15:47:44 -05:00
parent dc67ec962b
commit 21b15558a2
9 changed files with 135 additions and 89 deletions

View File

@ -16,16 +16,12 @@
package org.springframework.boot.loader.launch;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.springframework.boot.loader.launch.Archive.Entry;
/**
* Base class for a {@link Launcher} backed by an executable archive.
*
@ -41,14 +37,8 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx";
private final Archive archive;
private final ClassPathIndexFile classPathIndex;
public ExecutableArchiveLauncher() throws Exception {
this(Archive.create(Launcher.class));
}
@ -58,21 +48,6 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
this.classPathIndex = getClassPathIndex(this.archive);
}
ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
if (!archive.isExploded()) {
return null; // Regular archives already have a defined order
}
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getRootDirectory(), location);
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
return (location != null) ? location : getEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME;
}
@Override
protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {
if (this.classPathIndex != null) {
@ -102,13 +77,6 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
return this.archive.getClassPathUrls(this::isIncludedOnClassPathAndNotIndexed, this::isSearchedDirectory);
}
private boolean isIncludedOnClassPathAndNotIndexed(Entry entry) {
if (!isIncludedOnClassPath(entry)) {
return false;
}
return (this.classPathIndex == null) || !this.classPathIndex.containsEntry(entry.name());
}
/**
* Determine if the specified directory entry is a candidate for further searching.
* @param entry the entry to check
@ -119,18 +87,4 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
&& !isIncludedOnClassPath(entry);
}
/**
* Determine if the specified entry is a nested item that should be added to the
* classpath.
* @param entry the entry to check
* @return {@code true} if the entry is a nested item (jar or directory)
*/
protected abstract boolean isIncludedOnClassPath(Archive.Entry entry);
/**
* Return the path prefix for relevant entries in the archive.
* @return the entry path prefix
*/
protected abstract String getEntryPathPrefix();
}

View File

@ -36,24 +36,6 @@ public class JarLauncher extends ExecutableArchiveLauncher {
super(archive);
}
@Override
protected boolean isIncludedOnClassPath(Archive.Entry entry) {
return isLibraryFileOrClassesDirectory(entry);
}
@Override
protected String getEntryPathPrefix() {
return "BOOT-INF/";
}
static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) {
String name = entry.name();
if (entry.isDirectory()) {
return name.equals("BOOT-INF/classes/");
}
return name.startsWith("BOOT-INF/lib/");
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}

View File

@ -16,12 +16,16 @@
package org.springframework.boot.loader.launch;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.springframework.boot.loader.launch.Archive.Entry;
import org.springframework.boot.loader.net.protocol.Handlers;
/**
@ -30,12 +34,19 @@ import org.springframework.boot.loader.net.protocol.Handlers;
*
* @author Phillip Webb
* @author Dave Syer
* @author Scott Frederick
* @since 3.2.0
*/
public abstract class Launcher {
private static final String JAR_MODE_RUNNER_CLASS_NAME = JarModeRunner.class.getName();
protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx";
protected ClassPathIndexFile classPathIndex;
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
@ -102,6 +113,21 @@ public abstract class Launcher {
return (archive != null) && archive.isExploded();
}
ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
if (!archive.isExploded()) {
return null; // Regular archives already have a defined order
}
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getRootDirectory(), location);
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
return (location != null) ? location : getEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME;
}
/**
* Return the archive being launched or {@code null} if there is no archive.
* @return the launched archive
@ -122,4 +148,37 @@ public abstract class Launcher {
*/
protected abstract Set<URL> getClassPathUrls() throws Exception;
/**
* Return the path prefix for relevant entries in the archive.
* @return the entry path prefix
*/
protected String getEntryPathPrefix() {
return "BOOT-INF/";
}
/**
* Determine if the specified entry is a nested item that should be added to the
* classpath.
* @param entry the entry to check
* @return {@code true} if the entry is a nested item (jar or directory)
*/
protected boolean isIncludedOnClassPath(Archive.Entry entry) {
return isLibraryFileOrClassesDirectory(entry);
}
protected boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) {
String name = entry.name();
if (entry.isDirectory()) {
return name.equals("BOOT-INF/classes/");
}
return name.startsWith("BOOT-INF/lib/");
}
protected boolean isIncludedOnClassPathAndNotIndexed(Entry entry) {
if (!isIncludedOnClassPath(entry)) {
return false;
}
return (this.classPathIndex == null) || !this.classPathIndex.containsEntry(entry.name());
}
}

View File

@ -70,6 +70,7 @@ import org.springframework.boot.loader.net.protocol.jar.JarUrl;
* @author Janne Valkealahti
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @since 3.2.0
*/
public class PropertiesLauncher extends Launcher {
@ -148,6 +149,7 @@ public class PropertiesLauncher extends Launcher {
this.homeDirectory = getHomeDirectory();
initializeProperties();
this.paths = getPaths();
this.classPathIndex = getClassPathIndex(this.archive);
}
protected File getHomeDirectory() throws Exception {
@ -330,6 +332,10 @@ public class PropertiesLauncher extends Launcher {
@Override
protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {
String loaderClassName = getProperty("loader.classLoader");
if (this.classPathIndex != null) {
urls = new ArrayList<>(urls);
urls.addAll(this.classPathIndex.getUrls());
}
if (loaderClassName == null) {
return super.createClassLoader(urls);
}
@ -537,9 +543,9 @@ public class PropertiesLauncher extends Launcher {
}
}
private Set<URL> getClassPathUrlsForRoot() throws IOException {
private Set<URL> getClassPathUrlsForRoot() throws Exception {
debug.log("Adding classpath entries from root archive %s", this.archive);
return this.archive.getClassPathUrls(JarLauncher::isLibraryFileOrClassesDirectory);
return this.archive.getClassPathUrls(this::isIncludedOnClassPathAndNotIndexed, Archive.ALL_ENTRIES);
}
private Predicate<Entry> includeByPrefix(String prefix) {

View File

@ -35,17 +35,13 @@ public class WarLauncher extends ExecutableArchiveLauncher {
super(archive);
}
@Override
public boolean isIncludedOnClassPath(Archive.Entry entry) {
return isLibraryFileOrClassesDirectory(entry);
}
@Override
protected String getEntryPathPrefix() {
return "WEB-INF/";
}
static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) {
@Override
protected boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) {
String name = entry.name();
if (entry.isDirectory()) {
return name.equals("WEB-INF/classes/");

View File

@ -24,6 +24,7 @@ import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
@ -40,13 +41,13 @@ import org.junit.jupiter.api.io.TempDir;
import org.springframework.util.FileCopyUtils;
/**
* Base class for testing {@link ExecutableArchiveLauncher} implementations.
* Base class for testing {@link Launcher} implementations.
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Scott Frederick
*/
abstract class AbstractExecutableArchiveLauncherTests {
abstract class AbstractLauncherTests {
@TempDir
File tempDir;
@ -133,4 +134,8 @@ abstract class AbstractExecutableArchiveLauncherTests {
}
}
protected URLClassLoader createClassLoader(Launcher launcher) throws Exception {
return (URLClassLoader) launcher.createClassLoader(launcher.getClassPathUrls());
}
}

View File

@ -49,7 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
class JarLauncherTests extends AbstractLauncherTests {
@Test
void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception {
@ -115,10 +115,6 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
}));
}
private URLClassLoader createClassLoader(JarLauncher launcher) throws Exception {
return (URLClassLoader) launcher.createClassLoader(launcher.getClassPathUrls());
}
private URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}

View File

@ -22,7 +22,9 @@ import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@ -38,7 +40,6 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.net.protocol.jar.JarUrl;
import org.springframework.boot.loader.testsupport.TestJar;
@ -58,13 +59,11 @@ import static org.hamcrest.Matchers.containsString;
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Scott Frederick
*/
@ExtendWith(OutputCaptureExtension.class)
@AssertFileChannelDataBlocksClosed
class PropertiesLauncherTests {
@TempDir
File tempDir;
class PropertiesLauncherTests extends AbstractLauncherTests {
private PropertiesLauncher launcher;
@ -388,13 +387,66 @@ class PropertiesLauncherTests {
this.launcher = new PropertiesLauncher(archive);
this.launcher.launch(new String[0]);
waitFor("Hello World");
}
@Test
void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception {
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList()));
PropertiesLauncher launcher = new PropertiesLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
assertThat(classLoader.getURLs()).containsExactly(getExpectedFileUrls(explodedRoot));
}
@Test
void customClassLoaderAndExplodedJarAndShouldPreserveClasspathOrderWhenIndexPresent() throws Exception {
System.setProperty("loader.classLoader", URLClassLoader.class.getName());
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList()));
PropertiesLauncher launcher = new PropertiesLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
assertThat(classLoader.getParent()).isInstanceOf(URLClassLoader.class);
assertThat(((URLClassLoader) classLoader.getParent()).getURLs())
.containsExactly(getExpectedFileUrls(explodedRoot));
}
@Test
void jarFilesPresentInBootInfLibsAndNotInClasspathIndexShouldBeAddedAfterBootInfClasses() throws Exception {
ArrayList<String> extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar"));
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, extraLibs));
PropertiesLauncher launcher = new PropertiesLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
List<File> expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot);
URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new);
assertThat(classLoader.getURLs()).containsExactly(expectedFileUrls);
}
private void waitFor(String value) {
Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value));
}
private URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}
private List<File> getExpectedFiles(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "BOOT-INF/classes"));
expected.add(new File(parent, "BOOT-INF/lib/foo.jar"));
expected.add(new File(parent, "BOOT-INF/lib/bar.jar"));
expected.add(new File(parent, "BOOT-INF/lib/baz.jar"));
return expected;
}
private List<File> getExpectedFilesWithExtraLibs(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "BOOT-INF/classes"));
expected.add(new File(parent, "BOOT-INF/lib/extra-1.jar"));
expected.add(new File(parent, "BOOT-INF/lib/extra-2.jar"));
expected.add(new File(parent, "BOOT-INF/lib/foo.jar"));
expected.add(new File(parent, "BOOT-INF/lib/bar.jar"));
expected.add(new File(parent, "BOOT-INF/lib/baz.jar"));
return expected;
}
private Condition<URL> endingWith(String value) {
return new Condition<>() {

View File

@ -40,7 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
class WarLauncherTests extends AbstractLauncherTests {
@Test
void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception {
@ -86,10 +86,6 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
assertThat(urls).containsExactly(expectedFileUrls);
}
private URLClassLoader createClassLoader(Launcher launcher) throws Exception {
return (URLClassLoader) launcher.createClassLoader(launcher.getClassPathUrls());
}
private URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}