Add support for unpacking nested JARs
Update the executable JAR code to automatically unpack any entries which include an entry comment starting `UNPACK:` to the temp folder. The existing Maven and Gradle plugins have been updated with new configuration options and the `spring-boot-tools` project has been updated to write the appropriate entry comment based on a flag passed in via the `Library` class. This support has been added to allow libraries such a JRuby (which assumes that `jruby-complete.jar` is always accessible as file) to work with Spring Boot executable jars. Fixes gh-1070
This commit is contained in:
@@ -17,7 +17,10 @@
|
||||
package org.springframework.boot.loader.archive;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
@@ -27,6 +30,7 @@ import java.util.List;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
import org.springframework.boot.loader.jar.JarEntryData;
|
||||
import org.springframework.boot.loader.jar.JarEntryFilter;
|
||||
import org.springframework.boot.loader.jar.JarFile;
|
||||
@@ -39,12 +43,23 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
||||
*/
|
||||
public class JarFileArchive extends Archive {
|
||||
|
||||
private static final AsciiBytes UNPACK_MARKER = new AsciiBytes("UNPACK:");
|
||||
|
||||
private static final int BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
private final JarFile jarFile;
|
||||
|
||||
private final List<Entry> entries;
|
||||
|
||||
private URL url;
|
||||
|
||||
public JarFileArchive(File file) throws IOException {
|
||||
this(file, null);
|
||||
}
|
||||
|
||||
public JarFileArchive(File file, URL url) throws IOException {
|
||||
this(new JarFile(file));
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public JarFileArchive(JarFile jarFile) {
|
||||
@@ -58,6 +73,9 @@ public class JarFileArchive extends Archive {
|
||||
|
||||
@Override
|
||||
public URL getUrl() throws MalformedURLException {
|
||||
if (this.url != null) {
|
||||
return this.url;
|
||||
}
|
||||
return this.jarFile.getUrl();
|
||||
}
|
||||
|
||||
@@ -84,10 +102,54 @@ public class JarFileArchive extends Archive {
|
||||
|
||||
protected Archive getNestedArchive(Entry entry) throws IOException {
|
||||
JarEntryData data = ((JarFileEntry) entry).getJarEntryData();
|
||||
if (data.getComment().startsWith(UNPACK_MARKER)) {
|
||||
return getUnpackedNestedArchive(data);
|
||||
}
|
||||
JarFile jarFile = this.jarFile.getNestedJarFile(data);
|
||||
return new JarFileArchive(jarFile);
|
||||
}
|
||||
|
||||
private Archive getUnpackedNestedArchive(JarEntryData data) throws IOException {
|
||||
AsciiBytes hash = data.getComment().substring(UNPACK_MARKER.length());
|
||||
String name = data.getName().toString();
|
||||
if (name.lastIndexOf("/") != -1) {
|
||||
name = name.substring(name.lastIndexOf("/") + 1);
|
||||
}
|
||||
File file = new File(getTempUnpackFolder(), hash.toString() + "-" + name);
|
||||
if (!file.exists() || file.length() != data.getSize()) {
|
||||
unpack(data, file);
|
||||
}
|
||||
return new JarFileArchive(file, file.toURI().toURL());
|
||||
}
|
||||
|
||||
private File getTempUnpackFolder() {
|
||||
File tempFolder = new File(System.getProperty("java.io.tmpdir"));
|
||||
File unpackFolder = new File(tempFolder, "spring-boot-libs");
|
||||
unpackFolder.mkdirs();
|
||||
return unpackFolder;
|
||||
}
|
||||
|
||||
private void unpack(JarEntryData data, File file) throws IOException {
|
||||
InputStream inputStream = data.getData().getInputStream(ResourceAccess.ONCE);
|
||||
try {
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
try {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
finally {
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
|
||||
JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
|
||||
|
||||
@@ -96,10 +96,11 @@ public class Handler extends URLStreamHandler {
|
||||
return openConnection(getFallbackHandler(), url);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
this.logger.log(Level.WARNING, "Unable to open fallback handler", ex);
|
||||
if (reason instanceof IOException) {
|
||||
this.logger.log(Level.FINEST, "Unable to open fallback handler", ex);
|
||||
throw (IOException) reason;
|
||||
}
|
||||
this.logger.log(Level.WARNING, "Unable to open fallback handler", ex);
|
||||
if (reason instanceof RuntimeException) {
|
||||
throw (RuntimeException) reason;
|
||||
}
|
||||
@@ -111,7 +112,6 @@ public class Handler extends URLStreamHandler {
|
||||
if (this.fallbackHandler != null) {
|
||||
return this.fallbackHandler;
|
||||
}
|
||||
|
||||
for (String handlerClassName : FALLBACK_HANDLERS) {
|
||||
try {
|
||||
Class<?> handlerClass = Class.forName(handlerClassName);
|
||||
|
||||
@@ -93,7 +93,13 @@ public final class JarEntryData {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
RandomAccessData getData() throws IOException {
|
||||
/**
|
||||
* @return the underlying {@link RandomAccessData} for this entry. Generally this
|
||||
* method should not be called directly and instead data should be accessed via
|
||||
* {@link JarFile#getInputStream(ZipEntry)}.
|
||||
* @throws IOException
|
||||
*/
|
||||
public RandomAccessData getData() throws IOException {
|
||||
if (this.data == null) {
|
||||
// aspectjrt-1.7.4.jar has a different ext bytes length in the
|
||||
// local directory to the central directory. We need to re-read
|
||||
|
||||
@@ -35,6 +35,10 @@ import java.util.zip.ZipEntry;
|
||||
public abstract class TestJarCreator {
|
||||
|
||||
public static void createTestJar(File file) throws Exception {
|
||||
createTestJar(file, false);
|
||||
}
|
||||
|
||||
public static void createTestJar(File file, boolean unpackNested) throws Exception {
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(file);
|
||||
JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream);
|
||||
try {
|
||||
@@ -50,6 +54,9 @@ public abstract class TestJarCreator {
|
||||
byte[] nestedJarData = getNestedJarData();
|
||||
nestedEntry.setSize(nestedJarData.length);
|
||||
nestedEntry.setCompressedSize(nestedJarData.length);
|
||||
if (unpackNested) {
|
||||
nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000");
|
||||
}
|
||||
CRC32 crc32 = new CRC32();
|
||||
crc32.update(nestedJarData);
|
||||
nestedEntry.setCrc(crc32.getValue());
|
||||
|
||||
@@ -29,7 +29,9 @@ import org.springframework.boot.loader.TestJarCreator;
|
||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||
import org.springframework.boot.loader.util.AsciiBytes;
|
||||
|
||||
import static org.hamcrest.Matchers.endsWith;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
@@ -48,8 +50,12 @@ public class JarFileArchiveTests {
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
setup(false);
|
||||
}
|
||||
|
||||
private void setup(boolean unpackNested) throws Exception {
|
||||
this.rootJarFile = this.temporaryFolder.newFile();
|
||||
TestJarCreator.createTestJar(this.rootJarFile);
|
||||
TestJarCreator.createTestJar(this.rootJarFile, unpackNested);
|
||||
this.archive = new JarFileArchive(this.rootJarFile);
|
||||
}
|
||||
|
||||
@@ -80,6 +86,15 @@ public class JarFileArchiveTests {
|
||||
equalTo("jar:file:" + this.rootJarFile.getPath() + "!/nested.jar!/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNestedUnpackedArchive() throws Exception {
|
||||
setup(true);
|
||||
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||
Archive nested = this.archive.getNestedArchive(entry);
|
||||
assertThat(nested.getUrl().toString(), startsWith("file:"));
|
||||
assertThat(nested.getUrl().toString(), endsWith(".jar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFilteredArchive() throws Exception {
|
||||
Archive filteredArchive = this.archive
|
||||
|
||||
Reference in New Issue
Block a user