Skip to content

Commit a3ceaf6

Browse files
author
Phillip Webb
committed
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
1 parent a8777ed commit a3ceaf6

File tree

5 files changed

+179
-77
lines changed

5 files changed

+179
-77
lines changed

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Collections;
2626
import java.util.Enumeration;
2727

28+
import org.springframework.boot.loader.jar.Handler;
2829
import org.springframework.boot.loader.jar.JarFile;
2930

3031
/**
@@ -93,7 +94,6 @@ private boolean hasURLs() {
9394

9495
@Override
9596
public Enumeration<URL> getResources(String name) throws IOException {
96-
9797
if (this.rootClassLoader == null) {
9898
return findResources(name);
9999
}
@@ -116,6 +116,7 @@ public URL nextElement() {
116116
}
117117
return localResources.nextElement();
118118
}
119+
119120
};
120121
}
121122

@@ -128,7 +129,13 @@ protected Class<?> loadClass(String name, boolean resolve)
128129
synchronized (this) {
129130
Class<?> loadedClass = findLoadedClass(name);
130131
if (loadedClass == null) {
131-
loadedClass = doLoadClass(name);
132+
Handler.setUseFastConnectionExceptions(true);
133+
try {
134+
loadedClass = doLoadClass(name);
135+
}
136+
finally {
137+
Handler.setUseFastConnectionExceptions(false);
138+
}
132139
}
133140
if (resolve) {
134141
resolveClass(loadedClass);

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class Handler extends URLStreamHandler {
4242

4343
private static final String FILE_PROTOCOL = "file:";
4444

45-
private static final String SEPARATOR = JarURLConnection.SEPARATOR;
45+
private static final String SEPARATOR = "!/";
4646

4747
private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" };
4848

@@ -198,4 +198,14 @@ static void addToRootFileCache(File sourceFile, JarFile jarFile) {
198198
cache.put(sourceFile, jarFile);
199199
}
200200

201+
/**
202+
* Set if a generic static exception can be thrown when a URL cannot be connected.
203+
* This optimization is used during class loading to save creating lots of exceptions
204+
* which are then swallowed.
205+
* @param useFastConnectionExceptions if fast connection exceptions can be used.
206+
*/
207+
public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) {
208+
JarURLConnection.setUseFastExceptions(useFastConnectionExceptions);
209+
}
210+
201211
}

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
6666

6767
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
6868

69+
private static final AsciiBytes SLASH = new AsciiBytes("/");
70+
6971
private final RandomAccessDataFile rootFile;
7072

7173
private final String name;
@@ -250,6 +252,13 @@ public ZipEntry getEntry(String name) {
250252
}
251253

252254
public JarEntryData getJarEntryData(String name) {
255+
if (name == null) {
256+
return null;
257+
}
258+
return getJarEntryData(new AsciiBytes(name));
259+
}
260+
261+
public JarEntryData getJarEntryData(AsciiBytes name) {
253262
if (name == null) {
254263
return null;
255264
}
@@ -264,9 +273,9 @@ public JarEntryData getJarEntryData(String name) {
264273
entriesByName);
265274
}
266275

267-
JarEntryData entryData = entriesByName.get(new AsciiBytes(name));
268-
if (entryData == null && !name.endsWith("/")) {
269-
entryData = entriesByName.get(new AsciiBytes(name + "/"));
276+
JarEntryData entryData = entriesByName.get(name);
277+
if (entryData == null && !name.endsWith(SLASH)) {
278+
entryData = entriesByName.get(name.append(SLASH));
270279
}
271280
return entryData;
272281
}

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java

Lines changed: 140 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.io.InputStream;
2323
import java.net.MalformedURLException;
2424
import java.net.URL;
25+
import java.net.URLConnection;
26+
import java.net.URLStreamHandler;
2527
import java.util.jar.Manifest;
2628

2729
import org.springframework.boot.loader.util.AsciiBytes;
@@ -33,51 +35,73 @@
3335
*/
3436
class JarURLConnection extends java.net.JarURLConnection {
3537

36-
static final String PROTOCOL = "jar";
38+
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException();
3739

38-
static final String SEPARATOR = "!/";
40+
private static final String SEPARATOR = "!/";
3941

40-
private static final String PREFIX = PROTOCOL + ":" + "file:";
42+
private static final URL EMPTY_JAR_URL;
4143

42-
private final JarFile jarFile;
44+
static {
45+
try {
46+
EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() {
47+
@Override
48+
protected URLConnection openConnection(URL u) throws IOException {
49+
// Stub URLStreamHandler to prevent the wrong JAR Handler from being
50+
// Instantiated and cached.
51+
return null;
52+
}
53+
});
54+
}
55+
catch (MalformedURLException ex) {
56+
throw new IllegalStateException(ex);
57+
}
58+
}
4359

