Polish "Support provided-scope libraries when extracting an executable war"

Make the extracted war runnable with java -jar: move root content
such as static files and JSPs to META-INF/resources/, name the
application jar *.jar, and write the META-INF/ directory entry
before the manifest.

Fail early when a library and a provided library share a file
name, as both would be extracted to the same location. Assert the
Spring-Boot-Lib-Provided attribute in the Maven and Gradle tests
and document war extraction.

See gh-51372
This commit is contained in:
Moritz Halbritter
2026-10-08 16:04:23 +02:00
parent a31ee9f29b
commit 1dd4bee51c
19 changed files with 343 additions and 75 deletions
@@ -0,0 +1,45 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jarmode.tools;
import java.io.File;
import java.util.Locale;
/**
* The type of archive that is being processed.
*
* @author Moritz Halbritter
*/
enum ArchiveType {
/**
* A jar archive.
*/
JAR,
/**
* A war archive.
*/
WAR;
private static final String WAR_EXTENSION = ".war";
static ArchiveType of(File file) {
return (file.getName().toLowerCase(Locale.ROOT).endsWith(WAR_EXTENSION)) ? WAR : JAR;
}
}
@@ -87,7 +87,9 @@ class ExtractCommand extends Command {
"Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/");
private static final Option APPLICATION_FILENAME_OPTION = Option.of("application-filename", "string",
"Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename");
"Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename, with the extension replaced by .jar");
private static final String META_INF_DIRECTORY = "META-INF/";
private final Context context;
@@ -248,18 +250,25 @@ class ExtractCommand extends Command {
Manifest manifest = jarStructure.createLauncherManifest((library) -> librariesDirectory + library);
mkdirs(file.getParentFile());
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file))) {
Set<String> writtenEntries = new HashSet<>();
ManifestWriter manifestWriter = (sourceJarFile) -> {
JarEntry entry = createJarEntry(JarFile.MANIFEST_NAME,
sourceJarFile.getJarEntry(JarFile.MANIFEST_NAME));
output.putNextEntry(createJarEntry(META_INF_DIRECTORY, entry));
output.closeEntry();
writtenEntries.add(META_INF_DIRECTORY);
output.putNextEntry(entry);
manifest.write(new BufferedOutputStream(output));
output.closeEntry();
};
EnumSet<Type> allowedTypes = EnumSet.of(Type.APPLICATION_CLASS_OR_RESOURCE, Type.META_INF);
Set<String> writtenEntries = new HashSet<>();
EnumSet<Type> allowedTypes = EnumSet.of(Type.APPLICATION_CLASS_OR_RESOURCE, Type.META_INF,
Type.WEB_RESOURCE);
withJarEntries(this.context.getArchiveFile(), manifestWriter, ((stream, jarEntry) -> {
Entry entry = jarStructure.resolve(jarEntry);
if (entry != null && allowedTypes.contains(entry.type()) && StringUtils.hasLength(entry.location())) {
if (entry.type() == Type.WEB_RESOURCE) {
writeParentDirectories(output, writtenEntries, entry.location(), jarEntry);
}
JarEntry newJarEntry = createJarEntry(entry.location(), jarEntry);
if (writtenEntries.add(newJarEntry.getName())) {
output.putNextEntry(newJarEntry);
@@ -268,7 +277,7 @@ class ExtractCommand extends Command {
}
else {
if (!newJarEntry.isDirectory()) {
throw new IllegalStateException("Duplicate jar entry '%s' from original location '%s'"
throw new JarModeErrorException("Duplicate jar entry '%s' from original location '%s'"
.formatted(newJarEntry.getName(), entry.originalLocation()));
}
}
@@ -278,6 +287,19 @@ class ExtractCommand extends Command {
copyTimestamps(this.context.getArchiveFile(), file);
}
private void writeParentDirectories(JarOutputStream output, Set<String> writtenEntries, String location,
JarEntry originalEntry) throws IOException {
int index = location.indexOf('/');
while (index != -1 && index < location.length() - 1) {
String directory = location.substring(0, index + 1);
if (writtenEntries.add(directory)) {
output.putNextEntry(createJarEntry(directory, originalEntry));
output.closeEntry();
}
index = location.indexOf('/', index + 1);
}
}
private void copyTimestamps(File source, File destination) throws IOException {
BasicFileAttributes sourceAttributes = Files.getFileAttributeView(source.toPath(), BasicFileAttributeView.class)
.readAttributes();
@@ -291,7 +313,11 @@ class ExtractCommand extends Command {
if (value != null) {
return value;
}
return this.context.getArchiveFile().getName();
File archiveFile = this.context.getArchiveFile();
if (ArchiveType.of(archiveFile) == ArchiveType.WAR) {
return stripExtension(archiveFile.getName()) + ".jar";
}
return archiveFile.getName();
}
private static void extractEntry(InputStream stream, JarEntry entry, File file) throws IOException {
@@ -22,8 +22,10 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.jar.Attributes;
@@ -31,11 +33,13 @@ import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
import org.springframework.boot.loader.jarmode.JarModeErrorException;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
@@ -54,6 +58,10 @@ class IndexedJarStructure implements JarStructure {
private static final Set<String> ENTRY_IGNORE_LIST = Set.of("META-INF/", "META-INF/MANIFEST.MF",
"META-INF/services/java.nio.file.spi.FileSystemProvider");
private static final String LOADER_LOCATION = "org/springframework/boot/loader/";
private static final String WEB_RESOURCES_LOCATION = "META-INF/resources/";
private final Manifest originalManifest;
private final String libLocation;
@@ -64,12 +72,42 @@ class IndexedJarStructure implements JarStructure {
private final Set<String> classpathEntries;
IndexedJarStructure(Manifest originalManifest, String indexFile) {
private final Set<String> indexLocations;
private final ArchiveType archiveType;
IndexedJarStructure(Manifest originalManifest, String indexFile, ArchiveType archiveType) {
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);
this.indexLocations = getIndexLocations(originalManifest);
this.archiveType = archiveType;
checkForDuplicateLibraryNames();
}
private void checkForDuplicateLibraryNames() {
Map<String, String> libraries = new HashMap<>();
for (String classpathEntry : this.classpathEntries) {
String name = stripLibraryLocation(classpathEntry);
if (name == null) {
continue;
}
String existing = libraries.putIfAbsent(name, classpathEntry);
if (existing != null) {
throw new JarModeErrorException(
"Library name '%s' is used by both '%s' and '%s'".formatted(name, existing, classpathEntry));
}
}
}
private static Set<String> getIndexLocations(Manifest manifest) {
Attributes attributes = manifest.getMainAttributes();
return Stream
.of(attributes.getValue("Spring-Boot-Classpath-Index"), attributes.getValue("Spring-Boot-Layers-Index"))
.filter(StringUtils::hasLength)
.collect(Collectors.toSet());
}
private static String getLocation(Manifest manifest, String attribute) {
@@ -121,7 +159,24 @@ class IndexedJarStructure implements JarStructure {
if (name.startsWith("META-INF/")) {
return new Entry(name, name, Type.META_INF);
}
return null;
return resolveWebResource(name);
}
private @Nullable Entry resolveWebResource(String name) {
if (this.archiveType != ArchiveType.WAR || isWarPackagingEntry(name)) {
return null;
}
return new Entry(name, WEB_RESOURCES_LOCATION + name, Type.WEB_RESOURCE);
}
private boolean isWarPackagingEntry(String name) {
if (this.indexLocations.contains(name) || name.startsWith(this.libLocation)) {
return true;
}
if (this.providedLibLocation != null && name.startsWith(this.providedLibLocation)) {
return true;
}
return name.endsWith("/") && LOADER_LOCATION.startsWith(name);
}
@Override
@@ -141,13 +196,19 @@ class IndexedJarStructure implements JarStructure {
}
private String toStructureDependency(String libEntryName) {
String name = stripLibraryLocation(libEntryName);
Assert.state(name != null, () -> "Invalid library location " + libEntryName);
return name;
}
private @Nullable String stripLibraryLocation(String libEntryName) {
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);
return null;
}
private static String getMandatoryAttribute(Manifest manifest, String attribute) {
@@ -164,7 +225,7 @@ class IndexedJarStructure implements JarStructure {
ZipEntry entry = jarFile.getEntry(location);
if (entry != null) {
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
return new IndexedJarStructure(manifest, indexFile);
return new IndexedJarStructure(manifest, indexFile, ArchiveType.of(file));
}
}
return null;
@@ -73,7 +73,7 @@ interface JarStructure {
enum Type {
LIBRARY, APPLICATION_CLASS_OR_RESOURCE, LOADER, META_INF
LIBRARY, APPLICATION_CLASS_OR_RESOURCE, LOADER, META_INF, WEB_RESOURCE
}
@@ -79,8 +79,14 @@ abstract class AbstractJarModeTests {
File createArchive(Manifest manifest, @Nullable Instant creationTime, @Nullable Instant lastModifiedTime,
@Nullable Instant lastAccessTime, String... entries) throws IOException {
return createArchive("test.jar", manifest, creationTime, lastModifiedTime, lastAccessTime, entries);
}
File createArchive(String filename, Manifest manifest, @Nullable Instant creationTime,
@Nullable Instant lastModifiedTime, @Nullable Instant lastAccessTime, String... entries)
throws IOException {
Assert.state(entries.length % 2 == 0, "Entries must be key value pairs");
File file = new File(this.tempDir, "test.jar");
File file = new File(this.tempDir, filename);
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) {
ZipEntry manifestEntry = createEntry(JarFile.MANIFEST_NAME, creationTime, lastModifiedTime, lastAccessTime);
jar.putNextEntry(manifestEntry);
@@ -136,6 +136,12 @@ class ExtractCommandTests extends AbstractJarModeTests {
"Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index");
}
@Test
void shouldWriteMetaInfDirectoryBeforeManifest() throws IOException {
run(ExtractCommandTests.this.archive);
assertThat(getJarEntryNames(file("test/test.jar"))).startsWith("META-INF/", "META-INF/MANIFEST.MF");
}
@Test
void applicationContainsApplicationClassesAndResources() throws IOException {
run(ExtractCommandTests.this.archive);
@@ -260,7 +266,8 @@ class ExtractCommandTests extends AbstractJarModeTests {
run(file);
File application = file("test/test.jar");
List<String> entryNames = getJarEntryNames(application);
assertThat(entryNames).containsExactlyInAnyOrder("META-INF/native-image/", "META-INF/MANIFEST.MF");
assertThat(entryNames).containsExactlyInAnyOrder("META-INF/", "META-INF/native-image/",
"META-INF/MANIFEST.MF");
}
@Test
@@ -269,13 +276,86 @@ class ExtractCommandTests extends AbstractJarModeTests {
"/jar-contents/classpath.idx", "META-INF/native-image/native-image.properties",
"/jar-contents/empty-file", "BOOT-INF/classes/META-INF/native-image/native-image.properties",
"/jar-contents/empty-file");
assertThatIllegalStateException().isThrownBy(() -> run(file))
assertThatExceptionOfType(JarModeErrorException.class).isThrownBy(() -> run(file))
.withMessage(
"Duplicate jar entry 'META-INF/native-image/native-image.properties' from original location 'BOOT-INF/classes/META-INF/native-image/native-image.properties'");
}
}
@Nested
class ExtractWar {
private Manifest manifest;
private File war;
@BeforeEach
void setUp() throws IOException {
this.manifest = createManifest("Spring-Boot-Classpath-Index: WEB-INF/classpath.idx",
"Spring-Boot-Lib: WEB-INF/lib/", "Spring-Boot-Lib-Provided: WEB-INF/lib-provided/",
"Spring-Boot-Classes: WEB-INF/classes/", "Start-Class: org.example.Main",
"Spring-Boot-Layers-Index: WEB-INF/layers.idx");
this.war = createArchive("test.war", this.manifest, null, null, null, "WEB-INF/classpath.idx",
"/jar-contents/war-classpath.idx", "WEB-INF/layers.idx", "/jar-contents/war-layers.idx",
"WEB-INF/lib/dependency-1.jar", "/jar-contents/dependency-1",
"WEB-INF/lib-provided/dependency-2.jar", "/jar-contents/dependency-2",
"org/springframework/boot/loader/launch/WarLauncher.class", "/jar-contents/JarLauncher",
"WEB-INF/classes/application.properties", "/jar-contents/application.properties", "index.html",
"/jar-contents/empty-file", "WEB-INF/jsp/", "/jar-contents/empty-file", "WEB-INF/jsp/hello.jsp",
"/jar-contents/empty-file");
}
@Test
void shouldCreateApplicationJar() throws IOException {
run(this.war);
assertThat(listFilenames())
.contains("test/test.jar", "test/lib/dependency-1.jar", "test/lib/dependency-2.jar")
.doesNotContain("test/test.war");
}
@Test
void shouldMoveWebResourcesToMetaInfResources() throws IOException {
run(this.war);
assertThat(getJarEntryNames(file("test/test.jar"))).containsExactlyInAnyOrder("META-INF/MANIFEST.MF",
"application.properties", "META-INF/", "META-INF/resources/", "META-INF/resources/index.html",
"META-INF/resources/WEB-INF/", "META-INF/resources/WEB-INF/jsp/",
"META-INF/resources/WEB-INF/jsp/hello.jsp");
}
@Test
void shouldMoveWebResourcesToMetaInfResourcesWhenUsingLayers() throws IOException {
run(this.war, "--layers");
assertThat(listFilenames()).contains("test/dependencies/lib/dependency-1.jar",
"test/dependencies/lib/dependency-2.jar");
assertThat(getJarEntryNames(file("test/application/test.jar"))).contains("META-INF/resources/index.html",
"META-INF/resources/WEB-INF/jsp/hello.jsp");
}
@Test
void shouldFailWhenWebResourceCollidesWithClassesResource() throws IOException {
File war = createArchive("test.war", this.manifest, null, null, null, "WEB-INF/classpath.idx",
"/jar-contents/war-classpath.idx", "WEB-INF/lib/dependency-1.jar", "/jar-contents/dependency-1",
"index.html", "/jar-contents/empty-file", "WEB-INF/classes/META-INF/resources/index.html",
"/jar-contents/empty-file");
assertThatExceptionOfType(JarModeErrorException.class).isThrownBy(() -> run(war))
.withMessage("Duplicate jar entry 'META-INF/resources/index.html' from original location "
+ "'WEB-INF/classes/META-INF/resources/index.html'");
}
@Test
void shouldFailWhenLibraryAndProvidedLibraryHaveSameName() throws IOException {
File war = createArchive("test.war", this.manifest, null, null, null, "WEB-INF/classpath.idx",
"/jar-contents/war-duplicate-classpath.idx", "WEB-INF/lib/dependency-1.jar",
"/jar-contents/dependency-1", "WEB-INF/lib-provided/dependency-1.jar",
"/jar-contents/dependency-1");
assertThatExceptionOfType(JarModeErrorException.class).isThrownBy(() -> run(war))
.withMessage("Library name 'dependency-1.jar' is used by both 'WEB-INF/lib/dependency-1.jar' and "
+ "'WEB-INF/lib-provided/dependency-1.jar'");
}
}
@Nested
class ExtractWithLayers {
@@ -33,9 +33,10 @@ import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.jarmode.tools.JarStructure.Entry;
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
import org.springframework.boot.loader.jarmode.JarModeErrorException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link IndexedJarStructure}.
@@ -44,6 +45,23 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
class IndexedJarStructureTests {
private static final String WAR_MANIFEST = """
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
Spring-Boot-Layers-Index: WEB-INF/layers.idx
""";
private static final String WAR_INDEX_FILE = """
- "WEB-INF/lib/spring-webmvc-6.1.4.jar"
- "WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar"
""";
@Test
void shouldResolveLibraryEntry() throws IOException {
IndexedJarStructure structure = createStructure();
@@ -68,8 +86,42 @@ class IndexedJarStructureTests {
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");
assertThat(getAttributes(manifest))
.containsEntry("Class-Path", "spring-webmvc-6.1.4.jar tomcat-embed-core-10.1.19.jar")
.doesNotContainKey("Spring-Boot-Lib-Provided");
}
@Test
void shouldResolveWarRootEntryAsWebResource() throws IOException {
IndexedJarStructure structure = createWarStructure();
Entry entry = structure.resolve("WEB-INF/jsp/hello.jsp");
assertThat(entry).isNotNull();
assertThat(entry.location()).isEqualTo("META-INF/resources/WEB-INF/jsp/hello.jsp");
assertThat(entry.originalLocation()).isEqualTo("WEB-INF/jsp/hello.jsp");
assertThat(entry.type()).isEqualTo(Type.WEB_RESOURCE);
}
@Test
void shouldNotResolveWarIndexFilesAsWebResources() throws IOException {
IndexedJarStructure structure = createWarStructure();
assertThat(structure.resolve("WEB-INF/classpath.idx")).isNull();
assertThat(structure.resolve("WEB-INF/layers.idx")).isNull();
}
@Test
void shouldNotResolveWarLibraryOrLoaderDirectoriesAsWebResources() throws IOException {
IndexedJarStructure structure = createWarStructure();
assertThat(structure.resolve("WEB-INF/lib/")).isNull();
assertThat(structure.resolve("WEB-INF/lib/doesnt-exist.jar")).isNull();
assertThat(structure.resolve("WEB-INF/lib-provided/")).isNull();
assertThat(structure.resolve("org/")).isNull();
assertThat(structure.resolve("org/springframework/boot/")).isNull();
}
@Test
void shouldNotResolveJarRootEntry() throws IOException {
IndexedJarStructure structure = createStructure();
assertThat(structure.resolve("index.html")).isNull();
}
@Test
@@ -132,66 +184,34 @@ class IndexedJarStructureTests {
.isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE);
}
@Test
void shouldFailWhenLibraryAndProvidedLibraryHaveSameName() {
String indexFile = """
- "WEB-INF/lib/utils-1.0.jar"
- "WEB-INF/lib-provided/utils-1.0.jar"
""";
assertThatExceptionOfType(JarModeErrorException.class).isThrownBy(() -> createWarStructure(indexFile))
.withMessage("Library name 'utils-1.0.jar' is used by both 'WEB-INF/lib/utils-1.0.jar' and "
+ "'WEB-INF/lib-provided/utils-1.0.jar'");
}
private Map<String, String> getAttributes(Manifest manifest) {
Map<String, String> result = new HashMap<>();
manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString()));
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());
return new IndexedJarStructure(createManifest(), createIndexFile(), ArchiveType.JAR);
}
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);
return createWarStructure(WAR_INDEX_FILE);
}
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 IndexedJarStructure createWarStructure(String indexFile) throws IOException {
Manifest manifest = new Manifest(new ByteArrayInputStream(WAR_MANIFEST.getBytes(StandardCharsets.UTF_8)));
return new IndexedJarStructure(manifest, indexFile, ArchiveType.WAR);
}
private String createIndexFile() {
@@ -0,0 +1,2 @@
- "WEB-INF/lib/dependency-1.jar"
- "WEB-INF/lib-provided/dependency-2.jar"
@@ -0,0 +1,2 @@
- "WEB-INF/lib/dependency-1.jar"
- "WEB-INF/lib-provided/dependency-1.jar"
@@ -0,0 +1,12 @@
- "dependencies":
- "WEB-INF/lib/dependency-1.jar"
- "WEB-INF/lib-provided/"
- "spring-boot-loader":
- "org/"
- "application":
- "META-INF/"
- "WEB-INF/classes/"
- "WEB-INF/classpath.idx"
- "WEB-INF/jsp/"
- "WEB-INF/layers.idx"
- "index.html"
@@ -10,5 +10,5 @@ Options:
--layers string list Layers to extract
--destination string Directory to extract files to. Defaults to a directory named after the uber JAR (without the file extension)
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
--application-filename string Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename
--application-filename string Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename, with the extension replaced by .jar
--force Whether to ignore non-empty directories, extract anyway
@@ -10,5 +10,5 @@ Options:
--layers string list Layers to extract
--destination string Directory to extract files to. Defaults to a directory named after the uber JAR (without the file extension)
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
--application-filename string Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename
--application-filename string Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename, with the extension replaced by .jar
--force Whether to ignore non-empty directories, extract anyway
@@ -8,5 +8,5 @@ Options:
--layers string list Layers to extract
--destination string Directory to extract files to. Defaults to a directory named after the uber JAR (without the file extension)
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
--application-filename string Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename
--application-filename string Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename, with the extension replaced by .jar
--force Whether to ignore non-empty directories, extract anyway
@@ -416,9 +416,9 @@ public abstract class Packager {
else {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getClassesLocation());
}
String libraryLocation = getLayout().getLibraryLocation("", LibraryScope.COMPILE);
String libraryLocation = layout.getLibraryLocation("", LibraryScope.COMPILE);
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, libraryLocation);
String providedLibraryLocation = getLayout().getLibraryLocation("", LibraryScope.PROVIDED);
String providedLibraryLocation = layout.getLibraryLocation("", LibraryScope.PROVIDED);
if (!ObjectUtils.nullSafeEquals(providedLibraryLocation, libraryLocation)) {
putIfHasLength(attributes, BOOT_LIB_PROVIDED_ATTRIBUTE, providedLibraryLocation);
}
@@ -396,6 +396,8 @@ abstract class AbstractPackagerTests<P extends Packager> {
assertThat(actualManifest).isNotNull();
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"),
"BOOT-INF/lib/");
assertThat(actualManifest.getMainAttributes())
.doesNotContainKey(new Attributes.Name("Spring-Boot-Lib-Provided"));
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"),
"BOOT-INF/classes/");
}
@@ -409,6 +411,8 @@ abstract class AbstractPackagerTests<P extends Packager> {
assertThat(actualManifest).isNotNull();
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"),
"WEB-INF/lib/");
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib-Provided"),
"WEB-INF/lib-provided/");
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"),
"WEB-INF/classes/");
}