Optimize JarLauncher when used with exploded jar

- Previously, we would create a JarFileArchive for all nested jars.
This was an additional overhead. We only need to create a JarFileArchive
for jars that can have nested jars in them. For all other jars we only need
the URL to build the classpath.
- While iterating over nested entries in the exploded jar, we only need to
look at BOOT-INF and we can skip any entry that does not match that.

Closes gh-16655

Co-authored-by: Phillip Webb <pwebb@pivotal.io>
pull/19364/head
Madhura Bhave 5 years ago
parent 58022d72f5
commit 8f5777cf9e

@ -17,8 +17,8 @@
package org.springframework.boot.loader; package org.springframework.boot.loader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive;
@ -65,27 +65,64 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
} }
@Override @Override
protected List<Archive> getClassPathArchives() throws Exception { protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive)); Iterator<Archive> archives = this.archive.getNestedArchives(this::isSearchCandidate, this::isNestedArchive);
postProcessClassPathArchives(archives); if (isPostProcessingClassPathArchives()) {
archives = applyClassPathArchivePostProcessing(archives);
}
return archives; return archives;
} }
private Iterator<Archive> applyClassPathArchivePostProcessing(Iterator<Archive> archives) throws Exception {
List<Archive> list = new ArrayList<Archive>();
while (archives.hasNext()) {
list.add(archives.next());
}
postProcessClassPathArchives(list);
return list.iterator();
}
/**
* Determine if the specified entry is a a candidate for further searching.
* @param entry the entry to check
* @return {@code true} if the entry is a candidate for further searching
*/
protected boolean isSearchCandidate(Archive.Entry entry) {
return true;
}
/** /**
* Determine if the specified {@link JarEntry} is a nested item that should be added * Determine if the specified entry is a nested item that should be added to the
* to the classpath. The method is called once for each entry. * classpath.
* @param entry the jar entry * @param entry the entry to check
* @return {@code true} if the entry is a nested item (jar or folder) * @return {@code true} if the entry is a nested item (jar or folder)
*/ */
protected abstract boolean isNestedArchive(Archive.Entry entry); protected abstract boolean isNestedArchive(Archive.Entry entry);
/**
* Return if post processing needs to be applied to the archives. For back
* compatibility this method returns {@true}, but subclasses that don't override
* {@link #postProcessClassPathArchives(List)} should provide an implementation that
* returns {@code false}.
* @return if the {@link #postProcessClassPathArchives(List)} method is implemented
*/
protected boolean isPostProcessingClassPathArchives() {
return true;
}
/** /**
* Called to post-process archive entries before they are used. Implementations can * Called to post-process archive entries before they are used. Implementations can
* add and remove entries. * add and remove entries.
* @param archives the archives * @param archives the archives
* @throws Exception if the post processing fails * @throws Exception if the post processing fails
* @see #isPostProcessingClassPathArchives()
*/ */
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception { protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
} }
@Override
protected boolean supportsNestedJars() {
return this.archive.supportsNestedJars();
}
} }

