Support provided-scope libraries when extracting an executable war

See gh-51372

Signed-off-by: Sumit Kumar Das <151006536+skdas20@users.noreply.github.com>
This commit is contained in:
skdas20
2026-10-08 15:10:32 +02:00
committed by Moritz Halbritter
parent b1966933c6
commit a31ee9f29b
6 changed files with 106 additions and 8 deletions
@@ -86,14 +86,17 @@ class BootArchiveSupport {
}
void configureManifest(Manifest manifest, String mainClass, String classes, String lib,
@Nullable String classPathIndex, @Nullable String layersIndex, String jdkVersion,
String implementationTitle, @Nullable Object implementationVersion) {
@Nullable String libProvided, @Nullable String classPathIndex, @Nullable String layersIndex,
String jdkVersion, String implementationTitle, @Nullable Object implementationVersion) {
Attributes attributes = manifest.getAttributes();
attributes.putIfAbsent("Main-Class", this.loaderMainClass);
attributes.putIfAbsent("Start-Class", mainClass);
attributes.computeIfAbsent("Spring-Boot-Version", (name) -> determineSpringBootVersion());
attributes.putIfAbsent("Spring-Boot-Classes", classes);
attributes.putIfAbsent("Spring-Boot-Lib", lib);
if (libProvided != null) {
attributes.putIfAbsent("Spring-Boot-Lib-Provided", libProvided);
}
if (classPathIndex != null) {
attributes.putIfAbsent("Spring-Boot-Classpath-Index", classPathIndex);
}
@@ -131,7 +131,7 @@ public abstract class BootJar extends Jar implements BootArchive {
@Override
public void copy() {
this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY,
this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, null,
CLASSPATH_INDEX, (isLayeredDisabled()) ? null : LAYERS_INDEX,
this.getTargetJavaVersion().get().getMajorVersion(), this.projectName.get(), this.projectVersion.get());
super.copy();
@@ -106,7 +106,7 @@ public abstract class BootWar extends War implements BootArchive {
@Override
public void copy() {
this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY,
CLASSPATH_INDEX, (isLayeredDisabled()) ? null : LAYERS_INDEX,
LIB_PROVIDED_DIRECTORY, CLASSPATH_INDEX, (isLayeredDisabled()) ? null : LAYERS_INDEX,
this.getTargetJavaVersion().get().getMajorVersion(), this.projectName.get(), this.projectVersion.get());
super.copy();
}
@@ -49,7 +49,7 @@ import org.springframework.util.StringUtils;
class IndexedJarStructure implements JarStructure {
private static final List<String> MANIFEST_DENY_LIST = List.of("Start-Class", "Spring-Boot-Classes",
"Spring-Boot-Lib", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index");
"Spring-Boot-Lib", "Spring-Boot-Lib-Provided", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index");
private static final Set<String> ENTRY_IGNORE_LIST = Set.of("META-INF/", "META-INF/MANIFEST.MF",
"META-INF/services/java.nio.file.spi.FileSystemProvider");
@@ -58,6 +58,8 @@ class IndexedJarStructure implements JarStructure {
private final String libLocation;
private final @Nullable String providedLibLocation;
private final String classesLocation;
private final Set<String> classpathEntries;
@@ -65,6 +67,7 @@ class IndexedJarStructure implements JarStructure {
IndexedJarStructure(Manifest originalManifest, String indexFile) {
this.originalManifest = originalManifest;
this.libLocation = getLocation(originalManifest, "Spring-Boot-Lib");
this.providedLibLocation = getOptionalLocation(originalManifest, "Spring-Boot-Lib-Provided");
this.classesLocation = getLocation(originalManifest, "Spring-Boot-Classes");
this.classpathEntries = readIndexFile(indexFile);
}
@@ -74,6 +77,14 @@ class IndexedJarStructure implements JarStructure {
return (!location.endsWith("/")) ? location + "/" : location;
}
private static @Nullable String getOptionalLocation(Manifest manifest, String attribute) {
String location = manifest.getMainAttributes().getValue(attribute);
if (!StringUtils.hasLength(location)) {
return null;
}
return (!location.endsWith("/")) ? location + "/" : location;
}
private static Set<String> readIndexFile(String indexFile) {
String[] lines = Arrays.stream(indexFile.split("\n"))
.map((line) -> line.replace("\r", ""))
@@ -130,8 +141,13 @@ class IndexedJarStructure implements JarStructure {
}
private String toStructureDependency(String libEntryName) {
Assert.state(libEntryName.startsWith(this.libLocation), () -> "Invalid library location " + libEntryName);
return libEntryName.substring(this.libLocation.length());
if (libEntryName.startsWith(this.libLocation)) {
return libEntryName.substring(this.libLocation.length());
}
if (this.providedLibLocation != null && libEntryName.startsWith(this.providedLibLocation)) {
return libEntryName.substring(this.providedLibLocation.length());
}
throw new IllegalStateException("Invalid library location " + libEntryName);
}
private static String getMandatoryAttribute(Manifest manifest, String attribute) {
@@ -35,6 +35,7 @@ import org.springframework.boot.jarmode.tools.JarStructure.Entry;
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link IndexedJarStructure}.
@@ -53,6 +54,24 @@ class IndexedJarStructureTests {
assertThat(entry.type()).isEqualTo(Type.LIBRARY);
}
@Test
void shouldResolveLibraryEntryFromWarProvidedLocation() throws IOException {
IndexedJarStructure structure = createWarStructure();
Entry entry = structure.resolve("WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar");
assertThat(entry).isNotNull();
assertThat(entry.location()).isEqualTo("tomcat-embed-core-10.1.19.jar");
assertThat(entry.originalLocation()).isEqualTo("WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar");
assertThat(entry.type()).isEqualTo(Type.LIBRARY);
}
@Test
void shouldCreateLauncherManifestForWarWithProvidedLibraries() throws IOException {
IndexedJarStructure structure = createWarStructure();
Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity());
assertThat(getAttributes(manifest)).containsEntry("Class-Path",
"spring-webmvc-6.1.4.jar tomcat-embed-core-10.1.19.jar");
}
@Test
void shouldResolveApplicationEntry() throws IOException {
IndexedJarStructure structure = createStructure();
@@ -119,10 +138,62 @@ class IndexedJarStructureTests {
return result;
}
@Test
void shouldFailToResolveProvidedLibraryWhenAttributeIsMissing() throws IOException {
// an archive whose index references lib-provided but whose manifest does not
// record the location has nothing to flatten the entry against
IndexedJarStructure structure = createWarStructureWithoutProvidedLibAttribute();
assertThatIllegalStateException()
.isThrownBy(() -> structure.resolve("WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar"))
.withMessageContaining("Invalid library location");
}
@Test
void launcherManifestShouldNotContainProvidedLibAttribute() throws IOException {
IndexedJarStructure structure = createWarStructure();
Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity());
assertThat(getAttributes(manifest)).doesNotContainKey("Spring-Boot-Lib-Provided");
}
private IndexedJarStructure createStructure() throws IOException {
return new IndexedJarStructure(createManifest(), createIndexFile());
}
private IndexedJarStructure createWarStructure() throws IOException {
Manifest manifest = new Manifest(new ByteArrayInputStream("""
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.WarLauncher
Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests
Spring-Boot-Version: 3.3.0-SNAPSHOT
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Spring-Boot-Lib-Provided: WEB-INF/lib-provided/
Spring-Boot-Classpath-Index: WEB-INF/classpath.idx
""".getBytes(StandardCharsets.UTF_8)));
String indexFile = """
- "WEB-INF/lib/spring-webmvc-6.1.4.jar"
- "WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar"
""";
return new IndexedJarStructure(manifest, indexFile);
}
private IndexedJarStructure createWarStructureWithoutProvidedLibAttribute() throws IOException {
Manifest manifest = new Manifest(new ByteArrayInputStream("""
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.WarLauncher
Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests
Spring-Boot-Version: 3.3.0-SNAPSHOT
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Spring-Boot-Classpath-Index: WEB-INF/classpath.idx
""".getBytes(StandardCharsets.UTF_8)));
String indexFile = """
- "WEB-INF/lib/spring-webmvc-6.1.4.jar"
- "WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar"
""";
return new IndexedJarStructure(manifest, indexFile);
}
private String createIndexFile() {
return """
- "BOOT-INF/lib/spring-webmvc-6.1.4.jar"
@@ -45,6 +45,7 @@ import org.springframework.boot.loader.tools.AbstractJarWriter.EntryTransformer;
import org.springframework.boot.loader.tools.AbstractJarWriter.UnpackHandler;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -73,6 +74,8 @@ public abstract class Packager {
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final String BOOT_LIB_PROVIDED_ATTRIBUTE = "Spring-Boot-Lib-Provided";
private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index";
@@ -413,7 +416,12 @@ public abstract class Packager {
else {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getClassesLocation());
}
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, getLayout().getLibraryLocation("", LibraryScope.COMPILE));
String libraryLocation = getLayout().getLibraryLocation("", LibraryScope.COMPILE);
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, libraryLocation);
String providedLibraryLocation = getLayout().getLibraryLocation("", LibraryScope.PROVIDED);
if (!ObjectUtils.nullSafeEquals(providedLibraryLocation, libraryLocation)) {
putIfHasLength(attributes, BOOT_LIB_PROVIDED_ATTRIBUTE, providedLibraryLocation);
}
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
if (isLayered()) {
putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layout.getLayersIndexFileLocation());