Improve performance of fat jar loading

Tweak 'fat jar' handling to generally improve performance:

- Allow JarURLConnection to throw a static FileNotFoundException when
  loading classes. This exception is thrown many times when attempting
  to load a class and is silently swallowed so there is no point in
  providing the entry name.
- Expose JarFile.getJarEntryData(AsciiBytes) and store AsciiBytes in
  the JarURLConnection. Previously AsciiBytes were created, discarded
  then created again.
- Use EMPTY_JAR_URL for the JarURLConnection super constructor. The URL
  is never actually used so we can improve performance by using a
  constant.
- Extract JarEntryName for possible caching. The jar entry name
  extracted from the URL is now contained in an inner JarEntryName
  class. This could be cached if necessary (although currently it is
  not because no perceivable performance benefit was observed)

Fixes gh-1119
pull/1129/head
Phillip Webb 11 years ago
parent a8777eda76
commit a3ceaf63e2

@ -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 {
@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 {
} }
return localResources.nextElement(); return localResources.nextElement();
} }
}; };
} }
@ -128,8 +129,14 @@ public class LaunchedURLClassLoader extends URLClassLoader {
synchronized (this) { synchronized (this) {
Class<?> loadedClass = findLoadedClass(name); Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) { if (loadedClass == null) {
Handler.setUseFastConnectionExceptions(true);
try {
loadedClass = doLoadClass(name); loadedClass = doLoadClass(name);
} }
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
if (resolve) { if (resolve) {
resolveClass(loadedClass); resolveClass(loadedClass);
} }

@ -42,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" };
@ -198,4 +198,14 @@ public class Handler extends URLStreamHandler {
cache.put(sourceFile, jarFile); 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);
}
} }

@ -66,6 +66,8 @@ 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 static final AsciiBytes SLASH = new AsciiBytes("/");
private final RandomAccessDataFile rootFile; private final RandomAccessDataFile rootFile;
private final String name; private final String name;
@ -250,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;
} }
@ -264,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;
} }

@ -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;
*/ */
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 {
@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 {
@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 {
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,32 +187,30 @@ 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);
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName)
: this.contentType);
this.contentType = (this.contentType == null ? "content/unknown"
: this.contentType);
} }
return this.contentType;
static void setUseFastExceptions(boolean useFastExceptions) {
JarURLConnection.useFastExceptions.set(useFastExceptions);
} }
private static String buildRootUrl(JarFile jarFile) { /**
String path = jarFile.getRootJarFile().getFile().getPath(); * A JarEntryName parsed from a URL String.
StringBuilder builder = new StringBuilder(PREFIX.length() + path.length() */
+ SEPARATOR.length()); private static class JarEntryName {
builder.append(PREFIX);
builder.append(path); private final AsciiBytes name;
builder.append(SEPARATOR);
return builder.toString(); private String contentType;
public JarEntryName(String spec) {
this.name = decode(spec);
} }
private static String decode(String source) { private AsciiBytes decode(String source) {
int length = source.length(); int length = (source == null ? 0 : source.length());
if ((length == 0) || (source.indexOf('%') < 0)) { if ((length == 0) || (source.indexOf('%') < 0)) {
return source; return new AsciiBytes(source);
} }
ByteArrayOutputStream bos = new ByteArrayOutputStream(length); ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
@ -187,11 +226,10 @@ class JarURLConnection extends java.net.JarURLConnection {
bos.write(ch); bos.write(ch);
} }
// AsciiBytes is what is used to store the JarEntries so make it symmetric // AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray()).toString(); return new AsciiBytes(bos.toByteArray());
} }
private static char decodeEscapeSequence(String source, int i) { private char decodeEscapeSequence(String source, int i) {
int hi = Character.digit(source.charAt(i + 1), 16); int hi = Character.digit(source.charAt(i + 1), 16);
int lo = Character.digit(source.charAt(i + 2), 16); int lo = Character.digit(source.charAt(i + 2), 16);
if (hi == -1 || lo == -1) { if (hi == -1 || lo == -1) {
@ -200,4 +238,35 @@ class JarURLConnection extends java.net.JarURLConnection {
} }
return ((char) ((hi << 4) + lo)); 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;
}
}
} }

@ -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;

Loading…
Cancel
Save