Polish loader and loader-tools

Polish and refactor `spring-boot-loader` and `spring-boot-loader-tools`
to make it easier to add indexing and layering support.

Closes gh-19766
This commit is contained in:
Phillip Webb 2020-01-15 21:11:07 -08:00
parent 56e30258fb
commit ad72f86bdb
11 changed files with 284 additions and 186 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -56,12 +56,14 @@ import org.apache.commons.compress.archivers.zip.UnixStat;
*/
public class JarWriter implements LoaderClassesWriter, AutoCloseable {
private static final UnpackHandler NEVER_UNPACK = new NeverUnpackHandler();
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
private static final int BUFFER_SIZE = 32 * 1024;
private static final int UNIX_FILE_MODE = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM;
private static final int UNIX_DIR_MODE = UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM;
private final JarArchiveOutputStream jarOutput;
private final Set<String> writtenEntries = new HashSet<>();
@ -121,11 +123,11 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
* @throws IOException if the entries cannot be written
*/
public void writeEntries(JarFile jarFile) throws IOException {
this.writeEntries(jarFile, new IdentityEntryTransformer(), NEVER_UNPACK);
this.writeEntries(jarFile, EntryTransformer.NONE, UnpackHandler.NEVER);
}
void writeEntries(JarFile jarFile, UnpackHandler unpackHandler) throws IOException {
this.writeEntries(jarFile, new IdentityEntryTransformer(), unpackHandler);
this.writeEntries(jarFile, EntryTransformer.NONE, unpackHandler);
}
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler)
@ -244,7 +246,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
}
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter) throws IOException {
writeEntry(entry, entryWriter, NEVER_UNPACK);
writeEntry(entry, entryWriter, UnpackHandler.NEVER);
}
/**
@ -257,22 +259,10 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
*/
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler)
throws IOException {
String parent = entry.getName();
if (parent.endsWith("/")) {
parent = parent.substring(0, parent.length() - 1);
entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
}
else {
entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
}
if (parent.lastIndexOf('/') != -1) {
parent = parent.substring(0, parent.lastIndexOf('/') + 1);
if (!parent.isEmpty()) {
writeEntry(new JarArchiveEntry(parent), null, unpackHandler);
}
}
if (this.writtenEntries.add(entry.getName())) {
String name = entry.getName();
writeParentFolderEntries(name);
if (this.writtenEntries.add(name)) {
entry.setUnixMode(name.endsWith("/") ? UNIX_DIR_MODE : UNIX_FILE_MODE);
entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler);
this.jarOutput.putArchiveEntry(entry);
if (entryWriter != null) {
@ -282,6 +272,16 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
}
}
private void writeParentFolderEntries(String name) throws IOException {
String parent = name.endsWith("/") ? name.substring(0, name.length() - 1) : name;
while (parent.lastIndexOf('/') != -1) {
parent = parent.substring(0, parent.lastIndexOf('/'));
if (!parent.isEmpty()) {
writeEntry(new JarArchiveEntry(parent + "/"), null, UnpackHandler.NEVER);
}
}
}
private EntryWriter addUnpackCommentIfNecessary(JarArchiveEntry entry, EntryWriter entryWriter,
UnpackHandler unpackHandler) throws IOException {
if (entryWriter == null || !unpackHandler.requiresUnpack(entry.getName())) {
@ -444,50 +444,44 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
* An {@code EntryTransformer} enables the transformation of {@link JarEntry jar
* entries} during the writing process.
*/
@FunctionalInterface
interface EntryTransformer {
/**
* No-op entity transformer.
*/
EntryTransformer NONE = (jarEntry) -> jarEntry;
JarArchiveEntry transform(JarArchiveEntry jarEntry);
}
/**
* An {@code EntryTransformer} that returns the entry unchanged.
*/
private static final class IdentityEntryTransformer implements EntryTransformer {
@Override
public JarArchiveEntry transform(JarArchiveEntry jarEntry) {
return jarEntry;
}
}
/**
* An {@code UnpackHandler} determines whether or not unpacking is required and
* provides a SHA1 hash if required.
*/
interface UnpackHandler {
UnpackHandler NEVER = new UnpackHandler() {
@Override
public boolean requiresUnpack(String name) {
return false;
}
@Override
public String sha1Hash(String name) throws IOException {
throw new UnsupportedOperationException();
}
};
boolean requiresUnpack(String name);
String sha1Hash(String name) throws IOException;
}
private static final class NeverUnpackHandler implements UnpackHandler {
@Override
public boolean requiresUnpack(String name) {
return false;
}
@Override
public String sha1Hash(String name) {
throw new UnsupportedOperationException();
}
}
private static final class LibraryUnpackHandler implements UnpackHandler {
private final Library library;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -35,13 +35,26 @@ public interface Layout {
*/
String getLauncherClassName();
/**
* Returns the destination path for a given library.
* @param libraryName the name of the library (excluding any path)
* @param scope the scope of the library
* @return the location of the library relative to the root of the archive (should end
* with '/') or {@code null} if the library should not be included.
*/
default String getLibraryLocation(String libraryName, LibraryScope scope) {
return getLibraryDestination(libraryName, scope);
}
/**
* Returns the destination path for a given library.
* @param libraryName the name of the library (excluding any path)
* @param scope the scope of the library
* @return the destination relative to the root of the archive (should end with '/')
* or {@code null} if the library should not be included.
* @deprecated since 2.3.0 in favor of {@link #getLibraryLocation}
*/
@Deprecated
String getLibraryDestination(String libraryName, LibraryScope scope);
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -67,6 +67,12 @@ public final class Layouts {
return "org.springframework.boot.loader.JarLauncher";
}
@Override
public String getLibraryLocation(String libraryName, LibraryScope scope) {
return "BOOT-INF/lib/";
}
@Deprecated
@Override
public String getLibraryDestination(String libraryName, LibraryScope scope) {
return "BOOT-INF/lib/";
@ -123,15 +129,15 @@ public final class Layouts {
*/
public static class War implements Layout {
private static final Map<LibraryScope, String> SCOPE_DESTINATIONS;
private static final Map<LibraryScope, String> SCOPE_LOCATION;
static {
Map<LibraryScope, String> map = new HashMap<>();
map.put(LibraryScope.COMPILE, "WEB-INF/lib/");
map.put(LibraryScope.CUSTOM, "WEB-INF/lib/");
map.put(LibraryScope.RUNTIME, "WEB-INF/lib/");
map.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/");
SCOPE_DESTINATIONS = Collections.unmodifiableMap(map);
Map<LibraryScope, String> locations = new HashMap<>();
locations.put(LibraryScope.COMPILE, "WEB-INF/lib/");
locations.put(LibraryScope.CUSTOM, "WEB-INF/lib/");
locations.put(LibraryScope.RUNTIME, "WEB-INF/lib/");
locations.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/");
SCOPE_LOCATION = Collections.unmodifiableMap(locations);
}
@Override
@ -139,9 +145,15 @@ public final class Layouts {
return "org.springframework.boot.loader.WarLauncher";
}
@Override
public String getLibraryLocation(String libraryName, LibraryScope scope) {
return SCOPE_LOCATION.get(scope);
}
@Deprecated
@Override
public String getLibraryDestination(String libraryName, LibraryScope scope) {
return SCOPE_DESTINATIONS.get(scope);
return SCOPE_LOCATION.get(scope);
}
@Override

View File

@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@ -54,10 +55,10 @@ public class Repackager {
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
@ -81,13 +82,9 @@ public class Repackager {
}
public Repackager(File source, LayoutFactory layoutFactory) {
if (source == null) {
throw new IllegalArgumentException("Source file must be provided");
}
if (!source.exists() || !source.isFile()) {
throw new IllegalArgumentException(
"Source must refer to an existing file, got " + source.getAbsolutePath());
}
Assert.notNull(source, "Source file must be provided");
Assert.isTrue(source.exists() && source.isFile(),
"Source must refer to an existing file, got " + source.getAbsolutePath());
this.source = source.getAbsoluteFile();
this.layoutFactory = layoutFactory;
}
@ -124,9 +121,7 @@ public class Repackager {
* @param layout the layout
*/
public void setLayout(Layout layout) {
if (layout == null) {
throw new IllegalArgumentException("Layout must not be null");
}
Assert.notNull(layout, "Layout must not be null");
this.layout = layout;
}
@ -169,12 +164,8 @@ public class Repackager {
* @since 1.3.0
*/
public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException {
if (destination == null || destination.isDirectory()) {
throw new IllegalArgumentException("Invalid destination");
}
if (libraries == null) {
throw new IllegalArgumentException("Libraries must not be null");
}
Assert.isTrue(destination != null && !destination.isDirectory(), "Invalid destination");
Assert.notNull(libraries, "Libraries must not be null");
if (this.layout == null) {
this.layout = getLayoutFactory().getLayout(this.source);
}
@ -234,14 +225,7 @@ public class Repackager {
try (JarWriter writer = new JarWriter(destination, launchScript)) {
writer.writeManifest(buildManifest(sourceJar));
writeLoaderClasses(writer);
if (this.layout instanceof RepackagingLayout) {
writer.writeEntries(sourceJar,
new RenamingEntryTransformer(((RepackagingLayout) this.layout).getRepackagedClassesLocation()),
writeableLibraries);
}
else {
writer.writeEntries(sourceJar, writeableLibraries);
}
writer.writeEntries(sourceJar, getEntityTransformer(), writeableLibraries);
writeableLibraries.write(writer);
}
}
@ -255,6 +239,13 @@ public class Repackager {
}
}
private EntryTransformer getEntityTransformer() {
if (this.layout instanceof RepackagingLayout) {
return new RepackagingEntryTransformer((RepackagingLayout) this.layout);
}
return EntryTransformer.NONE;
}
private boolean isZip(File file) {
try {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
@ -276,41 +267,45 @@ public class Repackager {
}
private Manifest buildManifest(JarFile source) throws IOException {
Manifest manifest = source.getManifest();
if (manifest == null) {
manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
}
manifest = new Manifest(manifest);
String startClass = this.mainClass;
if (startClass == null) {
startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
}
if (startClass == null) {
startClass = findMainMethodWithTimeoutWarning(source);
}
String launcherClassName = this.layout.getLauncherClassName();
if (launcherClassName != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClassName);
if (startClass == null) {
throw new IllegalStateException("Unable to find main class");
}
manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass);
}
else if (startClass != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass);
}
String bootVersion = getClass().getPackage().getImplementationVersion();
manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion);
manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE, (this.layout instanceof RepackagingLayout)
? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() : this.layout.getClassesLocation());
String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE);
if (StringUtils.hasLength(lib)) {
manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib);
}
Manifest manifest = createInitialManifest(source);
addMainAndStartAttributes(source, manifest);
addBootAttributes(manifest.getMainAttributes());
return manifest;
}
private Manifest createInitialManifest(JarFile source) throws IOException {
if (source.getManifest() != null) {
return new Manifest(source.getManifest());
}
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
return manifest;
}
private void addMainAndStartAttributes(JarFile source, Manifest manifest) throws IOException {
String mainClass = getMainClass(source, manifest);
String launcherClass = this.layout.getLauncherClassName();
if (launcherClass != null) {
Assert.state(mainClass != null, "Unable to find main class");
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClass);
manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, mainClass);
}
else if (mainClass != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, mainClass);
}
}
private String getMainClass(JarFile source, Manifest manifest) throws IOException {
if (this.mainClass != null) {
return this.mainClass;
}
String attributeValue = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
if (attributeValue != null) {
return attributeValue;
}
return findMainMethodWithTimeoutWarning(source);
}
private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException {
long startTime = System.currentTimeMillis();
String mainMethod = findMainMethod(source);
@ -328,6 +323,32 @@ public class Repackager {
SPRING_BOOT_APPLICATION_CLASS_NAME);
}
private void addBootAttributes(Attributes attributes) {
attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion());
if (this.layout instanceof RepackagingLayout) {
addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) this.layout);
}
else {
addBootBootAttributesForPlainLayout(attributes, this.layout);
}
}
private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation());
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
}
private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, this.layout.getClassesLocation());
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
}
private void putIfHasLength(Attributes attributes, String name, String value) {
if (StringUtils.hasLength(value)) {
attributes.putValue(name, value);
}
}
private void renameFile(File file, File dest) {
if (!file.renameTo(dest)) {
throw new IllegalStateException("Unable to rename '" + file + "' to '" + dest + "'");
@ -359,12 +380,12 @@ public class Repackager {
/**
* An {@code EntryTransformer} that renames entries by applying a prefix.
*/
private static final class RenamingEntryTransformer implements EntryTransformer {
private static final class RepackagingEntryTransformer implements EntryTransformer {
private final String namePrefix;
private final RepackagingLayout layout;
private RenamingEntryTransformer(String namePrefix) {
this.namePrefix = namePrefix;
private RepackagingEntryTransformer(RepackagingLayout layout) {
this.layout = layout;
}
@Override
@ -372,33 +393,40 @@ public class Repackager {
if (entry.getName().equals("META-INF/INDEX.LIST")) {
return null;
}
if ((entry.getName().startsWith("META-INF/") && !entry.getName().equals("META-INF/aop.xml")
&& !entry.getName().endsWith(".kotlin_module")) || entry.getName().startsWith("BOOT-INF/")
|| entry.getName().equals("module-info.class")) {
if (!isTransformable(entry)) {
return entry;
}
JarArchiveEntry renamedEntry = new JarArchiveEntry(this.namePrefix + entry.getName());
renamedEntry.setTime(entry.getTime());
renamedEntry.setSize(entry.getSize());
renamedEntry.setMethod(entry.getMethod());
String transformedName = this.layout.getRepackagedClassesLocation() + entry.getName();
JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName);
transformedEntry.setTime(entry.getTime());
transformedEntry.setSize(entry.getSize());
transformedEntry.setMethod(entry.getMethod());
if (entry.getComment() != null) {
renamedEntry.setComment(entry.getComment());
transformedEntry.setComment(entry.getComment());
}
renamedEntry.setCompressedSize(entry.getCompressedSize());
renamedEntry.setCrc(entry.getCrc());
transformedEntry.setCompressedSize(entry.getCompressedSize());
transformedEntry.setCrc(entry.getCrc());
if (entry.getCreationTime() != null) {
renamedEntry.setCreationTime(entry.getCreationTime());
transformedEntry.setCreationTime(entry.getCreationTime());
}
if (entry.getExtra() != null) {
renamedEntry.setExtra(entry.getExtra());
transformedEntry.setExtra(entry.getExtra());
}
if (entry.getLastAccessTime() != null) {
renamedEntry.setLastAccessTime(entry.getLastAccessTime());
transformedEntry.setLastAccessTime(entry.getLastAccessTime());
}
if (entry.getLastModifiedTime() != null) {
renamedEntry.setLastModifiedTime(entry.getLastModifiedTime());
transformedEntry.setLastModifiedTime(entry.getLastModifiedTime());
}
return renamedEntry;
return transformedEntry;
}
private boolean isTransformable(JarArchiveEntry entry) {
String name = entry.getName();
if (name.startsWith("META-INF/")) {
return name.equals("META-INF/aop.xml") || name.endsWith(".kotlin_module");
}
return !name.startsWith("BOOT-INF/") && !name.equals("module-info.class");
}
}
@ -414,19 +442,19 @@ public class Repackager {
private WritableLibraries(Libraries libraries) throws IOException {
libraries.doWithLibraries((library) -> {
if (isZip(library.getFile())) {
String libraryDestination = Repackager.this.layout.getLibraryDestination(library.getName(),
library.getScope());
if (libraryDestination != null) {
Library existing = this.libraryEntryNames.putIfAbsent(libraryDestination + library.getName(),
library);
if (existing != null) {
throw new IllegalStateException("Duplicate library " + library.getName());
}
String location = getLocation(library);
if (location != null) {
Library existing = this.libraryEntryNames.putIfAbsent(location + library.getName(), library);
Assert.state(existing == null, "Duplicate library " + library.getName());
}
}
});
}
private String getLocation(Library library) {
return Repackager.this.layout.getLibraryLocation(library.getName(), library.getScope());
}
@Override
public boolean requiresUnpack(String name) {
Library library = this.libraryEntryNames.get(name);
@ -436,9 +464,7 @@ public class Repackager {
@Override
public String sha1Hash(String name) throws IOException {
Library library = this.libraryEntryNames.get(name);
if (library == null) {
throw new IllegalArgumentException("No library found for entry name '" + name + "'");
}
Assert.notNull(library, "No library found for entry name '" + name + "'");
return FileUtils.sha1Hash(library.getFile());
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -56,19 +56,19 @@ class LayoutsTests {
@Test
void jarLayout() {
Layout layout = new Layouts.Jar();
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE)).isEqualTo("BOOT-INF/lib/");
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM)).isEqualTo("BOOT-INF/lib/");
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED)).isEqualTo("BOOT-INF/lib/");
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME)).isEqualTo("BOOT-INF/lib/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.COMPILE)).isEqualTo("BOOT-INF/lib/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.CUSTOM)).isEqualTo("BOOT-INF/lib/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.PROVIDED)).isEqualTo("BOOT-INF/lib/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.RUNTIME)).isEqualTo("BOOT-INF/lib/");
}
@Test
void warLayout() {
Layout layout = new Layouts.War();
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE)).isEqualTo("WEB-INF/lib/");
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM)).isEqualTo("WEB-INF/lib/");
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED)).isEqualTo("WEB-INF/lib-provided/");
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME)).isEqualTo("WEB-INF/lib/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.COMPILE)).isEqualTo("WEB-INF/lib/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.CUSTOM)).isEqualTo("WEB-INF/lib/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.PROVIDED)).isEqualTo("WEB-INF/lib-provided/");
assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.RUNTIME)).isEqualTo("WEB-INF/lib/");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -324,8 +324,8 @@ class RepackagerTests {
Layout layout = mock(Layout.class);
LibraryScope scope = mock(LibraryScope.class);
given(layout.getLauncherClassName()).willReturn("testLauncher");
given(layout.getLibraryDestination(anyString(), eq(scope))).willReturn("test/");
given(layout.getLibraryDestination(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/");
given(layout.getLibraryLocation(anyString(), eq(scope))).willReturn("test/");
given(layout.getLibraryLocation(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/");
repackager.setLayout(layout);
repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope)));
assertThat(hasEntry(file, "test/" + libJarFile.getName())).isTrue();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -32,6 +32,8 @@ import org.springframework.boot.loader.archive.Archive;
*/
public abstract class ExecutableArchiveLauncher extends Launcher {
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
private final Archive archive;
public ExecutableArchiveLauncher() {
@ -44,11 +46,12 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
}
protected ExecutableArchiveLauncher(Archive archive) {
this.archive = archive;
}
protected final Archive getArchive() {
return this.archive;
try {
this.archive = archive;
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
@Override
@ -56,7 +59,7 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue("Start-Class");
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
@ -125,4 +128,12 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
return this.archive.supportsNestedJars();
}
/**
* Return the root archive.
* @return the root archive
*/
protected final Archive getArchive() {
return this.archive;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -77,7 +77,9 @@ public abstract class Launcher {
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(50);
while (archives.hasNext()) {
urls.add(archives.next().getUrl());
Archive archive = archives.next();
urls.add(archive.getUrl());
archive.close();
}
return createClassLoader(urls.toArray(new URL[0]));
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -23,7 +23,7 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
@ -53,7 +53,15 @@ public abstract class AbstractExecutableArchiveLauncherTests {
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/"));
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/"));
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/"));
JarEntry libFoo = new JarEntry(entryPrefix + "/lib/foo.jar");
addNestedJars(entryPrefix, "/lib/foo.jar", jarOutputStream);
addNestedJars(entryPrefix, "/lib/bar.jar", jarOutputStream);
addNestedJars(entryPrefix, "/lib/baz.jar", jarOutputStream);
jarOutputStream.close();
return archive;
}
private void addNestedJars(String entryPrefix, String lib, JarOutputStream jarOutputStream) throws IOException {
JarEntry libFoo = new JarEntry(entryPrefix + lib);
libFoo.setMethod(ZipEntry.STORED);
ByteArrayOutputStream fooJarStream = new ByteArrayOutputStream();
new JarOutputStream(fooJarStream).close();
@ -63,8 +71,6 @@ public abstract class AbstractExecutableArchiveLauncherTests {
libFoo.setCrc(crc32.getValue());
jarOutputStream.putNextEntry(libFoo);
jarOutputStream.write(fooJarStream.toByteArray());
jarOutputStream.close();
return archive;
}
protected File explode(File archive) throws IOException {
@ -87,11 +93,20 @@ public abstract class AbstractExecutableArchiveLauncherTests {
}
protected Set<URL> getUrls(List<Archive> archives) throws MalformedURLException {
Set<URL> urls = new HashSet<>(archives.size());
Set<URL> urls = new LinkedHashSet<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return urls;
}
protected final URL toUrl(File file) {
try {
return file.toURI().toURL();
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -42,9 +42,7 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "BOOT-INF/classes").toURI().toURL(),
new File(explodedRoot, "BOOT-INF/lib/foo.jar").toURI().toURL());
assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
for (Archive archive : archives) {
archive.close();
}
@ -57,14 +55,29 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
JarLauncher launcher = new JarLauncher(archive);
List<Archive> classPathArchives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add);
assertThat(classPathArchives).hasSize(2);
assertThat(classPathArchives).hasSize(4);
assertThat(getUrls(classPathArchives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"));
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/"));
for (Archive classPathArchive : classPathArchives) {
classPathArchive.close();
}
}
}
protected final URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}
protected final List<File> getExpectedFiles(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "BOOT-INF/classes"));
expected.add(new File(parent, "BOOT-INF/lib/foo.jar"));
expected.add(new File(parent, "BOOT-INF/lib/bar.jar"));
expected.add(new File(parent, "BOOT-INF/lib/baz.jar"));
return expected;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -42,29 +42,41 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "WEB-INF/classes").toURI().toURL(),
new File(explodedRoot, "WEB-INF/lib/foo.jar").toURI().toURL());
assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
for (Archive archive : archives) {
archive.close();
}
}
@Test
void archivedWarHasOnlyWebInfClassesAndContentsOWebInfLibOnClasspath() throws Exception {
void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception {
File jarRoot = createJarArchive("archive.war", "WEB-INF");
try (JarFileArchive archive = new JarFileArchive(jarRoot)) {
WarLauncher launcher = new WarLauncher(archive);
List<Archive> classPathArchives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add);
assertThat(classPathArchives).hasSize(2);
assertThat(getUrls(classPathArchives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"));
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/"));
for (Archive classPathArchive : classPathArchives) {
classPathArchive.close();
}
}
}
protected final URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}
protected final List<File> getExpectedFiles(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "WEB-INF/classes"));
expected.add(new File(parent, "WEB-INF/lib/foo.jar"));
expected.add(new File(parent, "WEB-INF/lib/bar.jar"));
expected.add(new File(parent, "WEB-INF/lib/baz.jar"));
return expected;
}
}