Commit a5cddf79 authored by Phillip Webb's avatar Phillip Webb

Reduce JarURLConnection allocations

Update JarURLConnection & Handler so that a shared static final
connection is returned for entries that cannot be found.

See gh-6215
parent 44b7f29e
......@@ -65,7 +65,7 @@ final class CentralDirectoryFileHeader implements FileHeader {
}
void load(byte[] data, int dataOffset, RandomAccessData variableData,
int variableOffset) throws IOException {
int variableOffset, JarEntryFilter filter) throws IOException {
// Load fixed part
this.header = data;
this.headerOffset = dataOffset;
......@@ -81,6 +81,9 @@ final class CentralDirectoryFileHeader implements FileHeader {
dataOffset = 0;
}
this.name = new AsciiBytes(data, dataOffset, (int) nameLength);
if (filter != null) {
this.name = filter.apply(this.name);
}
this.extra = NO_EXTRA;
this.comment = NO_COMMENT;
if (extraLength > 0) {
......@@ -172,10 +175,10 @@ final class CentralDirectoryFileHeader implements FileHeader {
}
public static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data,
int offset) throws IOException {
int offset, JarEntryFilter filter) throws IOException {
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
byte[] bytes = Bytes.get(data.getSubsection(offset, 46));
fileHeader.load(bytes, 0, data, offset);
fileHeader.load(bytes, 0, data, offset, filter);
return fileHeader;
}
......
......@@ -65,7 +65,7 @@ class CentralDirectoryParser {
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
int dataOffset = 0;
for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
fileHeader.load(bytes, dataOffset, null, 0);
fileHeader.load(bytes, dataOffset, null, 0, null);
visitFileHeader(dataOffset, fileHeader);
dataOffset += this.CENTRAL_DIRECTORY_HEADER_BASE_SIZE
+ fileHeader.getName().length() + fileHeader.getComment().length()
......
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -85,10 +85,10 @@ public class Handler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL url) throws IOException {
if (this.jarFile != null) {
return new JarURLConnection(url, this.jarFile);
return JarURLConnection.get(url, this.jarFile);
}
try {
return new JarURLConnection(url, getRootJarFileFromUrl(url));
return JarURLConnection.get(url, getRootJarFileFromUrl(url));
}
catch (Exception ex) {
return openFallbackConnection(url, ex);
......
......@@ -39,8 +39,8 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
private long localHeaderOffset;
JarEntry(JarFile jarFile, String name, CentralDirectoryFileHeader header) {
super(name);
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) {
super(header.getName().toString());
this.jarFile = jarFile;
this.localHeaderOffset = header.getLocalHeaderOffset();
setCompressedSize(header.getCompressedSize());
......
......@@ -200,6 +200,10 @@ public class JarFile extends java.util.jar.JarFile {
return (JarEntry) getEntry(name);
}
public boolean containsEntry(String name) {
return this.entries.containsEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
return this.entries.getEntry(name);
......@@ -320,8 +324,7 @@ public class JarFile extends java.util.jar.JarFile {
@Override
public String getName() {
String path = this.pathFromRoot;
return this.rootFile.getFile() + path;
return this.rootFile.getFile() + this.pathFromRoot;
}
boolean isSigned() {
......
......@@ -66,11 +66,12 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private int[] positions;
private final Map<Integer, JarEntry> entriesCache = Collections
.synchronizedMap(new LinkedHashMap<Integer, JarEntry>(16, 0.75f, true) {
private final Map<Integer, FileHeader> entriesCache = Collections
.synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, JarEntry> eldest) {
protected boolean removeEldestEntry(
Map.Entry<Integer, FileHeader> eldest) {
if (JarFileEntries.this.jarFile.isSigned()) {
return false;
}
......@@ -165,6 +166,10 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
return new EntryIterator();
}
public boolean containsEntry(String name) {
return getEntry(name, FileHeader.class, true) != null;
}
public JarEntry getEntry(String name) {
return getEntry(name, JarEntry.class, true);
}
......@@ -235,21 +240,17 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
@SuppressWarnings("unchecked")
private <T extends FileHeader> T getEntry(int index, Class<T> type,
boolean cacheEntry) {
JarEntry entry = this.entriesCache.get(index);
if (entry != null) {
return (T) entry;
}
try {
CentralDirectoryFileHeader header = CentralDirectoryFileHeader
.fromRandomAccessData(this.centralDirectoryData,
this.centralDirectoryOffsets[index]);
if (FileHeader.class.equals(type)) {
// No need to convert
return (T) header;
FileHeader cached = this.entriesCache.get(index);
FileHeader entry = (cached != null ? cached
: CentralDirectoryFileHeader.fromRandomAccessData(
this.centralDirectoryData,
this.centralDirectoryOffsets[index], this.filter));
if (CentralDirectoryFileHeader.class.equals(entry.getClass())
&& type.equals(JarEntry.class)) {
entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry);
}
entry = new JarEntry(this.jarFile, applyFilter(header.getName()).toString(),
header);
if (cacheEntry) {
if (cacheEntry && cached != entry) {
this.entriesCache.put(index, entry);
}
return (T) entry;
......
......@@ -35,9 +35,15 @@ import java.security.Permission;
* @author Phillip Webb
* @author Andy Wilkinson
*/
class JarURLConnection extends java.net.JarURLConnection {
final class JarURLConnection extends java.net.JarURLConnection {
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException();
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException(
"Jar file or entry not found");
private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException(
FILE_NOT_FOUND_EXCEPTION);
private static final String SEPARATOR = "!/";
......@@ -63,7 +69,8 @@ class JarURLConnection extends java.net.JarURLConnection {
private static final String READ_ACTION = "read";
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection
.notFound();
private final JarFile jarFile;
......@@ -75,48 +82,20 @@ class JarURLConnection extends java.net.JarURLConnection {
private JarEntry jarEntry;
protected JarURLConnection(URL url, JarFile jarFile) throws IOException {
private JarURLConnection(URL url, JarFile jarFile, JarEntryName jarEntryName)
throws IOException {
// What we pass to super is ultimately ignored
super(EMPTY_JAR_URL);
this.url = url;
String spec = extractFullSpec(url, jarFile.getPathFromRoot());
int separator;
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.substring(index));
}
private String extractFullSpec(URL url, String pathFromRoot) {
String file = url.getFile();
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex < 0) {
return "";
}
int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length();
return file.substring(specIndex);
}
private JarFile getNestedJarFile(JarFile jarFile, String name) throws IOException {
JarEntry jarEntry = jarFile.getJarEntry(name);
if (jarEntry == null) {
throwFileNotFound(jarEntry, jarFile);
}
return jarFile.getNestedJarFile(jarEntry);
}
private JarEntryName getJarEntryName(String spec) {
if (spec.length() == 0) {
return EMPTY_JAR_ENTRY_NAME;
}
return new JarEntryName(spec);
this.jarEntryName = jarEntryName;
}
@Override
public void connect() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (!this.jarEntryName.isEmpty() && this.jarEntry == null) {
this.jarEntry = this.jarFile.getJarEntry(getEntryName());
if (this.jarEntry == null) {
......@@ -126,15 +105,6 @@ class JarURLConnection extends java.net.JarURLConnection {
this.connected = true;
}
private void throwFileNotFound(Object entry, JarFile jarFile)
throws FileNotFoundException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException(
"JAR entry " + entry + " not found in " + jarFile.getName());
}
@Override
public JarFile getJarFile() throws IOException {
connect();
......@@ -143,6 +113,9 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
public URL getJarFileURL() {
if (this.jarFile == null) {
throw NOT_FOUND_CONNECTION_EXCEPTION;
}
if (this.jarFileUrl == null) {
this.jarFileUrl = buildJarFileUrl();
}
......@@ -167,7 +140,7 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
public JarEntry getJarEntry() throws IOException {
if (this.jarEntryName.isEmpty()) {
if (this.jarEntryName == null || this.jarEntryName.isEmpty()) {
return null;
}
connect();
......@@ -176,11 +149,17 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
public String getEntryName() {
if (this.jarFile == null) {
throw NOT_FOUND_CONNECTION_EXCEPTION;
}
return this.jarEntryName.toString();
}
@Override
public InputStream getInputStream() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (this.jarEntryName.isEmpty()) {
throw new IOException("no entry name specified");
}
......@@ -192,8 +171,20 @@ class JarURLConnection extends java.net.JarURLConnection {
return inputStream;
}
private void throwFileNotFound(Object entry, JarFile jarFile)
throws FileNotFoundException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException(
"JAR entry " + entry + " not found in " + jarFile.getName());
}
@Override
public int getContentLength() {
if (this.jarFile == null) {
return -1;
}
try {
if (this.jarEntryName.isEmpty()) {
return this.jarFile.size();
......@@ -214,11 +205,14 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
public String getContentType() {
return this.jarEntryName.getContentType();
return (this.jarEntryName == null ? null : this.jarEntryName.getContentType());
}
@Override
public Permission getPermission() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (this.permission == null) {
this.permission = new FilePermission(
this.jarFile.getRootJarFile().getFile().getPath(), READ_ACTION);
......@@ -230,6 +224,56 @@ class JarURLConnection extends java.net.JarURLConnection {
JarURLConnection.useFastExceptions.set(useFastExceptions);
}
static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
String spec = extractFullSpec(url, jarFile.getPathFromRoot());
int separator;
int index = 0;
while ((separator = spec.indexOf(SEPARATOR, index)) > 0) {
String entryName = spec.substring(index, separator);
JarEntry jarEntry = jarFile.getJarEntry(entryName);
if (jarEntry == null) {
return JarURLConnection.notFound(jarFile, JarEntryName.get(entryName));
}
jarFile = jarFile.getNestedJarFile(jarEntry);
index += separator + SEPARATOR.length();
}
JarEntryName jarEntryName = JarEntryName.get(spec, index);
if (Boolean.TRUE.equals(useFastExceptions.get())) {
if (!jarEntryName.isEmpty()
&& !jarFile.containsEntry(jarEntryName.toString())) {
return NOT_FOUND_CONNECTION;
}
}
return new JarURLConnection(url, jarFile, jarEntryName);
}
private static String extractFullSpec(URL url, String pathFromRoot) {
String file = url.getFile();
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex < 0) {
return "";
}
int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length();
return file.substring(specIndex);
}
private static JarURLConnection notFound() {
try {
return notFound(null, null);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName)
throws IOException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
return NOT_FOUND_CONNECTION;
}
return new JarURLConnection(null, jarFile, jarEntryName);
}
/**
* A JarEntryName parsed from a URL String.
*/
......@@ -316,6 +360,17 @@ class JarURLConnection extends java.net.JarURLConnection {
return type;
}
public static JarEntryName get(String spec) {
return get(spec, 0);
}
public static JarEntryName get(String spec, int beginIndex) {
if (spec.length() <= beginIndex) {
return EMPTY_JAR_ENTRY_NAME;
}
return new JarEntryName(spec.substring(beginIndex));
}
}
}
......@@ -18,11 +18,13 @@ package org.springframework.boot.loader.jar;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.TestJarCreator;
......@@ -33,12 +35,16 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link JarURLConnection}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class JarURLConnectionTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder(new File("target"));
@Rule
public ExpectedException thrown = ExpectedException.none();
private File rootJarFile;
private JarFile jarFile;
......@@ -52,76 +58,84 @@ public class JarURLConnectionTests {
@Test
public void connectionToRootUsingAbsoluteUrl() throws Exception {
URL absoluteUrl = new URL("jar:file:" + getAbsolutePath() + "!/");
assertThat(new JarURLConnection(absoluteUrl, this.jarFile).getContent())
URL url = new URL("jar:file:" + getAbsolutePath() + "!/");
assertThat(JarURLConnection.get(url, this.jarFile).getContent())
.isSameAs(this.jarFile);
}
@Test
public void connectionToRootUsingRelativeUrl() throws Exception {
URL relativeUrl = new URL("jar:file:" + getRelativePath() + "!/");
assertThat(new JarURLConnection(relativeUrl, this.jarFile).getContent())
URL url = new URL("jar:file:" + getRelativePath() + "!/");
assertThat(JarURLConnection.get(url, this.jarFile).getContent())
.isSameAs(this.jarFile);
}
@Test
public void connectionToEntryUsingAbsoluteUrl() throws Exception {
URL absoluteUrl = new URL("jar:file:" + getAbsolutePath() + "!/1.dat");
assertThat(new JarURLConnection(absoluteUrl, this.jarFile).getInputStream())
URL url = new URL("jar:file:" + getAbsolutePath() + "!/1.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 }));
}
@Test
public void connectionToEntryUsingRelativeUrl() throws Exception {
URL relativeUrl = new URL("jar:file:" + getRelativePath() + "!/1.dat");
assertThat(new JarURLConnection(relativeUrl, this.jarFile).getInputStream())
URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 }));
}
@Test
public void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix()
throws Exception {
URL absoluteUrl = new URL("jar:file:/" + getAbsolutePath() + "!/1.dat");
assertThat(new JarURLConnection(absoluteUrl, this.jarFile).getInputStream())
URL url = new URL("jar:file:/" + getAbsolutePath() + "!/1.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 }));
}
@Test
public void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception {
URL absoluteUrl = new URL(
"jar:file:" + getAbsolutePath() + "!/nested.jar!/3.dat");
assertThat(new JarURLConnection(absoluteUrl, this.jarFile).getInputStream())
URL url = new URL("jar:file:" + getAbsolutePath() + "!/nested.jar!/3.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception {
URL relativeUrl = new URL(
"jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
assertThat(new JarURLConnection(relativeUrl, this.jarFile).getInputStream())
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.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 }));
URL url = new URL("jar:file:" + getAbsolutePath() + "!/nested.jar!/3.dat");
JarFile nested = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
assertThat(JarURLConnection.get(url, nested).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 }));
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
JarFile nested = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
assertThat(JarURLConnection.get(url, nested).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void nestedJarNotFound() throws Exception {
URL url = new URL(
"jar:file:" + getAbsolutePath() + "!/nested.jar!/missing.jar!/1.dat");
JarFile nested = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
JarURLConnection connection = JarURLConnection.get(url, nested);
this.thrown.expect(FileNotFoundException.class);
this.thrown.expectMessage("JAR entry missing.jar not found in");
connection.connect();
}
private String getAbsolutePath() {
......
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