Add support for creating layered war files with Maven

See gh-22821
This commit is contained in:
Madhura Bhave
2021-01-28 12:39:19 -08:00
parent fd27c26e3b
commit 152698f2b2
47 changed files with 902 additions and 128 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.
@@ -36,7 +36,7 @@ import org.springframework.util.Assert;
*/
class Context {
private final File jarFile;
private final File archiveFile;
private final File workingDir;
@@ -46,23 +46,31 @@ class Context {
* Create a new {@link Context} instance.
*/
Context() {
this(getSourceJarFile(), Paths.get(".").toAbsolutePath().normalize().toFile());
this(getSourceArchiveFile(), Paths.get(".").toAbsolutePath().normalize().toFile());
}
/**
* Create a new {@link Context} instance with the specified value.
* @param jarFile the source jar file
* @param archiveFile the source archive file
* @param workingDir the working directory
*/
Context(File jarFile, File workingDir) {
Assert.state(jarFile != null && jarFile.isFile() && jarFile.exists()
&& jarFile.getName().toLowerCase().endsWith(".jar"), "Unable to find source JAR");
this.jarFile = jarFile;
Context(File archiveFile, File workingDir) {
Assert.state(isExistingFile(archiveFile) && isJarOrWar(archiveFile), "Unable to find source archive");
this.archiveFile = archiveFile;
this.workingDir = workingDir;
this.relativeDir = deduceRelativeDir(jarFile.getParentFile(), this.workingDir);
this.relativeDir = deduceRelativeDir(archiveFile.getParentFile(), this.workingDir);
}
private static File getSourceJarFile() {
private boolean isExistingFile(File archiveFile) {
return archiveFile != null && archiveFile.isFile() && archiveFile.exists();
}
private boolean isJarOrWar(File jarFile) {
String name = jarFile.getName().toLowerCase();
return name.endsWith(".jar") || name.endsWith(".war");
}
private static File getSourceArchiveFile() {
try {
ProtectionDomain domain = Context.class.getProtectionDomain();
CodeSource codeSource = (domain != null) ? domain.getCodeSource() : null;
@@ -106,11 +114,11 @@ class Context {
}
/**
* Return the source jar file that is running in tools mode.
* @return the jar file
* Return the source archive file that is running in tools mode.
* @return the archive file
*/
File getJarFile() {
return this.jarFile;
File getArchiveFile() {
return this.archiveFile;
}
/**
@@ -122,11 +130,11 @@ class Context {
}
/**
* Return the directory relative to {@link #getWorkingDir()} that contains the jar or
* Return the directory relative to {@link #getWorkingDir()} that contains the archive or
* {@code null} if none relative directory can be deduced.
* @return the relative dir ending in {@code /} or {@code null}
*/
String getRelativeJarDir() {
String getRelativeArchiveDir() {
return this.relativeDir;
}

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.
@@ -65,9 +65,9 @@ class ExtractCommand extends Command {
mkDirs(new File(destination, layer));
}
}
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getJarFile()))) {
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getArchiveFile()))) {
ZipEntry entry = zip.getNextEntry();
Assert.state(entry != null, "File '" + this.context.getJarFile().toString()
Assert.state(entry != null, "File '" + this.context.getArchiveFile().toString()
+ "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled");
while (entry != null) {
if (!entry.isDirectory()) {

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.
@@ -99,7 +99,7 @@ class HelpCommand extends Command {
}
private String getJavaCommand() {
return "java -Djarmode=layertools -jar " + this.context.getJarFile().getName();
return "java -Djarmode=layertools -jar " + this.context.getArchiveFile().getName();
}
}

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.
@@ -27,6 +27,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.springframework.util.Assert;
@@ -34,7 +35,7 @@ import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file.
* {@link Layers} implementation backed by a {@code layers.idx} file.
*
* @author Phillip Webb
* @author Madhura Bhave
@@ -91,8 +92,10 @@ class IndexedLayers implements Layers {
*/
static IndexedLayers get(Context context) {
try {
try (JarFile jarFile = new JarFile(context.getJarFile())) {
ZipEntry entry = jarFile.getEntry("BOOT-INF/layers.idx");
try (JarFile jarFile = new JarFile(context.getArchiveFile())) {
Manifest manifest = jarFile.getManifest();
String location = manifest.getMainAttributes().getValue("Spring-Boot-Layers-Index");
ZipEntry entry = (location != null) ? jarFile.getEntry(location) : null;
if (entry != null) {
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
return new IndexedLayers(indexFile);

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.
@@ -50,20 +50,12 @@ class ContextTests {
.withMessage("Unable to find source JAR");
}
@Test
void createWhenSourceIsNotJarThrowsException() throws Exception {
File zip = new File(this.temp, "test.zip");
Files.createFile(zip.toPath());
assertThatIllegalStateException().isThrownBy(() -> new Context(zip, this.temp))
.withMessage("Unable to find source JAR");
}
@Test
void getJarFileReturnsJar() throws Exception {
File jar = new File(this.temp, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, this.temp);
assertThat(context.getJarFile()).isEqualTo(jar);
assertThat(context.getArchiveFile()).isEqualTo(jar);
}
@Test
@@ -82,7 +74,7 @@ class ContextTests {
File jar = new File(target, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, this.temp);
assertThat(context.getRelativeJarDir()).isEqualTo("target");
assertThat(context.getRelativeArchiveDir()).isEqualTo("target");
}
@Test
@@ -90,7 +82,7 @@ class ContextTests {
File jar = new File(this.temp, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, this.temp);
assertThat(context.getRelativeJarDir()).isNull();
assertThat(context.getRelativeArchiveDir()).isNull();
}
@Test
@@ -102,7 +94,7 @@ class ContextTests {
File jar = new File(directory1, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, directory2);
assertThat(context.getRelativeJarDir()).isNull();
assertThat(context.getRelativeArchiveDir()).isNull();
}
}

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.
@@ -69,7 +69,7 @@ class ExtractCommandTests {
@Test
void runExtractsLayers() throws Exception {
given(this.context.getJarFile()).willReturn(this.jarFile);
given(this.context.getArchiveFile()).willReturn(this.jarFile);
given(this.context.getWorkingDir()).willReturn(this.extract);
this.command.run(Collections.emptyMap(), Collections.emptyList());
assertThat(this.extract.list()).containsOnly("a", "b", "c", "d");
@@ -81,7 +81,7 @@ class ExtractCommandTests {
@Test
void runWhenHasDestinationOptionExtractsLayers() {
given(this.context.getJarFile()).willReturn(this.jarFile);
given(this.context.getArchiveFile()).willReturn(this.jarFile);
File out = new File(this.extract, "out");
this.command.run(Collections.singletonMap(ExtractCommand.DESTINATION_OPTION, out.getAbsolutePath()),
Collections.emptyList());
@@ -93,7 +93,7 @@ class ExtractCommandTests {
@Test
void runWhenHasLayerParamsExtractsLimitedLayers() {
given(this.context.getJarFile()).willReturn(this.jarFile);
given(this.context.getArchiveFile()).willReturn(this.jarFile);
given(this.context.getWorkingDir()).willReturn(this.extract);
this.command.run(Collections.emptyMap(), Arrays.asList("a", "c"));
assertThat(this.extract.list()).containsOnly("a", "c");
@@ -107,7 +107,7 @@ class ExtractCommandTests {
try (FileWriter writer = new FileWriter(file)) {
writer.write("text");
}
given(this.context.getJarFile()).willReturn(file);
given(this.context.getArchiveFile()).willReturn(file);
given(this.context.getWorkingDir()).willReturn(this.extract);
assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList()))

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.
@@ -52,7 +52,7 @@ class HelpCommandTests {
@BeforeEach
void setup() throws Exception {
Context context = mock(Context.class);
given(context.getJarFile()).willReturn(createJarFile("test.jar"));
given(context.getArchiveFile()).willReturn(createJarFile("test.jar"));
this.command = new HelpCommand(context, LayerToolsJarMode.Runner.getCommands(context));
this.out = new TestPrintStream(this);
}

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,10 +16,14 @@
package org.springframework.boot.jarmode.layertools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
@@ -37,6 +41,9 @@ import static org.mockito.Mockito.mock;
*/
class IndexedLayersTests {
@TempDir
File temp;
@Test
void createWhenIndexFileIsEmptyThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n "))
@@ -82,8 +89,20 @@ class IndexedLayersTests {
assertThat(layers.getLayer(mockEntry("a b/c d"))).isEqualTo("application");
}
@Test
void getShouldReturnIndexedLayersFromContext() throws Exception {
Context context = mock(Context.class);
given(context.getArchiveFile()).willReturn(createWarFile("test.war"));
IndexedLayers layers = IndexedLayers.get(context);
assertThat(layers.getLayer(mockEntry("WEB-INF/lib/a.jar"))).isEqualTo("test");
}
private String getIndex() throws Exception {
ClassPathResource resource = new ClassPathResource("test-layers.idx", getClass());
return getIndex("test-layers.idx");
}
private String getIndex(String fileName) throws Exception {
ClassPathResource resource = new ClassPathResource(fileName, getClass());
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
return FileCopyUtils.copyToString(reader);
}
@@ -94,4 +113,23 @@ class IndexedLayersTests {
return entry;
}
private File createWarFile(String name) throws Exception {
File file = new File(this.temp, name);
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) {
out.putNextEntry(new ZipEntry("WEB-INF/lib/a/"));
out.closeEntry();
out.putNextEntry(new ZipEntry("WEB-INF/lib/a/a.jar"));
out.closeEntry();
out.putNextEntry(new ZipEntry("WEB-INF/classes/Demo.class"));
out.closeEntry();
out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
out.write(getIndex("test-manifest.MF").getBytes());
out.closeEntry();
out.putNextEntry(new ZipEntry("WEB-INF/layers.idx"));
out.write(getIndex("test-war-layers.idx").getBytes());
out.closeEntry();
}
return file;
}
}

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.
@@ -55,7 +55,7 @@ class LayerToolsJarModeTests {
@BeforeEach
void setup() throws Exception {
Context context = mock(Context.class);
given(context.getJarFile()).willReturn(createJarFile("test.jar"));
given(context.getArchiveFile()).willReturn(createJarFile("test.jar"));
this.out = new TestPrintStream(this);
this.systemOut = System.out;
System.setOut(this.out);

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.
@@ -60,7 +60,7 @@ class ListCommandTests {
@BeforeEach
void setup() throws Exception {
this.jarFile = createJarFile("test.jar");
given(this.context.getJarFile()).willReturn(this.jarFile);
given(this.context.getArchiveFile()).willReturn(this.jarFile);
this.command = new ListCommand(this.context);
this.out = new TestPrintStream(this);
}

View File

@@ -0,0 +1,12 @@
Manifest-Version: 1.0
Created-By: Maven WAR Plugin 3.3.1
Build-Jdk-Spec: 11
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.example.DemoApplication
Spring-Boot-Version: 2.5.0-SNAPSHOT
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Spring-Boot-Classpath-Index: WEB-INF/classpath.idx
Spring-Boot-Layers-Index: WEB-INF/layers.idx

View File

@@ -0,0 +1,6 @@
- "test":
- "WEB-INF/lib/a.jar"
- "WEB-INF/lib/b.jar"
- "empty":
- "application":
- "WEB-INF/classes/Demo.class"