Commit dbfd785e authored by Phillip Webb's avatar Phillip Webb

Merge branch 'gh-1119'

Improve fat JAR performance.

See gh-1119
parents 378d38e2 20fb55ea
...@@ -44,4 +44,5 @@ public class JarLauncher extends ExecutableArchiveLauncher { ...@@ -44,4 +44,5 @@ public class JarLauncher extends ExecutableArchiveLauncher {
public static void main(String[] args) { public static void main(String[] args) {
new JarLauncher().launch(args); new JarLauncher().launch(args);
} }
} }
...@@ -32,4 +32,5 @@ public interface JavaAgentDetector { ...@@ -32,4 +32,5 @@ public interface JavaAgentDetector {
* @param url The url to examine * @param url The url to examine
*/ */
public boolean isJavaAgentJar(URL url); public boolean isJavaAgentJar(URL url);
} }
...@@ -25,6 +25,7 @@ import java.util.Arrays; ...@@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import org.springframework.boot.loader.jar.Handler;
import org.springframework.boot.loader.jar.JarFile; import org.springframework.boot.loader.jar.JarFile;
/** /**
...@@ -93,7 +94,6 @@ public class LaunchedURLClassLoader extends URLClassLoader { ...@@ -93,7 +94,6 @@ public class LaunchedURLClassLoader extends URLClassLoader {
@Override @Override
public Enumeration<URL> getResources(String name) throws IOException { public Enumeration<URL> getResources(String name) throws IOException {
if (this.rootClassLoader == null) { if (this.rootClassLoader == null) {
return findResources(name); return findResources(name);
} }
...@@ -116,6 +116,7 @@ public class LaunchedURLClassLoader extends URLClassLoader { ...@@ -116,6 +116,7 @@ public class LaunchedURLClassLoader extends URLClassLoader {
} }
return localResources.nextElement(); return localResources.nextElement();
} }
}; };
} }
...@@ -128,7 +129,13 @@ public class LaunchedURLClassLoader extends URLClassLoader { ...@@ -128,7 +129,13 @@ public class LaunchedURLClassLoader extends URLClassLoader {
synchronized (this) { synchronized (this) {
Class<?> loadedClass = findLoadedClass(name); Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) { if (loadedClass == null) {
loadedClass = doLoadClass(name); Handler.setUseFastConnectionExceptions(true);
try {
loadedClass = doLoadClass(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
} }
if (resolve) { if (resolve) {
resolveClass(loadedClass); resolveClass(loadedClass);
...@@ -214,4 +221,5 @@ public class LaunchedURLClassLoader extends URLClassLoader { ...@@ -214,4 +221,5 @@ public class LaunchedURLClassLoader extends URLClassLoader {
// Ignore // Ignore
} }
} }
} }
...@@ -79,4 +79,5 @@ public class WarLauncher extends ExecutableArchiveLauncher { ...@@ -79,4 +79,5 @@ public class WarLauncher extends ExecutableArchiveLauncher {
public static void main(String[] args) { public static void main(String[] args) {
new WarLauncher().launch(args); new WarLauncher().launch(args);
} }
} }
...@@ -65,5 +65,7 @@ public interface RandomAccessData { ...@@ -65,5 +65,7 @@ public interface RandomAccessData {
* Obtain access to the underlying resource on each read, releasing it when done. * Obtain access to the underlying resource on each read, releasing it when done.
*/ */
PER_READ PER_READ
} }
} }
...@@ -221,6 +221,7 @@ public class RandomAccessDataFile implements RandomAccessData { ...@@ -221,6 +221,7 @@ public class RandomAccessDataFile implements RandomAccessData {
this.position += amount; this.position += amount;
return amount; return amount;
} }
} }
/** /**
...@@ -277,5 +278,7 @@ public class RandomAccessDataFile implements RandomAccessData { ...@@ -277,5 +278,7 @@ public class RandomAccessDataFile implements RandomAccessData {
throw new IOException(ex); throw new IOException(ex);
} }
} }
} }
} }
...@@ -76,4 +76,5 @@ class Bytes { ...@@ -76,4 +76,5 @@ class Bytes {
} }
return value; return value;
} }
} }
...@@ -119,4 +119,5 @@ class CentralDirectoryEndRecord { ...@@ -119,4 +119,5 @@ class CentralDirectoryEndRecord {
public int getNumberOfRecords() { public int getNumberOfRecords() {
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2); return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2);
} }
} }
...@@ -18,11 +18,14 @@ package org.springframework.boot.loader.jar; ...@@ -18,11 +18,14 @@ package org.springframework.boot.loader.jar;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.net.URLStreamHandler; import java.net.URLStreamHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
...@@ -39,7 +42,7 @@ public class Handler extends URLStreamHandler { ...@@ -39,7 +42,7 @@ public class Handler extends URLStreamHandler {
private static final String FILE_PROTOCOL = "file:"; private static final String FILE_PROTOCOL = "file:";
private static final String SEPARATOR = JarURLConnection.SEPARATOR; private static final String SEPARATOR = "!/";
private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" }; private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" };
...@@ -55,6 +58,11 @@ public class Handler extends URLStreamHandler { ...@@ -55,6 +58,11 @@ public class Handler extends URLStreamHandler {
OPEN_CONNECTION_METHOD = method; OPEN_CONNECTION_METHOD = method;
} }
private static SoftReference<Map<File, JarFile>> rootFileCache;
static {
rootFileCache = new SoftReference<Map<File, JarFile>>(null);
}
private final Logger logger = Logger.getLogger(getClass().getName()); private final Logger logger = Logger.getLogger(getClass().getName());
private final JarFile jarFile; private final JarFile jarFile;
...@@ -153,7 +161,14 @@ public class Handler extends URLStreamHandler { ...@@ -153,7 +161,14 @@ public class Handler extends URLStreamHandler {
throw new IllegalStateException("Not a file URL"); throw new IllegalStateException("Not a file URL");
} }
String path = name.substring(FILE_PROTOCOL.length()); String path = name.substring(FILE_PROTOCOL.length());
return new JarFile(new File(path)); File file = new File(path);
Map<File, JarFile> cache = rootFileCache.get();
JarFile jarFile = (cache == null ? null : cache.get(file));
if (jarFile == null) {
jarFile = new JarFile(file);
addToRootFileCache(file, jarFile);
}
return jarFile;
} }
catch (Exception ex) { catch (Exception ex) {
throw new IOException("Unable to open root Jar file '" + name + "'", ex); throw new IOException("Unable to open root Jar file '" + name + "'", ex);
...@@ -168,4 +183,29 @@ public class Handler extends URLStreamHandler { ...@@ -168,4 +183,29 @@ public class Handler extends URLStreamHandler {
} }
return jarFile.getNestedJarFile(jarEntry); return jarFile.getNestedJarFile(jarEntry);
} }
/**
* Add the given {@link JarFile} to the root file cache.
* @param sourceFile the source file to add
* @param jarFile the jar file.
*/
static void addToRootFileCache(File sourceFile, JarFile jarFile) {
Map<File, JarFile> cache = rootFileCache.get();
if (cache == null) {
cache = new ConcurrentHashMap<File, JarFile>();
rootFileCache = new SoftReference<Map<File, JarFile>>(cache);
}
cache.put(sourceFile, jarFile);
}
/**
* Set if a generic static exception can be thrown when a URL cannot be connected.
* This optimization is used during class loading to save creating lots of exceptions
* which are then swallowed.
* @param useFastConnectionExceptions if fast connection exceptions can be used.
*/
public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) {
JarURLConnection.setUseFastExceptions(useFastConnectionExceptions);
}
} }
...@@ -53,22 +53,30 @@ public final class JarEntryData { ...@@ -53,22 +53,30 @@ public final class JarEntryData {
private SoftReference<JarEntry> entry; private SoftReference<JarEntry> entry;
JarFile nestedJar;
public JarEntryData(JarFile source, byte[] header, InputStream inputStream) public JarEntryData(JarFile source, byte[] header, InputStream inputStream)
throws IOException { throws IOException {
this.source = source; this.source = source;
this.header = header; this.header = header;
long nameLength = Bytes.littleEndianValue(header, 28, 2); long nameLength = Bytes.littleEndianValue(header, 28, 2);
long extraLength = Bytes.littleEndianValue(header, 30, 2); long extraLength = Bytes.littleEndianValue(header, 30, 2);
long commentLength = Bytes.littleEndianValue(header, 32, 2); long commentLength = Bytes.littleEndianValue(header, 32, 2);
this.name = new AsciiBytes(Bytes.get(inputStream, nameLength)); this.name = new AsciiBytes(Bytes.get(inputStream, nameLength));
this.extra = Bytes.get(inputStream, extraLength); this.extra = Bytes.get(inputStream, extraLength);
this.comment = new AsciiBytes(Bytes.get(inputStream, commentLength)); this.comment = new AsciiBytes(Bytes.get(inputStream, commentLength));
this.localHeaderOffset = Bytes.littleEndianValue(header, 42, 4); this.localHeaderOffset = Bytes.littleEndianValue(header, 42, 4);
} }
private JarEntryData(JarEntryData master, JarFile source, AsciiBytes name) {
this.header = master.header;
this.extra = master.extra;
this.comment = master.comment;
this.localHeaderOffset = master.localHeaderOffset;
this.source = source;
this.name = name;
}
void setName(AsciiBytes name) { void setName(AsciiBytes name) {
this.name = name; this.name = name;
} }
...@@ -154,6 +162,10 @@ public final class JarEntryData { ...@@ -154,6 +162,10 @@ public final class JarEntryData {
return this.comment; return this.comment;
} }
JarEntryData createFilteredCopy(JarFile jarFile, AsciiBytes name) {
return new JarEntryData(this, jarFile, name);
}
/** /**
* Create a new {@link JarEntryData} instance from the specified input stream. * Create a new {@link JarEntryData} instance from the specified input stream.
* @param source the source {@link JarFile} * @param source the source {@link JarFile}
......
...@@ -43,15 +43,12 @@ import org.springframework.boot.loader.util.AsciiBytes; ...@@ -43,15 +43,12 @@ import org.springframework.boot.loader.util.AsciiBytes;
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality. * offers the following additional functionality.
* <ul> * <ul>
* <li>Jar entries can be {@link JarEntryFilter filtered} during construction and new * <li>New filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created}
* filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from * from existing files.</li>
* existing files.</li> * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* <li>A nested {@link JarFile} can be * on any directory entry.</li>
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* entry.</li> * embedded JAR files (as long as their entry is not compressed).</li>
* <li>A nested {@link JarFile} can be
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files
* (as long as their entry is not compressed).</li>
* <li>Entry data can be accessed as {@link RandomAccessData}.</li> * <li>Entry data can be accessed as {@link RandomAccessData}.</li>
* </ul> * </ul>
* *
...@@ -69,20 +66,20 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -69,20 +66,20 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
private final RandomAccessDataFile rootFile; private static final AsciiBytes SLASH = new AsciiBytes("/");
private final RandomAccessData data; private final RandomAccessDataFile rootFile;
private final String name; private final String name;
private final long size; private final RandomAccessData data;
private boolean signed;
private List<JarEntryData> entries; private final List<JarEntryData> entries;
private SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName; private SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName;
private boolean signed;
private JarEntryData manifestEntry; private JarEntryData manifestEntry;
private SoftReference<Manifest> manifest; private SoftReference<Manifest> manifest;
...@@ -90,21 +87,19 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -90,21 +87,19 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
/** /**
* Create a new {@link JarFile} backed by the specified file. * Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file * @param file the root jar file
* @param filters an optional set of jar entry filters
* @throws IOException * @throws IOException
*/ */
public JarFile(File file, JarEntryFilter... filters) throws IOException { public JarFile(File file) throws IOException {
this(new RandomAccessDataFile(file), filters); this(new RandomAccessDataFile(file));
} }
/** /**
* Create a new {@link JarFile} backed by the specified file. * Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file * @param file the root jar file
* @param filters an optional set of jar entry filters
* @throws IOException * @throws IOException
*/ */
JarFile(RandomAccessDataFile file, JarEntryFilter... filters) throws IOException { JarFile(RandomAccessDataFile file) throws IOException {
this(file, file.getFile().getAbsolutePath(), file, filters); this(file, file.getFile().getAbsolutePath(), file);
} }
/** /**
...@@ -113,18 +108,25 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -113,18 +108,25 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
* @param rootFile the root jar file * @param rootFile the root jar file
* @param name the name of this file * @param name the name of this file
* @param data the underlying data * @param data the underlying data
* @param filters an optional set of jar entry filters
* @throws IOException * @throws IOException
*/ */
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data, private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data)
JarEntryFilter... filters) throws IOException { throws IOException {
super(rootFile.getFile()); super(rootFile.getFile());
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data); CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
this.rootFile = rootFile;
this.name = name;
this.data = getArchiveData(endRecord, data); this.data = getArchiveData(endRecord, data);
this.entries = loadJarEntries(endRecord);
}
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data,
List<JarEntryData> entries, JarEntryFilter... filters) throws IOException {
super(rootFile.getFile());
this.rootFile = rootFile; this.rootFile = rootFile;
this.name = name; this.name = name;
this.size = data.getSize(); this.data = data;
loadJarEntries(endRecord, filters); this.entries = filterEntries(entries, filters);
} }
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord, private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord,
...@@ -136,36 +138,48 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -136,36 +138,48 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
return data.getSubsection(offset, data.getSize() - offset); return data.getSubsection(offset, data.getSize() - offset);
} }
private void loadJarEntries(CentralDirectoryEndRecord endRecord, private List<JarEntryData> loadJarEntries(CentralDirectoryEndRecord endRecord)
JarEntryFilter[] filters) throws IOException { throws IOException {
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data); RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
int numberOfRecords = endRecord.getNumberOfRecords(); int numberOfRecords = endRecord.getNumberOfRecords();
this.entries = new ArrayList<JarEntryData>(numberOfRecords); List<JarEntryData> entries = new ArrayList<JarEntryData>(numberOfRecords);
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE); InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
try { try {
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream); JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
while (entry != null) { while (entry != null) {
addJarEntry(entry, filters); entries.add(entry);
processEntry(entry);
entry = JarEntryData.fromInputStream(this, inputStream); entry = JarEntryData.fromInputStream(this, inputStream);
} }
} }
finally { finally {
inputStream.close(); inputStream.close();
} }
return entries;
} }
private void addJarEntry(JarEntryData entry, JarEntryFilter[] filters) { private List<JarEntryData> filterEntries(List<JarEntryData> entries,
AsciiBytes name = entry.getName(); JarEntryFilter[] filters) {
for (JarEntryFilter filter : filters) { List<JarEntryData> filteredEntries = new ArrayList<JarEntryData>(entries.size());
name = (filter == null || name == null ? name : filter.apply(name, entry)); for (JarEntryData entry : entries) {
} AsciiBytes name = entry.getName();
if (name != null) { for (JarEntryFilter filter : filters) {
entry.setName(name); name = (filter == null || name == null ? name : filter.apply(name, entry));
this.entries.add(entry); }
if (name.startsWith(META_INF)) { if (name != null) {
processMetaInfEntry(name, entry); JarEntryData filteredCopy = entry.createFilteredCopy(this, name);
filteredEntries.add(filteredCopy);
processEntry(filteredCopy);
} }
} }
return filteredEntries;
}
private void processEntry(JarEntryData entry) {
AsciiBytes name = entry.getName();
if (name.startsWith(META_INF)) {
processMetaInfEntry(name, entry);
}
} }
private void processMetaInfEntry(AsciiBytes name, JarEntryData entry) { private void processMetaInfEntry(AsciiBytes name, JarEntryData entry) {
...@@ -238,6 +252,13 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -238,6 +252,13 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
} }
public JarEntryData getJarEntryData(String name) { public JarEntryData getJarEntryData(String name) {
if (name == null) {
return null;
}
return getJarEntryData(new AsciiBytes(name));
}
public JarEntryData getJarEntryData(AsciiBytes name) {
if (name == null) { if (name == null) {
return null; return null;
} }
...@@ -252,9 +273,9 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -252,9 +273,9 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
entriesByName); entriesByName);
} }
JarEntryData entryData = entriesByName.get(new AsciiBytes(name)); JarEntryData entryData = entriesByName.get(name);
if (entryData == null && !name.endsWith("/")) { if (entryData == null && !name.endsWith(SLASH)) {
entryData = entriesByName.get(new AsciiBytes(name + "/")); entryData = entriesByName.get(name.append(SLASH));
} }
return entryData; return entryData;
} }
...@@ -297,29 +318,26 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -297,29 +318,26 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
/** /**
* Return a nested {@link JarFile} loaded from the specified entry. * Return a nested {@link JarFile} loaded from the specified entry.
* @param ze the zip entry * @param ze the zip entry
* @param filters an optional set of jar entry filters to be applied
* @return a {@link JarFile} for the entry * @return a {@link JarFile} for the entry
* @throws IOException * @throws IOException
*/ */
public synchronized JarFile getNestedJarFile(final ZipEntry ze, public synchronized JarFile getNestedJarFile(final ZipEntry ze) throws IOException {
JarEntryFilter... filters) throws IOException {
return getNestedJarFile(getContainedEntry(ze).getSource()); return getNestedJarFile(getContainedEntry(ze).getSource());
} }
/** /**
* Return a nested {@link JarFile} loaded from the specified entry. * Return a nested {@link JarFile} loaded from the specified entry.
* @param sourceEntry the zip entry * @param sourceEntry the zip entry
* @param filters an optional set of jar entry filters to be applied
* @return a {@link JarFile} for the entry * @return a {@link JarFile} for the entry
* @throws IOException * @throws IOException
*/ */
public synchronized JarFile getNestedJarFile(final JarEntryData sourceEntry, public synchronized JarFile getNestedJarFile(JarEntryData sourceEntry)
JarEntryFilter... filters) throws IOException { throws IOException {
try { try {
if (sourceEntry.isDirectory()) { if (sourceEntry.nestedJar == null) {
return getNestedJarFileFromDirectoryEntry(sourceEntry, filters); sourceEntry.nestedJar = createJarFileFromEntry(sourceEntry);
} }
return getNestedJarFileFromFileEntry(sourceEntry, filters); return sourceEntry.nestedJar;
} }
catch (IOException ex) { catch (IOException ex) {
throw new IOException("Unable to open nested jar file '" throw new IOException("Unable to open nested jar file '"
...@@ -327,12 +345,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -327,12 +345,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
} }
} }
private JarFile getNestedJarFileFromDirectoryEntry(JarEntryData sourceEntry, private JarFile createJarFileFromEntry(JarEntryData sourceEntry) throws IOException {
JarEntryFilter... filters) throws IOException { if (sourceEntry.isDirectory()) {
return createJarFileFromDirectoryEntry(sourceEntry);
}
return createJarFileFromFileEntry(sourceEntry);
}
private JarFile createJarFileFromDirectoryEntry(JarEntryData sourceEntry)
throws IOException {
final AsciiBytes sourceName = sourceEntry.getName(); final AsciiBytes sourceName = sourceEntry.getName();
JarEntryFilter[] filtersToUse = new JarEntryFilter[filters.length + 1]; JarEntryFilter filter = new JarEntryFilter() {
System.arraycopy(filters, 0, filtersToUse, 1, filters.length);
filtersToUse[0] = new JarEntryFilter() {
@Override @Override
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) { public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
if (name.startsWith(sourceName) && !name.equals(sourceName)) { if (name.startsWith(sourceName) && !name.equals(sourceName)) {
...@@ -343,17 +366,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -343,17 +366,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
}; };
return new JarFile(this.rootFile, getName() + "!/" return new JarFile(this.rootFile, getName() + "!/"
+ sourceEntry.getName().substring(0, sourceName.length() - 1), this.data, + sourceEntry.getName().substring(0, sourceName.length() - 1), this.data,
filtersToUse); this.entries, filter);
} }
private JarFile getNestedJarFileFromFileEntry(JarEntryData sourceEntry, private JarFile createJarFileFromFileEntry(JarEntryData sourceEntry)
JarEntryFilter... filters) throws IOException { throws IOException {
if (sourceEntry.getMethod() != ZipEntry.STORED) { if (sourceEntry.getMethod() != ZipEntry.STORED) {
throw new IllegalStateException("Unable to open nested compressed entry " throw new IllegalStateException("Unable to open nested compressed entry "
+ sourceEntry.getName()); + sourceEntry.getName());
} }
return new JarFile(this.rootFile, getName() + "!/" + sourceEntry.getName(), return new JarFile(this.rootFile, getName() + "!/" + sourceEntry.getName(),
sourceEntry.getData(), filters); sourceEntry.getData());
} }
/** /**
...@@ -364,7 +387,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -364,7 +387,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
*/ */
public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters) public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters)
throws IOException { throws IOException {
return new JarFile(this.rootFile, getName(), this.data, filters); return new JarFile(this.rootFile, getName(), this.data, this.entries, filters);
} }
private JarEntry getContainedEntry(ZipEntry zipEntry) throws IOException { private JarEntry getContainedEntry(ZipEntry zipEntry) throws IOException {
...@@ -377,7 +400,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -377,7 +400,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
@Override @Override
public int size() { public int size() {
return (int) this.size; return (int) this.data.getSize();
} }
@Override @Override
...@@ -455,4 +478,5 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD ...@@ -455,4 +478,5 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
*/ */
SYSTEM_INDEPENDENT SYSTEM_INDEPENDENT
} }
} }
...@@ -22,6 +22,8 @@ import java.io.IOException; ...@@ -22,6 +22,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.util.AsciiBytes; import org.springframework.boot.loader.util.AsciiBytes;
...@@ -33,51 +35,73 @@ import org.springframework.boot.loader.util.AsciiBytes; ...@@ -33,51 +35,73 @@ import org.springframework.boot.loader.util.AsciiBytes;
*/ */
class JarURLConnection extends java.net.JarURLConnection { class JarURLConnection extends java.net.JarURLConnection {
static final String PROTOCOL = "jar"; private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException();
static final String SEPARATOR = "!/"; private static final String SEPARATOR = "!/";
private static final String PREFIX = PROTOCOL + ":" + "file:"; private static final URL EMPTY_JAR_URL;
private final JarFile jarFile; static {
try {
EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
// Stub URLStreamHandler to prevent the wrong JAR Handler from being
// Instantiated and cached.
return null;
}
});
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
private JarEntryData jarEntryData; private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName("");
private String jarEntryName; private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
private String contentType; private final String jarFileUrlSpec;
private final JarFile jarFile;
private JarEntryData jarEntryData;
private URL jarFileUrl; private URL jarFileUrl;
private JarEntryName jarEntryName;
protected JarURLConnection(URL url, JarFile jarFile) throws MalformedURLException { protected JarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
super(new URL(buildRootUrl(jarFile))); // What we pass to super is ultimately ignored
super(EMPTY_JAR_URL);
this.url = url; this.url = url;
this.jarFile = jarFile; this.jarFile = jarFile;
String spec = url.getFile(); String spec = url.getFile();
int separator = spec.lastIndexOf(SEPARATOR); int separator = spec.lastIndexOf(SEPARATOR);
if (separator == -1) { if (separator == -1) {
throw new MalformedURLException("no " + SEPARATOR + " found in url spec:" throw new MalformedURLException("no " + SEPARATOR + " found in url spec:"
+ spec); + spec);
} }
if (separator + 2 != spec.length()) { this.jarFileUrlSpec = spec.substring(0, separator);
this.jarEntryName = decode(spec.substring(separator + 2)); this.jarEntryName = getJarEntryName(spec.substring(separator + 2));
} }
String container = spec.substring(0, separator); private JarEntryName getJarEntryName(String spec) {
if (container.indexOf(SEPARATOR) == -1) { if (spec.length() == 0) {
this.jarFileUrl = new URL(container); return EMPTY_JAR_ENTRY_NAME;
}
else {
this.jarFileUrl = new URL("jar:" + container);
} }
return new JarEntryName(spec);
} }
@Override @Override
public void connect() throws IOException { public void connect() throws IOException {
if (this.jarEntryName != null) { if (!this.jarEntryName.isEmpty()) {
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName); this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName
.asAsciiBytes());
if (this.jarEntryData == null) { if (this.jarEntryData == null) {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException("JAR entry " + this.jarEntryName throw new FileNotFoundException("JAR entry " + this.jarEntryName
+ " not found in " + this.jarFile.getName()); + " not found in " + this.jarFile.getName());
} }
...@@ -103,9 +127,24 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -103,9 +127,24 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override @Override
public URL getJarFileURL() { public URL getJarFileURL() {
if (this.jarFileUrl == null) {
this.jarFileUrl = buildJarFileUrl();
}
return this.jarFileUrl; return this.jarFileUrl;
} }
private URL buildJarFileUrl() {
try {
if (this.jarFileUrlSpec.indexOf(SEPARATOR) == -1) {
return new URL(this.jarFileUrlSpec);
}
return new URL("jar:" + this.jarFileUrlSpec);
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
@Override @Override
public JarEntry getJarEntry() throws IOException { public JarEntry getJarEntry() throws IOException {
connect(); connect();
...@@ -114,13 +153,13 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -114,13 +153,13 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override @Override
public String getEntryName() { public String getEntryName() {
return this.jarEntryName; return this.jarEntryName.toString();
} }
@Override @Override
public InputStream getInputStream() throws IOException { public InputStream getInputStream() throws IOException {
connect(); connect();
if (this.jarEntryName == null) { if (this.jarEntryName.isEmpty()) {
throw new IOException("no entry name specified"); throw new IOException("no entry name specified");
} }
return this.jarEntryData.getInputStream(); return this.jarEntryData.getInputStream();
...@@ -130,8 +169,10 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -130,8 +169,10 @@ class JarURLConnection extends java.net.JarURLConnection {
public int getContentLength() { public int getContentLength() {
try { try {
connect(); connect();
return this.jarEntryData == null ? this.jarFile.size() : this.jarEntryData if (this.jarEntryData != null) {
.getSize(); return this.jarEntryData.getSize();
}
return this.jarFile.size();
} }
catch (IOException ex) { catch (IOException ex) {
return -1; return -1;
...@@ -146,58 +187,86 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -146,58 +187,86 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override @Override
public String getContentType() { public String getContentType() {
if (this.contentType == null) { return this.jarEntryName.getContentType();
// Guess the content type, don't bother with steams as mark is not }
// supported
this.contentType = (this.jarEntryName == null ? "x-java/jar" : null); static void setUseFastExceptions(boolean useFastExceptions) {
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName) JarURLConnection.useFastExceptions.set(useFastExceptions);
: this.contentType); }
this.contentType = (this.contentType == null ? "content/unknown"
: this.contentType); /**
} * A JarEntryName parsed from a URL String.
return this.contentType; */
} private static class JarEntryName {
private static String buildRootUrl(JarFile jarFile) { private final AsciiBytes name;
String path = jarFile.getRootJarFile().getFile().getPath();
StringBuilder builder = new StringBuilder(PREFIX.length() + path.length() private String contentType;
+ SEPARATOR.length());
builder.append(PREFIX); public JarEntryName(String spec) {
builder.append(path); this.name = decode(spec);
builder.append(SEPARATOR); }
return builder.toString();
} private AsciiBytes decode(String source) {
int length = (source == null ? 0 : source.length());
private static String decode(String source) { if ((length == 0) || (source.indexOf('%') < 0)) {
int length = source.length(); return new AsciiBytes(source);
if ((length == 0) || (source.indexOf('%') < 0)) { }
return source; ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
} for (int i = 0; i < length; i++) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(length); int ch = source.charAt(i);
for (int i = 0; i < length; i++) { if (ch == '%') {
int ch = source.charAt(i); if ((i + 2) >= length) {
if (ch == '%') { throw new IllegalArgumentException("Invalid encoded sequence \""
if ((i + 2) >= length) { + source.substring(i) + "\"");
throw new IllegalArgumentException("Invalid encoded sequence \"" }
+ source.substring(i) + "\""); ch = decodeEscapeSequence(source, i);
i += 2;
} }
ch = decodeEscapeSequence(source, i); bos.write(ch);
i += 2;
} }
bos.write(ch); // AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray());
} }
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray()).toString();
} private char decodeEscapeSequence(String source, int i) {
int hi = Character.digit(source.charAt(i + 1), 16);
int lo = Character.digit(source.charAt(i + 2), 16);
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException("Invalid encoded sequence \""
+ source.substring(i) + "\"");
}
return ((char) ((hi << 4) + lo));
}
@Override
public String toString() {
return this.name.toString();
}
public AsciiBytes asAsciiBytes() {
return this.name;
}
public boolean isEmpty() {
return this.name.length() == 0;
}
public String getContentType() {
if (this.contentType == null) {
this.contentType = deduceContentType();
}
return this.contentType;
}
private static char decodeEscapeSequence(String source, int i) { private String deduceContentType() {
int hi = Character.digit(source.charAt(i + 1), 16); // Guess the content type, don't bother with streams as mark is not supported
int lo = Character.digit(source.charAt(i + 2), 16); String type = (isEmpty() ? "x-java/jar" : null);
if (hi == -1 || lo == -1) { type = (type != null ? type : guessContentTypeFromName(toString()));
throw new IllegalArgumentException("Invalid encoded sequence \"" type = (type != null ? type : "content/unknown");
+ source.substring(i) + "\""); return type;
} }
return ((char) ((hi << 4) + lo));
} }
} }
...@@ -128,6 +128,13 @@ public final class AsciiBytes { ...@@ -128,6 +128,13 @@ public final class AsciiBytes {
return append(string.getBytes(UTF_8)); return append(string.getBytes(UTF_8));
} }
public AsciiBytes append(AsciiBytes asciiBytes) {
if (asciiBytes == null || asciiBytes.length() == 0) {
return this;
}
return append(asciiBytes.bytes);
}
public AsciiBytes append(byte[] bytes) { public AsciiBytes append(byte[] bytes) {
if (bytes == null || bytes.length == 0) { if (bytes == null || bytes.length == 0) {
return this; return this;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment