Add classpath index support for exploded war archives

Update the Maven and Gradle packaging for war files so that a
`classpath.idx` file is written into the archive that provides the
original order of the classpath, as was previously done for jar files.
The `WarLauncher` class will use this file when running as an exploded
archive to ensure that the classpath order is the same as when running
from the far war.

Fixes gh-19875
This commit is contained in:
Scott Frederick
2021-12-09 16:25:30 -06:00
parent 8b5600fca8
commit 8f57f0babb
14 changed files with 168 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@@ -21,9 +21,11 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
/**
* Base class for executable archive {@link Launcher}s.
@@ -31,6 +33,7 @@ import org.springframework.boot.loader.archive.Archive;
* @author Phillip Webb
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Scott Frederick
* @since 1.0.0
*/
public abstract class ExecutableArchiveLauncher extends Launcher {
@@ -39,6 +42,8 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
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;
@@ -64,9 +69,21 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
}
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
// Only needed for exploded archives, regular ones already have a defined order
if (archive instanceof ExplodedArchive) {
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
}
return null;
}
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 : getArchiveEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME;
}
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
@@ -133,7 +150,10 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
* @since 2.3.0
*/
protected boolean isSearchCandidate(Archive.Entry entry) {
return true;
if (getArchiveEntryPathPrefix() == null) {
return true;
}
return entry.getName().startsWith(getArchiveEntryPathPrefix());
}
/**
@@ -166,6 +186,14 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
/**
* Return the path prefix for entries in the archive.
* @return the path prefix
*/
protected String getArchiveEntryPathPrefix() {
return null;
}
@Override
protected boolean isExploded() {
return this.archive.isExploded();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@@ -16,13 +16,8 @@
package org.springframework.boot.loader;
import java.io.IOException;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.EntryFilter;
import org.springframework.boot.loader.archive.ExplodedArchive;
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
@@ -32,12 +27,11 @@ import org.springframework.boot.loader.archive.ExplodedArchive;
* @author Phillip Webb
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Scott Frederick
* @since 1.0.0
*/
public class JarLauncher extends ExecutableArchiveLauncher {
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
@@ -52,38 +46,21 @@ public class JarLauncher extends ExecutableArchiveLauncher {
super(archive);
}
@Override
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
// Only needed for exploded archives, regular ones already have a defined order
if (archive instanceof ExplodedArchive) {
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
}
return super.getClassPathIndex(archive);
}
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 : DEFAULT_CLASSPATH_INDEX_LOCATION;
}
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
@Override
protected String getArchiveEntryPathPrefix() {
return "BOOT-INF/";
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
@@ -17,7 +17,6 @@
package org.springframework.boot.loader;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry;
/**
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
@@ -26,6 +25,7 @@ import org.springframework.boot.loader.archive.Archive.Entry;
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Scott Frederick
* @since 1.0.0
*/
public class WarLauncher extends ExecutableArchiveLauncher {
@@ -42,11 +42,6 @@ public class WarLauncher extends ExecutableArchiveLauncher {
return false;
}
@Override
protected boolean isSearchCandidate(Entry entry) {
return entry.getName().startsWith("WEB-INF/");
}
@Override
public boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
@@ -55,6 +50,11 @@ public class WarLauncher extends ExecutableArchiveLauncher {
return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/");
}
@Override
protected String getArchiveEntryPathPrefix() {
return "WEB-INF/";
}
public static void main(String[] args) throws Exception {
new WarLauncher().launch(args);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@@ -47,6 +47,7 @@ import org.springframework.util.FileCopyUtils;
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Scott Frederick
*/
public abstract class AbstractExecutableArchiveLauncherTests {
@@ -80,9 +81,9 @@ public abstract class AbstractExecutableArchiveLauncherTests {
if (indexed) {
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classpath.idx"));
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("- \"BOOT-INF/lib/foo.jar\"\n");
writer.write("- \"BOOT-INF/lib/bar.jar\"\n");
writer.write("- \"BOOT-INF/lib/baz.jar\"\n");
writer.write("- \"" + entryPrefix + "/lib/foo.jar\"\n");
writer.write("- \"" + entryPrefix + "/lib/bar.jar\"\n");
writer.write("- \"" + entryPrefix + "/lib/baz.jar\"\n");
writer.flush();
jarOutputStream.closeEntry();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@@ -18,7 +18,11 @@ package org.springframework.boot.loader;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -33,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link WarLauncher}.
*
* @author Andy Wilkinson
* @author Scott Frederick
*/
class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
@@ -66,6 +71,29 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
}
}
@Test
void explodedWarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception {
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, Collections.emptyList()));
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
URL[] urls = classLoader.getURLs();
assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot));
}
@Test
void warFilesPresentInWebInfLibsAndNotInClasspathIndexShouldBeAddedAfterWebInfClasses() throws Exception {
ArrayList<String> extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar"));
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, extraLibs));
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
URL[] urls = classLoader.getURLs();
List<File> expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot);
URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new);
assertThat(urls).containsExactly(expectedFileUrls);
}
protected final URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}
@@ -79,4 +107,15 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
return expected;
}
protected final List<File> getExpectedFilesWithExtraLibs(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "WEB-INF/classes"));
expected.add(new File(parent, "WEB-INF/lib/extra-1.jar"));
expected.add(new File(parent, "WEB-INF/lib/extra-2.jar"));
expected.add(new File(parent, "WEB-INF/lib/foo.jar"));
expected.add(new File(parent, "WEB-INF/lib/bar.jar"));
expected.add(new File(parent, "WEB-INF/lib/baz.jar"));
return expected;
}
}