@ -17,6 +17,7 @@
package org.springframework.boot.loader; package org.springframework.boot.loader;
import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.EntryFilter;
/** /**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
@ -29,9 +30,12 @@ import org.springframework.boot.loader.archive.Archive;
*/ */
public class JarLauncher extends ExecutableArchiveLauncher { public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
if (entry.isDirectory()) {
static final String BOOT_INF_LIB = "BOOT-INF/lib/"; return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
};
public JarLauncher() { public JarLauncher() {
} }
@ -40,12 +44,19 @@ public class JarLauncher extends ExecutableArchiveLauncher {
super(archive); super(archive);
} }
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
@Override @Override
protected boolean isNestedArchive(Archive.Entry entry) { protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) { return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
} }
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {

@ -19,9 +19,11 @@ package org.springframework.boot.loader;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource; import java.security.CodeSource;
import java.security.ProtectionDomain; import java.security.ProtectionDomain;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; import java.util.List;
import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive;
@ -46,8 +48,10 @@ public abstract class Launcher {
* @throws Exception if the application fails to launch * @throws Exception if the application fails to launch
*/ */
protected void launch(String[] args) throws Exception { protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler(); if (supportsNestedJars()) {
ClassLoader classLoader = createClassLoader(getClassPathArchives()); JarFile.registerUrlProtocolHandler();
}
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
launch(args, getMainClass(), classLoader); launch(args, getMainClass(), classLoader);
} }
@ -56,11 +60,24 @@ public abstract class Launcher {
* @param archives the archives * @param archives the archives
* @return the classloader * @return the classloader
* @throws Exception if the classloader cannot be created * @throws Exception if the classloader cannot be created
* @deprecated since 2.3.0 in favor of {@link #createClassLoader(Iterator)}
*/ */
@Deprecated
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size()); return createClassLoader(archives.iterator());
for (Archive archive : archives) { }
urls.add(archive.getUrl());
/**
* Create a classloader for the specified archives.
* @param archives the archives
* @return the classloader
* @throws Exception if the classloader cannot be created
* @since 2.3.0
*/
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(50);
while (archives.hasNext()) {
urls.add(archives.next().getUrl());
} }
return createClassLoader(urls.toArray(new URL[0])); return createClassLoader(urls.toArray(new URL[0]));
} }
@ -72,7 +89,10 @@ public abstract class Launcher {
* @throws Exception if the classloader cannot be created * @throws Exception if the classloader cannot be created
*/ */
protected ClassLoader createClassLoader(URL[] urls) throws Exception { protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); if (supportsNestedJars()) {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
return new URLClassLoader(urls, getClass().getClassLoader());
} }
/** /**
@ -109,8 +129,23 @@ public abstract class Launcher {
* Returns the archives that will be used to construct the class path. * Returns the archives that will be used to construct the class path.
* @return the class path archives * @return the class path archives
* @throws Exception if the class path archives cannot be obtained * @throws Exception if the class path archives cannot be obtained
* @since 2.3.0
*/
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
return getClassPathArchives().iterator();
}
/**
* Returns the archives that will be used to construct the class path.
* @return the class path archives
* @throws Exception if the class path archives cannot be obtained
* @deprecated since 2.3.0 in favor of implementing
* {@link #getClassPathArchivesIterator()}.
*/ */
protected abstract List<Archive> getClassPathArchives() throws Exception; @Deprecated
protected List<Archive> getClassPathArchives() throws Exception {
throw new IllegalStateException("Unexpected call to getClassPathArchives()");
}
protected final Archive createArchive() throws Exception { protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain(); ProtectionDomain protectionDomain = getClass().getProtectionDomain();
@ -127,4 +162,14 @@ public abstract class Launcher {
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
} }
/**
* Returns if the launcher needs to support fully nested JARs. If this method returns
* {@code false} then only regular JARs are supported and the additional URL and
* ClassLoader support infrastructure will not be installed.
* @return if nested JARs are supported
*/
protected boolean supportsNestedJars() {
return true;
}
} }

