From a5cddf79a801344588f4469b102d65f8bb0d8f62 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 25 Jun 2016 13:34:32 -0700 Subject: [PATCH] Reduce JarURLConnection allocations Update JarURLConnection & Handler so that a shared static final connection is returned for entries that cannot be found. See gh-6215 --- .../jar/CentralDirectoryFileHeader.java | 9 +- .../loader/jar/CentralDirectoryParser.java | 2 +- .../boot/loader/jar/Handler.java | 6 +- .../boot/loader/jar/JarEntry.java | 4 +- .../boot/loader/jar/JarFile.java | 7 +- .../boot/loader/jar/JarFileEntries.java | 33 ++-- .../boot/loader/jar/JarURLConnection.java | 151 ++++++++++++------ .../loader/jar/JarURLConnectionTests.java | 70 ++++---- 8 files changed, 179 insertions(+), 103 deletions(-) diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java index 4bae7c687a..ad63d39a94 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java @@ -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; } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java index 0186cbc262..4b8a694c22 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java @@ -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() diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java index 04d2595f6b..bb28ba5970 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -1,5 +1,5 @@ /* - * 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); diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index 8b8dda519e..1fbda8de08 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -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()); diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java index bcf5dfc855..ab0a447619 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -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() { diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java index 9c0c62e2c6..59968f9d94 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -66,11 +66,12 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { private int[] positions; - private final Map entriesCache = Collections - .synchronizedMap(new LinkedHashMap(16, 0.75f, true) { + private final Map entriesCache = Collections + .synchronizedMap(new LinkedHashMap(16, 0.75f, true) { @Override - protected boolean removeEldestEntry(Map.Entry eldest) { + protected boolean removeEldestEntry( + Map.Entry eldest) { if (JarFileEntries.this.jarFile.isSigned()) { return false; } @@ -165,6 +166,10 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { 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 { @SuppressWarnings("unchecked") private T getEntry(int index, Class 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; diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java index 9de969d5af..ca2ff46c16 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -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 useFastExceptions = new ThreadLocal(); + + 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 useFastExceptions = new ThreadLocal(); + 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)); + } + } } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java index 943c604b50..32a38cb2ee 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java @@ -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() {