Merge branch 'gh-1119'
Improve fat JAR performance. See gh-1119
This commit is contained in:
@@ -44,4 +44,5 @@ public class JarLauncher extends ExecutableArchiveLauncher {
|
||||
public static void main(String[] args) {
|
||||
new JarLauncher().launch(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,4 +32,5 @@ public interface JavaAgentDetector {
|
||||
* @param url The url to examine
|
||||
*/
|
||||
public boolean isJavaAgentJar(URL url);
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import org.springframework.boot.loader.jar.Handler;
|
||||
import org.springframework.boot.loader.jar.JarFile;
|
||||
|
||||
/**
|
||||
@@ -93,7 +94,6 @@ public class LaunchedURLClassLoader extends URLClassLoader {
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
|
||||
if (this.rootClassLoader == null) {
|
||||
return findResources(name);
|
||||
}
|
||||
@@ -116,6 +116,7 @@ public class LaunchedURLClassLoader extends URLClassLoader {
|
||||
}
|
||||
return localResources.nextElement();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,7 +129,13 @@ public class LaunchedURLClassLoader extends URLClassLoader {
|
||||
synchronized (this) {
|
||||
Class<?> loadedClass = findLoadedClass(name);
|
||||
if (loadedClass == null) {
|
||||
loadedClass = doLoadClass(name);
|
||||
Handler.setUseFastConnectionExceptions(true);
|
||||
try {
|
||||
loadedClass = doLoadClass(name);
|
||||
}
|
||||
finally {
|
||||
Handler.setUseFastConnectionExceptions(false);
|
||||
}
|
||||
}
|
||||
if (resolve) {
|
||||
resolveClass(loadedClass);
|
||||
@@ -214,4 +221,5 @@ public class LaunchedURLClassLoader extends URLClassLoader {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,4 +79,5 @@ public class WarLauncher extends ExecutableArchiveLauncher {
|
||||
public static void main(String[] args) {
|
||||
new WarLauncher().launch(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,5 +65,7 @@ public interface RandomAccessData {
|
||||
* Obtain access to the underlying resource on each read, releasing it when done.
|
||||
*/
|
||||
PER_READ
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ public class RandomAccessDataFile implements RandomAccessData {
|
||||
this.position += amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,5 +278,7 @@ public class RandomAccessDataFile implements RandomAccessData {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -76,4 +76,5 @@ class Bytes {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -119,4 +119,5 @@ class CentralDirectoryEndRecord {
|
||||
public int getNumberOfRecords() {
|
||||
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ package org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -39,7 +42,7 @@ public class Handler extends URLStreamHandler {
|
||||
|
||||
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" };
|
||||
|
||||
@@ -55,6 +58,11 @@ public class Handler extends URLStreamHandler {
|
||||
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 JarFile jarFile;
|
||||
@@ -153,7 +161,14 @@ public class Handler extends URLStreamHandler {
|
||||
throw new IllegalStateException("Not a file URL");
|
||||
}
|
||||
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) {
|
||||
throw new IOException("Unable to open root Jar file '" + name + "'", ex);
|
||||
@@ -168,4 +183,29 @@ public class Handler extends URLStreamHandler {
|
||||
}
|
||||
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 {
|
||||
|
||||
private SoftReference<JarEntry> entry;
|
||||
|
||||
JarFile nestedJar;
|
||||
|
||||
public JarEntryData(JarFile source, byte[] header, InputStream inputStream)
|
||||
throws IOException {
|
||||
|
||||
this.source = source;
|
||||
this.header = header;
|
||||
long nameLength = Bytes.littleEndianValue(header, 28, 2);
|
||||
long extraLength = Bytes.littleEndianValue(header, 30, 2);
|
||||
long commentLength = Bytes.littleEndianValue(header, 32, 2);
|
||||
|
||||
this.name = new AsciiBytes(Bytes.get(inputStream, nameLength));
|
||||
this.extra = Bytes.get(inputStream, extraLength);
|
||||
this.comment = new AsciiBytes(Bytes.get(inputStream, commentLength));
|
||||
|
||||
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) {
|
||||
this.name = name;
|
||||
}
|
||||
@@ -154,6 +162,10 @@ public final class JarEntryData {
|
||||
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.
|
||||
* @param source the source {@link JarFile}
|
||||
|
||||
@@ -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
|
||||
* offers the following additional functionality.
|
||||
* <ul>
|
||||
* <li>Jar entries can be {@link JarEntryFilter filtered} during construction and new
|
||||
* filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from
|
||||
* existing files.</li>
|
||||
* <li>A nested {@link JarFile} can be
|
||||
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory
|
||||
* entry.</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>New filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created}
|
||||
* from existing files.</li>
|
||||
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
|
||||
* on any directory entry.</li>
|
||||
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
|
||||
* embedded JAR files (as long as their entry is not compressed).</li>
|
||||
* <li>Entry data can be accessed as {@link RandomAccessData}.</li>
|
||||
* </ul>
|
||||
*
|
||||
@@ -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 final RandomAccessDataFile rootFile;
|
||||
private static final AsciiBytes SLASH = new AsciiBytes("/");
|
||||
|
||||
private final RandomAccessData data;
|
||||
private final RandomAccessDataFile rootFile;
|
||||
|
||||
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 boolean signed;
|
||||
|
||||
private JarEntryData manifestEntry;
|
||||
|
||||
private SoftReference<Manifest> manifest;
|
||||
@@ -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.
|
||||
* @param file the root jar file
|
||||
* @param filters an optional set of jar entry filters
|
||||
* @throws IOException
|
||||
*/
|
||||
public JarFile(File file, JarEntryFilter... filters) throws IOException {
|
||||
this(new RandomAccessDataFile(file), filters);
|
||||
public JarFile(File file) throws IOException {
|
||||
this(new RandomAccessDataFile(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JarFile} backed by the specified file.
|
||||
* @param file the root jar file
|
||||
* @param filters an optional set of jar entry filters
|
||||
* @throws IOException
|
||||
*/
|
||||
JarFile(RandomAccessDataFile file, JarEntryFilter... filters) throws IOException {
|
||||
this(file, file.getFile().getAbsolutePath(), file, filters);
|
||||
JarFile(RandomAccessDataFile file) throws IOException {
|
||||
this(file, file.getFile().getAbsolutePath(), file);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,18 +108,25 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
* @param rootFile the root jar file
|
||||
* @param name the name of this file
|
||||
* @param data the underlying data
|
||||
* @param filters an optional set of jar entry filters
|
||||
* @throws IOException
|
||||
*/
|
||||
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data,
|
||||
JarEntryFilter... filters) throws IOException {
|
||||
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data)
|
||||
throws IOException {
|
||||
super(rootFile.getFile());
|
||||
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
|
||||
this.data = getArchiveData(endRecord, data);
|
||||
this.rootFile = rootFile;
|
||||
this.name = name;
|
||||
this.size = data.getSize();
|
||||
loadJarEntries(endRecord, filters);
|
||||
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.name = name;
|
||||
this.data = data;
|
||||
this.entries = filterEntries(entries, filters);
|
||||
}
|
||||
|
||||
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord,
|
||||
@@ -136,35 +138,47 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
return data.getSubsection(offset, data.getSize() - offset);
|
||||
}
|
||||
|
||||
private void loadJarEntries(CentralDirectoryEndRecord endRecord,
|
||||
JarEntryFilter[] filters) throws IOException {
|
||||
private List<JarEntryData> loadJarEntries(CentralDirectoryEndRecord endRecord)
|
||||
throws IOException {
|
||||
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
|
||||
int numberOfRecords = endRecord.getNumberOfRecords();
|
||||
this.entries = new ArrayList<JarEntryData>(numberOfRecords);
|
||||
List<JarEntryData> entries = new ArrayList<JarEntryData>(numberOfRecords);
|
||||
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
|
||||
try {
|
||||
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
|
||||
while (entry != null) {
|
||||
addJarEntry(entry, filters);
|
||||
entries.add(entry);
|
||||
processEntry(entry);
|
||||
entry = JarEntryData.fromInputStream(this, inputStream);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
inputStream.close();
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private void addJarEntry(JarEntryData entry, JarEntryFilter[] filters) {
|
||||
AsciiBytes name = entry.getName();
|
||||
for (JarEntryFilter filter : filters) {
|
||||
name = (filter == null || name == null ? name : filter.apply(name, entry));
|
||||
}
|
||||
if (name != null) {
|
||||
entry.setName(name);
|
||||
this.entries.add(entry);
|
||||
if (name.startsWith(META_INF)) {
|
||||
processMetaInfEntry(name, entry);
|
||||
private List<JarEntryData> filterEntries(List<JarEntryData> entries,
|
||||
JarEntryFilter[] filters) {
|
||||
List<JarEntryData> filteredEntries = new ArrayList<JarEntryData>(entries.size());
|
||||
for (JarEntryData entry : entries) {
|
||||
AsciiBytes name = entry.getName();
|
||||
for (JarEntryFilter filter : filters) {
|
||||
name = (filter == null || name == null ? name : filter.apply(name, entry));
|
||||
}
|
||||
if (name != null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +252,13 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
}
|
||||
|
||||
public JarEntryData getJarEntryData(String name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
return getJarEntryData(new AsciiBytes(name));
|
||||
}
|
||||
|
||||
public JarEntryData getJarEntryData(AsciiBytes name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -252,9 +273,9 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
entriesByName);
|
||||
}
|
||||
|
||||
JarEntryData entryData = entriesByName.get(new AsciiBytes(name));
|
||||
if (entryData == null && !name.endsWith("/")) {
|
||||
entryData = entriesByName.get(new AsciiBytes(name + "/"));
|
||||
JarEntryData entryData = entriesByName.get(name);
|
||||
if (entryData == null && !name.endsWith(SLASH)) {
|
||||
entryData = entriesByName.get(name.append(SLASH));
|
||||
}
|
||||
return entryData;
|
||||
}
|
||||
@@ -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.
|
||||
* @param ze the zip entry
|
||||
* @param filters an optional set of jar entry filters to be applied
|
||||
* @return a {@link JarFile} for the entry
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized JarFile getNestedJarFile(final ZipEntry ze,
|
||||
JarEntryFilter... filters) throws IOException {
|
||||
public synchronized JarFile getNestedJarFile(final ZipEntry ze) throws IOException {
|
||||
return getNestedJarFile(getContainedEntry(ze).getSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a nested {@link JarFile} loaded from the specified 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
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized JarFile getNestedJarFile(final JarEntryData sourceEntry,
|
||||
JarEntryFilter... filters) throws IOException {
|
||||
public synchronized JarFile getNestedJarFile(JarEntryData sourceEntry)
|
||||
throws IOException {
|
||||
try {
|
||||
if (sourceEntry.isDirectory()) {
|
||||
return getNestedJarFileFromDirectoryEntry(sourceEntry, filters);
|
||||
if (sourceEntry.nestedJar == null) {
|
||||
sourceEntry.nestedJar = createJarFileFromEntry(sourceEntry);
|
||||
}
|
||||
return getNestedJarFileFromFileEntry(sourceEntry, filters);
|
||||
return sourceEntry.nestedJar;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IOException("Unable to open nested jar file '"
|
||||
@@ -327,12 +345,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
}
|
||||
}
|
||||
|
||||
private JarFile getNestedJarFileFromDirectoryEntry(JarEntryData sourceEntry,
|
||||
JarEntryFilter... filters) throws IOException {
|
||||
private JarFile createJarFileFromEntry(JarEntryData sourceEntry) throws IOException {
|
||||
if (sourceEntry.isDirectory()) {
|
||||
return createJarFileFromDirectoryEntry(sourceEntry);
|
||||
}
|
||||
return createJarFileFromFileEntry(sourceEntry);
|
||||
}
|
||||
|
||||
private JarFile createJarFileFromDirectoryEntry(JarEntryData sourceEntry)
|
||||
throws IOException {
|
||||
final AsciiBytes sourceName = sourceEntry.getName();
|
||||
JarEntryFilter[] filtersToUse = new JarEntryFilter[filters.length + 1];
|
||||
System.arraycopy(filters, 0, filtersToUse, 1, filters.length);
|
||||
filtersToUse[0] = new JarEntryFilter() {
|
||||
JarEntryFilter filter = new JarEntryFilter() {
|
||||
@Override
|
||||
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
|
||||
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
|
||||
@@ -343,17 +366,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
};
|
||||
return new JarFile(this.rootFile, getName() + "!/"
|
||||
+ sourceEntry.getName().substring(0, sourceName.length() - 1), this.data,
|
||||
filtersToUse);
|
||||
this.entries, filter);
|
||||
}
|
||||
|
||||
private JarFile getNestedJarFileFromFileEntry(JarEntryData sourceEntry,
|
||||
JarEntryFilter... filters) throws IOException {
|
||||
private JarFile createJarFileFromFileEntry(JarEntryData sourceEntry)
|
||||
throws IOException {
|
||||
if (sourceEntry.getMethod() != ZipEntry.STORED) {
|
||||
throw new IllegalStateException("Unable to open nested compressed entry "
|
||||
+ 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
|
||||
*/
|
||||
public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters)
|
||||
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 {
|
||||
@@ -377,7 +400,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return (int) this.size;
|
||||
return (int) this.data.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -455,4 +478,5 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||
*/
|
||||
SYSTEM_INDEPENDENT
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.util.AsciiBytes;
|
||||
@@ -33,51 +35,73 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
||||
*/
|
||||
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;
|
||||
|
||||
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 static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName("");
|
||||
|
||||
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
|
||||
|
||||
private final String jarFileUrlSpec;
|
||||
|
||||
private final JarFile jarFile;
|
||||
|
||||
private JarEntryData jarEntryData;
|
||||
|
||||
private String jarEntryName;
|
||||
|
||||
private String contentType;
|
||||
|
||||
private URL jarFileUrl;
|
||||
|
||||
private JarEntryName jarEntryName;
|
||||
|
||||
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.jarFile = jarFile;
|
||||
|
||||
String spec = url.getFile();
|
||||
int separator = spec.lastIndexOf(SEPARATOR);
|
||||
if (separator == -1) {
|
||||
throw new MalformedURLException("no " + SEPARATOR + " found in url spec:"
|
||||
+ spec);
|
||||
}
|
||||
if (separator + 2 != spec.length()) {
|
||||
this.jarEntryName = decode(spec.substring(separator + 2));
|
||||
}
|
||||
this.jarFileUrlSpec = spec.substring(0, separator);
|
||||
this.jarEntryName = getJarEntryName(spec.substring(separator + 2));
|
||||
}
|
||||
|
||||
String container = spec.substring(0, separator);
|
||||
if (container.indexOf(SEPARATOR) == -1) {
|
||||
this.jarFileUrl = new URL(container);
|
||||
}
|
||||
else {
|
||||
this.jarFileUrl = new URL("jar:" + container);
|
||||
private JarEntryName getJarEntryName(String spec) {
|
||||
if (spec.length() == 0) {
|
||||
return EMPTY_JAR_ENTRY_NAME;
|
||||
}
|
||||
return new JarEntryName(spec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
if (this.jarEntryName != null) {
|
||||
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName);
|
||||
if (!this.jarEntryName.isEmpty()) {
|
||||
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName
|
||||
.asAsciiBytes());
|
||||
if (this.jarEntryData == null) {
|
||||
if (Boolean.TRUE.equals(useFastExceptions.get())) {
|
||||
throw FILE_NOT_FOUND_EXCEPTION;
|
||||
}
|
||||
throw new FileNotFoundException("JAR entry " + this.jarEntryName
|
||||
+ " not found in " + this.jarFile.getName());
|
||||
}
|
||||
@@ -103,9 +127,24 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||
|
||||
@Override
|
||||
public URL getJarFileURL() {
|
||||
if (this.jarFileUrl == null) {
|
||||
this.jarFileUrl = buildJarFileUrl();
|
||||
}
|
||||
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
|
||||
public JarEntry getJarEntry() throws IOException {
|
||||
connect();
|
||||
@@ -114,13 +153,13 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||
|
||||
@Override
|
||||
public String getEntryName() {
|
||||
return this.jarEntryName;
|
||||
return this.jarEntryName.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
connect();
|
||||
if (this.jarEntryName == null) {
|
||||
if (this.jarEntryName.isEmpty()) {
|
||||
throw new IOException("no entry name specified");
|
||||
}
|
||||
return this.jarEntryData.getInputStream();
|
||||
@@ -130,8 +169,10 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||
public int getContentLength() {
|
||||
try {
|
||||
connect();
|
||||
return this.jarEntryData == null ? this.jarFile.size() : this.jarEntryData
|
||||
.getSize();
|
||||
if (this.jarEntryData != null) {
|
||||
return this.jarEntryData.getSize();
|
||||
}
|
||||
return this.jarFile.size();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return -1;
|
||||
@@ -146,58 +187,86 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
if (this.contentType == null) {
|
||||
// Guess the content type, don't bother with steams as mark is not
|
||||
// supported
|
||||
this.contentType = (this.jarEntryName == null ? "x-java/jar" : null);
|
||||
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName)
|
||||
: this.contentType);
|
||||
this.contentType = (this.contentType == null ? "content/unknown"
|
||||
: this.contentType);
|
||||
}
|
||||
return this.contentType;
|
||||
return this.jarEntryName.getContentType();
|
||||
}
|
||||
|
||||
private static String buildRootUrl(JarFile jarFile) {
|
||||
String path = jarFile.getRootJarFile().getFile().getPath();
|
||||
StringBuilder builder = new StringBuilder(PREFIX.length() + path.length()
|
||||
+ SEPARATOR.length());
|
||||
builder.append(PREFIX);
|
||||
builder.append(path);
|
||||
builder.append(SEPARATOR);
|
||||
return builder.toString();
|
||||
static void setUseFastExceptions(boolean useFastExceptions) {
|
||||
JarURLConnection.useFastExceptions.set(useFastExceptions);
|
||||
}
|
||||
|
||||
private static String decode(String source) {
|
||||
int length = source.length();
|
||||
if ((length == 0) || (source.indexOf('%') < 0)) {
|
||||
return source;
|
||||
/**
|
||||
* A JarEntryName parsed from a URL String.
|
||||
*/
|
||||
private static class JarEntryName {
|
||||
|
||||
private final AsciiBytes name;
|
||||
|
||||
private String contentType;
|
||||
|
||||
public JarEntryName(String spec) {
|
||||
this.name = decode(spec);
|
||||
}
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int ch = source.charAt(i);
|
||||
if (ch == '%') {
|
||||
if ((i + 2) >= length) {
|
||||
throw new IllegalArgumentException("Invalid encoded sequence \""
|
||||
+ source.substring(i) + "\"");
|
||||
}
|
||||
ch = decodeEscapeSequence(source, i);
|
||||
i += 2;
|
||||
|
||||
private AsciiBytes decode(String source) {
|
||||
int length = (source == null ? 0 : source.length());
|
||||
if ((length == 0) || (source.indexOf('%') < 0)) {
|
||||
return new AsciiBytes(source);
|
||||
}
|
||||
bos.write(ch);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int ch = source.charAt(i);
|
||||
if (ch == '%') {
|
||||
if ((i + 2) >= length) {
|
||||
throw new IllegalArgumentException("Invalid encoded sequence \""
|
||||
+ source.substring(i) + "\"");
|
||||
}
|
||||
ch = decodeEscapeSequence(source, i);
|
||||
i += 2;
|
||||
}
|
||||
bos.write(ch);
|
||||
}
|
||||
// AsciiBytes is what is used to store the JarEntries so make it symmetric
|
||||
return new AsciiBytes(bos.toByteArray());
|
||||
}
|
||||
|
||||
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 String deduceContentType() {
|
||||
// Guess the content type, don't bother with streams as mark is not supported
|
||||
String type = (isEmpty() ? "x-java/jar" : null);
|
||||
type = (type != null ? type : guessContentTypeFromName(toString()));
|
||||
type = (type != null ? type : "content/unknown");
|
||||
return type;
|
||||
}
|
||||
// AsciiBytes is what is used to store the JarEntries so make it symmetric
|
||||
return new AsciiBytes(bos.toByteArray()).toString();
|
||||
|
||||
}
|
||||
|
||||
private static 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,13 @@ public final class AsciiBytes {
|
||||
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) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return this;
|
||||
|
||||
Reference in New Issue
Block a user