mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-09 19:09:03 +00:00
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:
+8
-2
@@ -55,6 +55,7 @@ import org.gradle.api.internal.file.archive.ZipEntryConstants;
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
import org.gradle.internal.component.external.model.ModuleComponentArtifactIdentifier;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -88,6 +89,8 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
|
||||
private final String libPath;
|
||||
|
||||
private final @Nullable String libProvidedPath;
|
||||
|
||||
private final String classesPath;
|
||||
|
||||
private final String indexPath;
|
||||
@@ -96,11 +99,12 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
|
||||
private T task;
|
||||
|
||||
protected AbstractBootArchiveTests(Class<T> taskClass, String launcherClass, String libPath, String classesPath,
|
||||
String indexPath) {
|
||||
protected AbstractBootArchiveTests(Class<T> taskClass, String launcherClass, String libPath,
|
||||
@Nullable String libProvidedPath, String classesPath, String indexPath) {
|
||||
this.taskClass = taskClass;
|
||||
this.launcherClass = launcherClass;
|
||||
this.libPath = libPath;
|
||||
this.libProvidedPath = libProvidedPath;
|
||||
this.classesPath = classesPath;
|
||||
this.indexPath = indexPath;
|
||||
}
|
||||
@@ -124,6 +128,8 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes"))
|
||||
.isEqualTo(this.classesPath);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib-Provided"))
|
||||
.isEqualTo(this.libProvidedPath);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Version")).isNotNull();
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Implementation-Title"))
|
||||
.isEqualTo(this.project.getName());
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
||||
|
||||
BootJarTests() {
|
||||
super(BootJar.class, "org.springframework.boot.loader.launch.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/",
|
||||
"BOOT-INF/");
|
||||
super(BootJar.class, "org.springframework.boot.loader.launch.JarLauncher", "BOOT-INF/lib/", null,
|
||||
"BOOT-INF/classes/", "BOOT-INF/");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class BootWarTests extends AbstractBootArchiveTests<BootWar> {
|
||||
|
||||
BootWarTests() {
|
||||
super(BootWar.class, "org.springframework.boot.loader.launch.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/",
|
||||
"WEB-INF/");
|
||||
super(BootWar.class, "org.springframework.boot.loader.launch.WarLauncher", "WEB-INF/lib/",
|
||||
"WEB-INF/lib-provided/", "WEB-INF/classes/", "WEB-INF/");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
||||
+4
@@ -33,6 +33,10 @@ $ java -jar my-app/my-app.jar
|
||||
|
||||
After startup, you should not expect any differences in execution time between running an executable jar and running an extracted jar.
|
||||
|
||||
NOTE: An executable war can be extracted in the same way.
|
||||
The result is a jar, for example `my-app/my-app.jar`, that is meant to be run with `java -jar`.
|
||||
Content from the root of the war, such as static files and JSPs from `src/main/webapp`, is moved to `META-INF/resources/` in the extracted jar.
|
||||
|
||||
TIP: Run `java -Djarmode=tools -jar my-app.jar help extract` to see all possible options.
|
||||
|
||||
|
||||
|
||||
+45
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+31
-5
@@ -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 {
|
||||
|
||||
+65
-4
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -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);
|
||||
|
||||
+82
-2
@@ -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 {
|
||||
|
||||
|
||||
+71
-51
@@ -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"
|
||||
+2
@@ -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"
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
+4
@@ -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/");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user