44-
private JarEntryData jarEntryData;
60+
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName("");
4561

46-
private String jarEntryName;
62+
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
4763

48-
private String contentType;
64+
private final String jarFileUrlSpec;
65+
66+
private final JarFile jarFile;
67+
68+
private JarEntryData jarEntryData;
4969

5070
private URL jarFileUrl;
5171

72+
private JarEntryName jarEntryName;
73+
5274
protected JarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
53-
super(new URL(buildRootUrl(jarFile)));
75+
// What we pass to super is ultimately ignored
76+
super(EMPTY_JAR_URL);
5477
this.url = url;
5578
this.jarFile = jarFile;
56-
5779
String spec = url.getFile();
5880
int separator = spec.lastIndexOf(SEPARATOR);
5981
if (separator == -1) {
6082
throw new MalformedURLException("no " + SEPARATOR + " found in url spec:"
6183
+ spec);
6284
}
63-
if (separator + 2 != spec.length()) {
64-
this.jarEntryName = decode(spec.substring(separator + 2));
65-
}
85+
this.jarFileUrlSpec = spec.substring(0, separator);
86+
this.jarEntryName = getJarEntryName(spec.substring(separator + 2));
87+
}
6688

67-
String container = spec.substring(0, separator);
68-
if (container.indexOf(SEPARATOR) == -1) {
69-
this.jarFileUrl = new URL(container);
70-
}
71-
else {
72-
this.jarFileUrl = new URL("jar:" + container);
89+
private JarEntryName getJarEntryName(String spec) {
90+
if (spec.length() == 0) {
91+
return EMPTY_JAR_ENTRY_NAME;
7392
}
93+
return new JarEntryName(spec);
7494
}
7595

7696
@Override
7797
public void connect() throws IOException {
78-
if (this.jarEntryName != null) {
79-
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName);
98+
if (!this.jarEntryName.isEmpty()) {
99+
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName
100+
.asAsciiBytes());
80101
if (this.jarEntryData == null) {
102+
if (Boolean.TRUE.equals(useFastExceptions.get())) {
103+
throw FILE_NOT_FOUND_EXCEPTION;
104+
}
81105
throw new FileNotFoundException("JAR entry " + this.jarEntryName
82106
+ " not found in " + this.jarFile.getName());
83107
}
@@ -103,9 +127,24 @@ public JarFile getJarFile() throws IOException {
103127

104128
@Override
105129
public URL getJarFileURL() {
130+
if (this.jarFileUrl == null) {
131+
this.jarFileUrl = buildJarFileUrl();
132+
}
106133
return this.jarFileUrl;
107134
}
108135

136+
private URL buildJarFileUrl() {
137+
try {
138+
if (this.jarFileUrlSpec.indexOf(SEPARATOR) == -1) {
139+
return new URL(this.jarFileUrlSpec);
140+
}
141+
return new URL("jar:" + this.jarFileUrlSpec);
142+
}
143+
catch (MalformedURLException ex) {
144+
throw new IllegalStateException(ex);
145+
}
146+
}
147+
109148
@Override
110149
public JarEntry getJarEntry() throws IOException {
111150
connect();
@@ -114,13 +153,13 @@ public JarEntry getJarEntry() throws IOException {
114153

115154
@Override
116155
public String getEntryName() {
117-
return this.jarEntryName;
156+
return this.jarEntryName.toString();
118157
}
119158

120159
@Override
121160
public InputStream getInputStream() throws IOException {
122161
connect();
123-
if (this.jarEntryName == null) {
162+
if (this.jarEntryName.isEmpty()) {
124163
throw new IOException("no entry name specified");
125164
}
126165
return this.jarEntryData.getInputStream();
@@ -130,8 +169,10 @@ public InputStream getInputStream() throws IOException {
130169
public int getContentLength() {
131170
try {
132171
connect();
133-
return this.jarEntryData == null ? this.jarFile.size() : this.jarEntryData
134-
.getSize();
172+
if (this.jarEntryData != null) {
173+
return this.jarEntryData.getSize();
174+
}
175+
return this.jarFile.size();
135176
}
136177
catch (IOException ex) {
137178
return -1;
@@ -146,58 +187,86 @@ public Object getContent() throws IOException {
146187

147188
@Override
148189
public String getContentType() {
149-
if (this.contentType == null) {
150-
// Guess the content type, don't bother with steams as mark is not
151-
// supported
152-
this.contentType = (this.jarEntryName == null ? "x-java/jar" : null);
153-
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName)
154-
: this.contentType);
155-
this.contentType = (this.contentType == null ? "content/unknown"
156-
: this.contentType);
157-
}
158-
return this.contentType;
159-
}
160-
161-
private static String buildRootUrl(JarFile jarFile) {
162-
String path = jarFile.getRootJarFile().getFile().getPath();
163-
StringBuilder builder = new StringBuilder(PREFIX.length() + path.length()
164-
+ SEPARATOR.length());
165-
builder.append(PREFIX);
166-
builder.append(path);
167-
builder.append(SEPARATOR);
168-
return builder.toString();
169-
}
170-
171-
private static String decode(String source) {
172-
int length = source.length();
173-
if ((length == 0) || (source.indexOf('%') < 0)) {
174-
return source;
175-
}
176-
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
177-
for (int i = 0; i < length; i++) {
178-
int ch = source.charAt(i);
179-
if (ch == '%') {
180-
if ((i + 2) >= length) {
181-
throw new IllegalArgumentException("Invalid encoded sequence \""
182-
+ source.substring(i) + "\"");
190+
return this.jarEntryName.getContentType();
191+
}
192+
193+
static void setUseFastExceptions(boolean useFastExceptions) {
194+
JarURLConnection.useFastExceptions.set(useFastExceptions);
195+
}
196+
197+
/**
198+
* A JarEntryName parsed from a URL String.
199+
*/
200+
private static class JarEntryName {
201+
202+
private final AsciiBytes name;
203+
204+
private String contentType;
205+
206+
public JarEntryName(String spec) {
207+
this.name = decode(spec);
208+
}
209+
210+
private AsciiBytes decode(String source) {
211+
int length = (source == null ? 0 : source.length());
212+
if ((length == 0) || (source.indexOf('%') < 0)) {
213+
return new AsciiBytes(source);
214+
}
215+
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
216+
for (int i = 0; i < length; i++) {
217+
int ch = source.charAt(i);
218+
if (ch == '%') {
219+
if ((i + 2) >= length) {
220+
throw new IllegalArgumentException("Invalid encoded sequence \""
221+
+ source.substring(i) + "\"");
222+
}
223+
ch = decodeEscapeSequence(source, i);
224+
i += 2;
183225
}
184-
ch = decodeEscapeSequence(source, i);
185-
i += 2;
226+
bos.write(ch);
186227
}
187-
bos.write(ch);
228+
// AsciiBytes is what is used to store the JarEntries so make it symmetric
229+
return new AsciiBytes(bos.toByteArray());
188230
}
189-
// AsciiBytes is what is used to store the JarEntries so make it symmetric
190-
return new AsciiBytes(bos.toByteArray()).toString();
191231

192-
}
232+
private char decodeEscapeSequence(String source, int i) {
233+
int hi = Character.digit(source.charAt(i + 1), 16);
234+
int lo = Character.digit(source.charAt(i + 2), 16);
235+
if (hi == -1 || lo == -1) {
236+
throw new IllegalArgumentException("Invalid encoded sequence \""
237+
+ source.substring(i) + "\"");
238+
}
239+
return ((char) ((hi << 4) + lo));
240+
}
241+
242+
@Override
243+
public String toString() {
244+
return this.name.toString();
245+
}
246+
247+
public AsciiBytes asAsciiBytes() {
248+
return this.name;
249+
}
250+
251+
public boolean isEmpty() {
252+
return this.name.length() == 0;
253+
}
254+
255+
public String getContentType() {
256+
if (this.contentType == null) {
257+
this.contentType = deduceContentType();
258+
}
259+
return this.contentType;
260+
}
193261

194-
private static char decodeEscapeSequence(String source, int i) {
195-
int hi = Character.digit(source.charAt(i + 1), 16);
196-
int lo = Character.digit(source.charAt(i + 2), 16);
197-
if (hi == -1 || lo == -1) {
198-
throw new IllegalArgumentException("Invalid encoded sequence \""
199-
+ source.substring(i) + "\"");
262+
private String deduceContentType() {
263+
// Guess the content type, don't bother with streams as mark is not supported
264+
String type = (isEmpty() ? "x-java/jar" : null);
265+
type = (type != null ? type : guessContentTypeFromName(toString()));
266+
type = (type != null ? type : "content/unknown");
267+
return type;
200268
}
201-
return ((char) ((hi << 4) + lo));
269+
202270
}
271+
203272
}

0 commit comments

Comments
 (0)