Commit dbfd785e authored by Phillip Webb's avatar Phillip Webb

Merge branch 'gh-1119'

Improve fat JAR performance.

See gh-1119
parents 378d38e2 20fb55ea
......@@ -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);
}
}
}
}
......@@ -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}
......
......@@ -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;
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 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;
}
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();
}
private static String decode(String source) {
int length = source.length();
if ((length == 0) || (source.indexOf('%') < 0)) {
return source;
}
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) + "\"");
return this.jarEntryName.getContentType();
}
static void setUseFastExceptions(boolean useFastExceptions) {
JarURLConnection.useFastExceptions.set(useFastExceptions);
}
/**
* 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);
}
private AsciiBytes decode(String source) {
int length = (source == null ? 0 : source.length());
if ((length == 0) || (source.indexOf('%') < 0)) {
return new AsciiBytes(source);
}
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;
}
ch = decodeEscapeSequence(source, i);
i += 2;
bos.write(ch);
}
bos.write(ch);
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray());
}
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray()).toString();
}
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 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) + "\"");
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;
}
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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment