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..e27f1bec33 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); 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 fcc4b3d946..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 @@ -42,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" }; @@ -198,4 +198,14 @@ public class Handler extends URLStreamHandler { 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/JarFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java index e52bc6c0f9..600bde4203 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 @@ -66,6 +66,8 @@ 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;