Improve the performance of JarURLConnection

This commit improves the performance of JarURLConnection. There are two
main changes:

Firstly, the way in which the spec is determined has been changed so
that it’s no longer necessary to create an absolute file. Instead,
the JarFile’s pathFromRoot is used to extract the spec from the URL
relative to the JarFile.

Secondly, the number of temporary Objects that are created has been
reduced, for example by tracking an index as we process a String
rather than creating a new substring for each iteration.

See gh-6215
This commit is contained in:
Andy Wilkinson 2016-06-24 16:14:06 +01:00
parent 9788a7bdda
commit 0d207d438a
4 changed files with 45 additions and 38 deletions

View File

@ -199,6 +199,10 @@ final class AsciiBytes {
return false;
}
static String toString(byte[] bytes) {
return new String(bytes, UTF_8);
}
public static int hashCode(String string) {
// We're compatible with String's hashCode().
return string.hashCode();

View File

@ -364,6 +364,10 @@ public class JarFile extends java.util.jar.JarFile {
this.entries.clearCache();
}
protected String getPathFromRoot() {
return this.pathFromRoot;
}
/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.

View File

@ -17,22 +17,17 @@
package org.springframework.boot.loader.jar;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.net.URLStreamHandler;
import java.security.Permission;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
@ -68,16 +63,6 @@ class JarURLConnection extends java.net.JarURLConnection {
private static final String READ_ACTION = "read";
private static final Map<String, String> absoluteFileCache = Collections
.synchronizedMap(new LinkedHashMap<String, String>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() >= 50;
}
});
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
private final JarFile jarFile;
@ -94,32 +79,25 @@ class JarURLConnection extends java.net.JarURLConnection {
// What we pass to super is ultimately ignored
super(EMPTY_JAR_URL);
this.url = url;
String spec = getNormalizedFileUrl(url)
.substring(jarFile.getUrl().getFile().length());
String spec = extractFullSpec(url, jarFile.getPathFromRoot());
int separator;
while ((separator = spec.indexOf(SEPARATOR)) > 0) {
jarFile = getNestedJarFile(jarFile, spec.substring(0, separator));
spec = spec.substring(separator + SEPARATOR.length());
int index = 0;
while ((separator = spec.indexOf(SEPARATOR, index)) > 0) {
jarFile = getNestedJarFile(jarFile, spec.substring(index, separator));
index += separator + SEPARATOR.length();
}
this.jarFile = jarFile;
this.jarEntryName = getJarEntryName(spec);
this.jarEntryName = getJarEntryName(spec.substring(index));
}
private String getNormalizedFileUrl(URL url) {
private String extractFullSpec(URL url, String pathFromRoot) {
String file = url.getFile();
String path = "";
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex > 0) {
path = file.substring(separatorIndex);
file = file.substring(0, separatorIndex);
if (separatorIndex < 0) {
return "";
}
String absoluteFile = JarURLConnection.absoluteFileCache.get(file);
if (absoluteFile == null) {
absoluteFile = new File(URI.create(file).getSchemeSpecificPart())
.getAbsoluteFile().toURI().toString();
JarURLConnection.absoluteFileCache.put(file, absoluteFile);
}
return absoluteFile + path;
return file
.substring(separatorIndex + SEPARATOR.length() + pathFromRoot.length());
}
private JarFile getNestedJarFile(JarFile jarFile, String name) throws IOException {
@ -266,14 +244,13 @@ class JarURLConnection extends java.net.JarURLConnection {
}
private String decode(String source) {
int length = (source == null ? 0 : source.length());
if ((length == 0) || (source.indexOf('%') < 0)) {
return new AsciiBytes(source).toString();
if (source.length() == 0 || (source.indexOf('%') < 0)) {
return source;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length());
write(source, bos);
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray()).toString();
return AsciiBytes.toString(bos.toByteArray());
}
private void write(String source, ByteArrayOutputStream outputStream) {

View File

@ -102,6 +102,28 @@ public class JarURLConnectionTests {
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile()
throws Exception {
URL absoluteUrl = new URL(
"jar:file:" + getAbsolutePath() + "!/nested.jar!/3.dat");
assertThat(new JarURLConnection(absoluteUrl,
this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar")))
.getInputStream()).hasSameContentAs(
new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile()
throws Exception {
URL absoluteUrl = new URL(
"jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
assertThat(new JarURLConnection(absoluteUrl,
this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar")))
.getInputStream()).hasSameContentAs(
new ByteArrayInputStream(new byte[] { 3 }));
}
private String getAbsolutePath() {
return this.rootJarFile.getAbsolutePath().replace('\\', '/');
}