@ -28,6 +28,7 @@ import java.net.URLConnection;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -447,12 +448,12 @@ public class PropertiesLauncher extends Launcher {
} }
@Override @Override
protected List<Archive> getClassPathArchives() throws Exception { protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
List<Archive> lib = new ArrayList<>(); List<Archive> lib = new ArrayList<>();
for (String path : this.paths) { for (String path : this.paths) {
for (Archive archive : getClassPathArchives(path)) { for (Archive archive : getClassPathArchives(path)) {
if (archive instanceof ExplodedArchive) { if (archive instanceof ExplodedArchive) {
List<Archive> nested = new ArrayList<>(archive.getNestedArchives(new ArchiveEntryFilter())); List<Archive> nested = asList(archive.getNestedArchives(null, new ArchiveEntryFilter()));
nested.add(0, archive); nested.add(0, archive);
lib.addAll(nested); lib.addAll(nested);
} }
@ -462,7 +463,7 @@ public class PropertiesLauncher extends Launcher {
} }
} }
addNestedEntries(lib); addNestedEntries(lib);
return lib; return lib.iterator();
} }
private List<Archive> getClassPathArchives(String path) throws Exception { private List<Archive> getClassPathArchives(String path) throws Exception {
@ -543,7 +544,7 @@ public class PropertiesLauncher extends Launcher {
root = ""; root = "";
} }
EntryFilter filter = new PrefixMatchingArchiveFilter(root); EntryFilter filter = new PrefixMatchingArchiveFilter(root);
List<Archive> archives = new ArrayList<>(parent.getNestedArchives(filter)); List<Archive> archives = asList(parent.getNestedArchives(null, filter));
if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar") && parent != this.parent) { if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar") && parent != this.parent) {
// You can't find the root with an entry filter so it has to be added // You can't find the root with an entry filter so it has to be added
// explicitly. But don't add the root of the parent archive. // explicitly. But don't add the root of the parent archive.
@ -557,12 +558,10 @@ public class PropertiesLauncher extends Launcher {
// directories, meaning we are running from an executable JAR. We add nested // directories, meaning we are running from an executable JAR. We add nested
// entries from there with low priority (i.e. at end). // entries from there with low priority (i.e. at end).
try { try {
lib.addAll(this.parent.getNestedArchives((entry) -> { Iterator<Archive> archives = this.parent.getNestedArchives(null, JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER);
if (entry.isDirectory()) { while (archives.hasNext()) {
return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES); lib.add(archives.next());
} }
return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB);
}));
} }
catch (IOException ex) { catch (IOException ex) {
// Ignore // Ignore
@ -591,6 +590,14 @@ public class PropertiesLauncher extends Launcher {
return path; return path;
} }
private List<Archive> asList(Iterator<Archive> iterator) {
List<Archive> list = new ArrayList<Archive>();
while (iterator.hasNext()) {
list.add(iterator.next());
}
return list;
}
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
args = launcher.getArgs(args); args = launcher.getArgs(args);

@ -17,6 +17,7 @@
package org.springframework.boot.loader; package org.springframework.boot.loader;
import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry;
/** /**
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives. * {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
@ -29,14 +30,6 @@ import org.springframework.boot.loader.archive.Archive;
*/ */
public class WarLauncher extends ExecutableArchiveLauncher { public class WarLauncher extends ExecutableArchiveLauncher {
private static final String WEB_INF = "WEB-INF/";
private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
private static final String WEB_INF_LIB = WEB_INF + "lib/";
private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
public WarLauncher() { public WarLauncher() {
} }
@ -44,14 +37,22 @@ public class WarLauncher extends ExecutableArchiveLauncher {
super(archive); super(archive);
} }
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Entry entry) {
return entry.getName().startsWith("WEB-INF/");
}
@Override @Override
public boolean isNestedArchive(Archive.Entry entry) { public boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) { if (entry.isDirectory()) {
return entry.getName().equals(WEB_INF_CLASSES); return entry.getName().equals("WEB-INF/classes/");
}
else {
return entry.getName().startsWith(WEB_INF_LIB) || entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
} }
return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/");
} }
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {

@ -19,6 +19,7 @@ package org.springframework.boot.loader.archive;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.jar.Manifest; import java.util.jar.Manifest;
@ -47,13 +48,44 @@ public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
*/ */
Manifest getManifest() throws IOException; Manifest getManifest() throws IOException;
/**
* Returns nested {@link Archive}s for entries that match the specified filters.
* @param searchFilter filter used to limit when additional sub-entry searching is
* required or {@code null} if all entries should be considered.
* @param includeFilter filter used to determine which entries should be included in
* the result or {@code null} if all entries should be included
* @return the nested archives
* @throws IOException on IO error
* @since 2.3.0
*/
default Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter)
throws IOException {
EntryFilter combinedFilter = (entry) -> (searchFilter == null || searchFilter.matches(entry))
&& (includeFilter == null || includeFilter.matches(entry));
List<Archive> nestedArchives = getNestedArchives(combinedFilter);
return nestedArchives.iterator();
}
/** /**
* Returns nested {@link Archive}s for entries that match the specified filter. * Returns nested {@link Archive}s for entries that match the specified filter.
* @param filter the filter used to limit entries * @param filter the filter used to limit entries
* @return nested archives * @return nested archives
* @throws IOException if nested archives cannot be read * @throws IOException if nested archives cannot be read
* @deprecated since 2.3.0 in favor of
* {@link #getNestedArchives(EntryFilter, EntryFilter)}
*/ */
List<Archive> getNestedArchives(EntryFilter filter) throws IOException; @Deprecated
default List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
throw new IllegalStateException("Unexpected call to getNestedArchives(filter)");
}
@Deprecated
@Override
Iterator<Entry> iterator();
default boolean supportsNestedJars() {
return true;
}
/** /**
* Closes the {@code Archive}, releasing any open resources. * Closes the {@code Archive}, releasing any open resources.
@ -87,6 +119,7 @@ public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
/** /**
* Strategy interface to filter {@link Entry Entries}. * Strategy interface to filter {@link Entry Entries}.
*/ */
@FunctionalInterface
interface EntryFilter { interface EntryFilter {
/** /**

@ -20,8 +20,8 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -29,7 +29,6 @@ import java.util.Deque;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import java.util.jar.Manifest; import java.util.jar.Manifest;
@ -39,6 +38,7 @@ import java.util.jar.Manifest;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Madhura Bhave
* @since 1.0.0 * @since 1.0.0
*/ */
public class ExplodedArchive implements Archive { public class ExplodedArchive implements Archive {
@ -99,24 +99,23 @@ public class ExplodedArchive implements Archive {
} }
@Override @Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException { public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>(); return new ArchiveIterator(this.root, this.recursive, searchFilter, includeFilter);
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
} }
@Override @Override
public Iterator<Entry> iterator() { public Iterator<Entry> iterator() {
return new FileEntryIterator(this.root, this.recursive); return new EntryIterator(this.root, this.recursive, null, null);
} }
protected Archive getNestedArchive(Entry entry) throws IOException { protected Archive getNestedArchive(Entry entry) throws IOException {
File file = ((FileEntry) entry).getFile(); File file = ((FileEntry) entry).getFile();
return (file.isDirectory() ? new ExplodedArchive(file) : new JarFileArchive(file)); return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive((FileEntry) entry));
}
@Override
public boolean supportsNestedJars() {
return false;
} }
@Override @Override
@ -132,21 +131,30 @@ public class ExplodedArchive implements Archive {
/** /**
* File based {@link Entry} {@link Iterator}. * File based {@link Entry} {@link Iterator}.
*/ */
private static class FileEntryIterator implements Iterator<Entry> { private abstract static class AbstractIterator<T> implements Iterator<T> {
private final Comparator<File> entryComparator = new EntryComparator(); private static final Comparator<File> entryComparator = Comparator.comparing(File::getAbsolutePath);
private final File root; private final File root;
private final boolean recursive; private final boolean recursive;
private final EntryFilter searchFilter;
private final EntryFilter includeFilter;
private final Deque<Iterator<File>> stack = new LinkedList<>(); private final Deque<Iterator<File>> stack = new LinkedList<>();
private File current; private FileEntry current;
private String rootUrl;
FileEntryIterator(File root, boolean recursive) { AbstractIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
this.root = root; this.root = root;
this.rootUrl = this.root.toURI().getPath();
this.recursive = recursive; this.recursive = recursive;
this.searchFilter = searchFilter;
this.includeFilter = includeFilter;
this.stack.add(listFiles(root)); this.stack.add(listFiles(root));
this.current = poll(); this.current = poll();
} }
@ -157,34 +165,28 @@ public class ExplodedArchive implements Archive {
} }
@Override @Override
public Entry next() { public T next() {
if (this.current == null) { FileEntry entry = this.current;
if (entry == null) {
throw new NoSuchElementException(); throw new NoSuchElementException();
} }
File file = this.current;
if (file.isDirectory() && (this.recursive || file.getParentFile().equals(this.root))) {
this.stack.addFirst(listFiles(file));
}
this.current = poll(); this.current = poll();
String name = file.toURI().getPath().substring(this.root.toURI().getPath().length()); return adapt(entry);
return new FileEntry(name, file);
} }
private Iterator<File> listFiles(File file) { private FileEntry poll() {
File[] files = file.listFiles();
if (files == null) {
return Collections.emptyIterator();
}
Arrays.sort(files, this.entryComparator);
return Arrays.asList(files).iterator();
}
private File poll() {
while (!this.stack.isEmpty()) { while (!this.stack.isEmpty()) {
while (this.stack.peek().hasNext()) { while (this.stack.peek().hasNext()) {
File file = this.stack.peek().next(); File file = this.stack.peek().next();
if (!SKIPPED_NAMES.contains(file.getName())) { if (SKIPPED_NAMES.contains(file.getName())) {
return file; continue;
}
FileEntry entry = getFileEntry(file);
if (isListable(entry)) {
this.stack.addFirst(listFiles(file));
}
if (this.includeFilter == null || this.includeFilter.matches(entry)) {
return entry;
} }
} }
this.stack.poll(); this.stack.poll();
@ -192,21 +194,64 @@ public class ExplodedArchive implements Archive {
return null; return null;
} }
private FileEntry getFileEntry(File file) {
URI uri = file.toURI();
String name = uri.getPath().substring(this.rootUrl.length());
try {
return new FileEntry(name, file, uri.toURL());
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
private boolean isListable(FileEntry entry) {
return entry.isDirectory() && (this.recursive || entry.getFile().getParentFile().equals(this.root))
&& (this.searchFilter == null || this.searchFilter.matches(entry))
&& (this.includeFilter == null || !this.includeFilter.matches(entry));
}
private Iterator<File> listFiles(File file) {
File[] files = file.listFiles();
if (files == null) {
return Collections.emptyIterator();
}
Arrays.sort(files, entryComparator);
return Arrays.asList(files).iterator();
}
@Override @Override
public void remove() { public void remove() {
throw new UnsupportedOperationException("remove"); throw new UnsupportedOperationException("remove");
} }
/** protected abstract T adapt(FileEntry entry);
* {@link Comparator} that orders {@link File} entries by their absolute paths.
*/
private static class EntryComparator implements Comparator<File> {
@Override }
public int compare(File o1, File o2) {
return o1.getAbsolutePath().compareTo(o2.getAbsolutePath()); private static class EntryIterator extends AbstractIterator<Entry> {
}
EntryIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
super(root, recursive, searchFilter, includeFilter);
}
@Override
protected Entry adapt(FileEntry entry) {
return entry;
}
}
private static class ArchiveIterator extends AbstractIterator<Archive> {
ArchiveIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
super(root, recursive, searchFilter, includeFilter);
}
@Override
protected Archive adapt(FileEntry entry) {
File file = entry.getFile();
return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive(entry));
} }
} }
@ -220,9 +265,12 @@ public class ExplodedArchive implements Archive {
private final File file; private final File file;
FileEntry(String name, File file) { private final URL url;
FileEntry(String name, File file, URL url) {
this.name = name; this.name = name;
this.file = file; this.file = file;
this.url = url;
} }
File getFile() { File getFile() {
@ -239,6 +287,55 @@ public class ExplodedArchive implements Archive {
return this.name; return this.name;
} }
URL getUrl() {
return this.url;
}
}
/**
* {@link Archive} implementation backed by a simple JAR file that doesn't itself
* contain nested archives.
*/
private static class SimpleJarFileArchive implements Archive {
private final URL url;
SimpleJarFileArchive(FileEntry file) {
this.url = file.getUrl();
}
@Override
public URL getUrl() throws MalformedURLException {
return this.url;
}
@Override
public Manifest getManifest() throws IOException {
return null;
}
@Override
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter)
throws IOException {
return Collections.emptyIterator();
}
@Override
public Iterator<Entry> iterator() {
return Collections.emptyIterator();
}
@Override
public String toString() {
try {
return getUrl().toString();
}
catch (Exception ex) {
return "jar archive";
}
}
} }
} }

