diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 12b68384e91f..ea0877b10911 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -84,14 +84,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); } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index b4a2143995af..e11a1e090286 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -131,7 +131,7 @@ ResolvedDependencies getResolvedDependencies() { @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(); diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index 46a3520ac22c..b8f5d6ec756b 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -106,7 +106,7 @@ ResolvedDependencies getResolvedDependencies() { @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(); } diff --git a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java index c4c7dc266ffe..a90f0b03ce8d 100644 --- a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java +++ b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java @@ -49,7 +49,7 @@ class IndexedJarStructure implements JarStructure { private static final List 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 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 List 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 @@ private static String getLocation(Manifest manifest, String attribute) { 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 List readIndexFile(String indexFile) { String[] lines = Arrays.stream(indexFile.split("\n")) .map((line) -> line.replace("\r", "")) @@ -130,8 +141,13 @@ public Manifest createLauncherManifest(UnaryOperator libraryTransformer) } 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) { diff --git a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java index 0585b824254b..7f48232280d3 100644 --- a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java +++ b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java @@ -35,6 +35,7 @@ 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 @@ void shouldResolveLibraryEntry() throws IOException { 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 @@ private Map getAttributes(Manifest manifest) { 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" diff --git a/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 50754fa09c9a..109a0acd40bb 100644 --- a/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -45,6 +45,7 @@ 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; /** @@ -69,6 +70,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"; @@ -406,7 +409,12 @@ private void addBootAttributesForLayout(Attributes attributes) { 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());