diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java index 10569575a1..c52a76fc47 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -44,4 +44,5 @@ public class JarLauncher extends ExecutableArchiveLauncher { public static void main(String[] args) { new JarLauncher().launch(args); } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JavaAgentDetector.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JavaAgentDetector.java index 42823b6131..b3ac8844cc 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JavaAgentDetector.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JavaAgentDetector.java @@ -32,4 +32,5 @@ public interface JavaAgentDetector { * @param url The url to examine */ public boolean isJavaAgentJar(URL url); + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java index b840f22e90..b805dee5cf 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -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 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 } } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java index 56c8b4fe34..909a1e8b74 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -79,4 +79,5 @@ public class WarLauncher extends ExecutableArchiveLauncher { public static void main(String[] args) { new WarLauncher().launch(args); } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java index 7ae0e74750..e2b3d10b88 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java @@ -65,5 +65,7 @@ public interface RandomAccessData { * Obtain access to the underlying resource on each read, releasing it when done. */ PER_READ + } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java index 492b81b543..51121fcad0 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java @@ -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); } } + } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java index c9d348f150..8244f0871c 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java @@ -76,4 +76,5 @@ class Bytes { } return value; } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java index 748aa9ce55..4f6a34ea2e 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -119,4 +119,5 @@ class CentralDirectoryEndRecord { public int getNumberOfRecords() { return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2); } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java index 5f91138d92..25569b2b5c 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -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> rootFileCache; + static { + rootFileCache = new SoftReference>(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 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 cache = rootFileCache.get(); + if (cache == null) { + cache = new ConcurrentHashMap(); + rootFileCache = new SoftReference>(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); + } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java index cdf7559273..1a336d93eb 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java @@ -53,22 +53,30 @@ public final class JarEntryData { private SoftReference 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} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java index 0e2ff7cbd2..907c4767ee 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -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. *
    - *
  • Jar entries can be {@link JarEntryFilter filtered} during construction and new - * filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from - * existing files.
  • - *
  • A nested {@link JarFile} can be - * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory - * entry.
  • - *
  • A nested {@link JarFile} can be - * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files - * (as long as their entry is not compressed).
  • + *
  • New filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} + * from existing files.
  • + *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based + * on any directory entry.
  • + *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for + * embedded JAR files (as long as their entry is not compressed).
  • *
  • Entry data can be accessed as {@link RandomAccessData}.
  • *
* @@ -69,20 +66,20 @@ public class JarFile extends java.util.jar.JarFile implements Iterable entries; + private final List entries; private SoftReference> entriesByName; + private boolean signed; + private JarEntryData manifestEntry; private SoftReference manifest; @@ -90,21 +87,19 @@ public class JarFile extends java.util.jar.JarFile implements Iterable 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 loadJarEntries(CentralDirectoryEndRecord endRecord) + throws IOException { RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data); int numberOfRecords = endRecord.getNumberOfRecords(); - this.entries = new ArrayList(numberOfRecords); + List entries = new ArrayList(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 filterEntries(List entries, + JarEntryFilter[] filters) { + List filteredEntries = new ArrayList(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 useFastExceptions = new ThreadLocal(); + + 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)); - } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/AsciiBytes.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/AsciiBytes.java index 15df426ffd..2291c9ce5a 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/AsciiBytes.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/AsciiBytes.java @@ -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;