@ -23,11 +23,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.Manifest; import java.util.jar.Manifest;
@ -80,19 +76,13 @@ public class JarFileArchive implements Archive {
} }
@Override @Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException { public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>(); return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter);
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
} }
@Override @Override
public Iterator<Entry> iterator() { public Iterator<Entry> iterator() {
return new EntryIterator(this.jarFile.entries()); return new EntryIterator(this.jarFile.iterator(), null, null);
} }
@Override @Override
@ -169,29 +159,85 @@ public class JarFileArchive implements Archive {
} }
/** /**
* {@link Archive.Entry} iterator implementation backed by {@link JarEntry}. * Abstract base class for iterator implementations.
*/ */
private static class EntryIterator implements Iterator<Entry> { private abstract static class AbstractIterator<T> implements Iterator<T> {
private final Iterator<JarEntry> iterator;
private final Enumeration<JarEntry> enumeration; private final EntryFilter searchFilter;
EntryIterator(Enumeration<JarEntry> enumeration) { private final EntryFilter includeFilter;
this.enumeration = enumeration;
private Entry current;
AbstractIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
this.iterator = iterator;
this.searchFilter = searchFilter;
this.includeFilter = includeFilter;
this.current = poll();
} }
@Override @Override
public boolean hasNext() { public boolean hasNext() {
return this.enumeration.hasMoreElements(); return this.current != null;
}
@Override
public T next() {
T result = adapt(this.current);
this.current = poll();
return result;
}
private Entry poll() {
while (this.iterator.hasNext()) {
JarFileEntry candidate = new JarFileEntry(this.iterator.next());
if ((this.searchFilter == null || this.searchFilter.matches(candidate))
&& (this.includeFilter == null || this.includeFilter.matches(candidate))) {
return candidate;
}
}
return null;
}
protected abstract T adapt(Entry entry);
}
/**
* {@link Archive.Entry} iterator implementation backed by {@link JarEntry}.
*/
private static class EntryIterator extends AbstractIterator<Entry> {
EntryIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
super(iterator, searchFilter, includeFilter);
} }
@Override @Override
public Entry next() { protected Entry adapt(Entry entry) {
return new JarFileEntry(this.enumeration.nextElement()); return entry;
}
}
/**
* Nested {@link Archive} iterator implementation backed by {@link JarEntry}.
*/
private class NestedArchiveIterator extends AbstractIterator<Archive> {
NestedArchiveIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
super(iterator, searchFilter, includeFilter);
} }
@Override @Override
public void remove() { protected Archive adapt(Entry entry) {
throw new UnsupportedOperationException("remove"); try {
return getNestedArchive(entry);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
} }
} }

@ -48,7 +48,7 @@ import org.springframework.boot.loader.data.RandomAccessDataFile;
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 1.0.0 * @since 1.0.0
*/ */
public class JarFile extends java.util.jar.JarFile { public class JarFile extends java.util.jar.JarFile implements Iterable<java.util.jar.JarEntry> {
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
@ -191,7 +191,7 @@ public class JarFile extends java.util.jar.JarFile {
@Override @Override
public Enumeration<java.util.jar.JarEntry> entries() { public Enumeration<java.util.jar.JarEntry> entries() {
final Iterator<JarEntry> iterator = this.entries.iterator(); Iterator<java.util.jar.JarEntry> iterator = iterator();
return new Enumeration<java.util.jar.JarEntry>() { return new Enumeration<java.util.jar.JarEntry>() {
@Override @Override
@ -207,6 +207,17 @@ public class JarFile extends java.util.jar.JarFile {
}; };
} }
/**
* Return an iterator for the contained entries.
* @see java.lang.Iterable#iterator()
* @since 2.3.0
*/
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public Iterator<java.util.jar.JarEntry> iterator() {
return (Iterator) this.entries.iterator();
}
public JarEntry getJarEntry(CharSequence name) { public JarEntry getJarEntry(CharSequence name) {
return this.entries.getEntry(name); return this.entries.getEntry(name);
} }

@ -20,6 +20,7 @@ import java.io.File;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import org.codehaus.plexus.util.CollectionUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive;
@ -39,7 +40,7 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception { void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception {
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF")); File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF"));
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).hasSize(2); assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "BOOT-INF/classes").toURI().toURL(), assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "BOOT-INF/classes").toURI().toURL(),
new File(explodedRoot, "BOOT-INF/lib/foo.jar").toURI().toURL()); new File(explodedRoot, "BOOT-INF/lib/foo.jar").toURI().toURL());
@ -53,7 +54,7 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
File jarRoot = createJarArchive("archive.jar", "BOOT-INF"); File jarRoot = createJarArchive("archive.jar", "BOOT-INF");
try (JarFileArchive archive = new JarFileArchive(jarRoot)) { try (JarFileArchive archive = new JarFileArchive(jarRoot)) {
JarLauncher launcher = new JarLauncher(archive); JarLauncher launcher = new JarLauncher(archive);
List<Archive> classPathArchives = launcher.getClassPathArchives(); List<Archive> classPathArchives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(classPathArchives).hasSize(2); assertThat(classPathArchives).hasSize(2);
assertThat(getUrls(classPathArchives)).containsOnly( assertThat(getUrls(classPathArchives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"), new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"),

@ -30,6 +30,7 @@ import java.util.jar.Manifest;
import org.assertj.core.api.Condition; import org.assertj.core.api.Condition;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.codehaus.plexus.util.CollectionUtils;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -139,7 +140,7 @@ class PropertiesLauncherTests {
System.setProperty("loader.path", "jars/"); System.setProperty("loader.path", "jars/");
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[jars/]"); assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[jars/]");
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).areExactly(1, endingWith("app.jar")); assertThat(archives).areExactly(1, endingWith("app.jar"));
} }
@ -169,7 +170,7 @@ class PropertiesLauncherTests {
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()) assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]"); .isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]");
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar")); assertThat(archives).areExactly(1, endingWith("app.jar"));
} }
@ -178,7 +179,7 @@ class PropertiesLauncherTests {
void testUserSpecifiedRootOfJarPathWithDot() throws Exception { void testUserSpecifiedRootOfJarPathWithDot() throws Exception {
System.setProperty("loader.path", "nested-jars/app.jar!/./"); System.setProperty("loader.path", "nested-jars/app.jar!/./");
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar")); assertThat(archives).areExactly(1, endingWith("app.jar"));
} }
@ -187,7 +188,7 @@ class PropertiesLauncherTests {
void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception {
System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./"); System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./");
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
} }
@ -196,7 +197,7 @@ class PropertiesLauncherTests {
System.setProperty("loader.path", "nested-jars/app.jar"); System.setProperty("loader.path", "nested-jars/app.jar");
System.setProperty("loader.main", "demo.Application"); System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar")); assertThat(archives).areExactly(1, endingWith("app.jar"));
} }
@ -206,7 +207,7 @@ class PropertiesLauncherTests {
System.setProperty("loader.path", "nested-jars/app.jar!/foo.jar"); System.setProperty("loader.path", "nested-jars/app.jar!/foo.jar");
System.setProperty("loader.main", "demo.Application"); System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).hasSize(1).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).hasSize(1).areExactly(1, endingWith("foo.jar!/"));
} }
@ -335,7 +336,7 @@ class PropertiesLauncherTests {
loaderPath.mkdir(); loaderPath.mkdir();
System.setProperty("loader.path", loaderPath.toURI().toURL().toString()); System.setProperty("loader.path", loaderPath.toURI().toURL().toString());
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives.size()).isEqualTo(1); assertThat(archives.size()).isEqualTo(1);
File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root"); File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root");
assertThat(archiveRoot).isEqualTo(loaderPath); assertThat(archiveRoot).isEqualTo(loaderPath);

@ -20,6 +20,7 @@ import java.io.File;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import org.codehaus.plexus.util.CollectionUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive;
@ -39,7 +40,7 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception {
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF")); File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF"));
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = launcher.getClassPathArchives(); List<Archive> archives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(archives).hasSize(2); assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "WEB-INF/classes").toURI().toURL(), assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "WEB-INF/classes").toURI().toURL(),
new File(explodedRoot, "WEB-INF/lib/foo.jar").toURI().toURL()); new File(explodedRoot, "WEB-INF/lib/foo.jar").toURI().toURL());
@ -53,7 +54,7 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
File jarRoot = createJarArchive("archive.war", "WEB-INF"); File jarRoot = createJarArchive("archive.war", "WEB-INF");
try (JarFileArchive archive = new JarFileArchive(jarRoot)) { try (JarFileArchive archive = new JarFileArchive(jarRoot)) {
WarLauncher launcher = new WarLauncher(archive); WarLauncher launcher = new WarLauncher(archive);
List<Archive> classPathArchives = launcher.getClassPathArchives(); List<Archive> classPathArchives = CollectionUtils.iteratorToList(launcher.getClassPathArchivesIterator());
assertThat(classPathArchives).hasSize(2); assertThat(classPathArchives).hasSize(2);
assertThat(getUrls(classPathArchives)).containsOnly( assertThat(getUrls(classPathArchives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"), new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"),

@ -74,7 +74,6 @@ class ExplodedArchiveTests {
private void createArchive(String folderName) throws Exception { private void createArchive(String folderName) throws Exception {
File file = new File(this.tempDir, "test.jar"); File file = new File(this.tempDir, "test.jar");
TestJarCreator.createTestJar(file); TestJarCreator.createTestJar(file);
this.rootFolder = (StringUtils.hasText(folderName) ? new File(this.tempDir, folderName) this.rootFolder = (StringUtils.hasText(folderName) ? new File(this.tempDir, folderName)
: new File(this.tempDir, UUID.randomUUID().toString())); : new File(this.tempDir, UUID.randomUUID().toString()));
JarFile jarFile = new JarFile(file); JarFile jarFile = new JarFile(file);
@ -102,7 +101,7 @@ class ExplodedArchiveTests {
@Test @Test
void getEntries() { void getEntries() {
Map<String, Archive.Entry> entries = getEntriesMap(this.archive); Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
assertThat(entries.size()).isEqualTo(12); assertThat(entries).hasSize(12);
} }
@Test @Test
@ -121,7 +120,7 @@ class ExplodedArchiveTests {
Entry entry = getEntriesMap(this.archive).get("nested.jar"); Entry entry = getEntriesMap(this.archive).get("nested.jar");
Archive nested = this.archive.getNestedArchive(entry); Archive nested = this.archive.getNestedArchive(entry);
assertThat(nested.getUrl().toString()).isEqualTo(this.rootFolder.toURI() + "nested.jar"); assertThat(nested.getUrl().toString()).isEqualTo(this.rootFolder.toURI() + "nested.jar");
((JarFileArchive) nested).close(); nested.close();
} }
@Test @Test

Loading…
Cancel
Save