Ensure that JarFileArchive unpacks entries to unique location

Previously, JarFileArchive would always unpack any entries marked for
unpacking to ${java.io.tmpdir}/spring-boot-libs. This could cause
problems if multiple Spring Boot applications were running on the same
host:

- If the apps are run as different users the first application would
  create the spring-boot-libs directory and the second and subsequent
  applications may not have write permissions to that directory
- Multiple apps may overwrite each others unpacked libs. At best this
  will mean one copy of a jar is overwritten with another identical
  copy. At worst the jars may have different contents so that some of
  the contents of the original jar disappear unexpectedly.

This commit updates JarFileArchive to use an application-specific
location when unpacking libs. A directory beneath ${java.io.tmpdir} is
still used but it's now named <jar-file-name>-spring-boot-libs-<uuid>.
A loop, limited to 1000 attempts, is used to avoid problems caused by
the uuid clashing.

Closes gh-4124
This commit is contained in:
Andy Wilkinson 2015-10-09 14:37:33 +01:00
parent f7d2bafb3e
commit 888fa90265
3 changed files with 76 additions and 24 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2015 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.
@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
@ -40,6 +41,7 @@ import org.springframework.boot.loader.util.AsciiBytes;
* {@link Archive} implementation backed by a {@link JarFile}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class JarFileArchive extends Archive {
@ -53,6 +55,8 @@ public class JarFileArchive extends Archive {
private URL url;
private File tempUnpackFolder;
public JarFileArchive(File file) throws IOException {
this(file, null);
}
@ -123,10 +127,25 @@ public class JarFileArchive extends Archive {
}
private File getTempUnpackFolder() {
File tempFolder = new File(System.getProperty("java.io.tmpdir"));
File unpackFolder = new File(tempFolder, "spring-boot-libs");
unpackFolder.mkdirs();
return unpackFolder;
if (this.tempUnpackFolder == null) {
File tempFolder = new File(System.getProperty("java.io.tmpdir"));
this.tempUnpackFolder = createUnpackFolder(tempFolder);
}
return this.tempUnpackFolder;
}
private File createUnpackFolder(File parent) {
int attempts = 0;
while (attempts++ < 1000) {
String fileName = new File(this.jarFile.getName()).getName();
File unpackFolder = new File(parent,
fileName + "-spring-boot-libs-" + UUID.randomUUID());
if (unpackFolder.mkdirs()) {
return unpackFolder;
}
}
throw new IllegalStateException(
"Failed to create unpack folder in directory '" + parent + "'");
}
private void unpack(JarEntryData data, File file) throws IOException {

View File

@ -50,27 +50,33 @@ public abstract class TestJarCreator {
writeDirEntry(jarOutputStream, "special/");
writeEntry(jarOutputStream, "special/\u00EB.dat", '\u00EB');
JarEntry nestedEntry = new JarEntry("nested.jar");
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());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutputStream.putNextEntry(nestedEntry);
jarOutputStream.write(nestedJarData);
jarOutputStream.closeEntry();
writeNestedEntry("nested.jar", unpackNested, jarOutputStream);
writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream);
}
finally {
jarOutputStream.close();
}
}
private static void writeNestedEntry(String name, boolean unpackNested,
JarOutputStream jarOutputStream) throws Exception, IOException {
JarEntry nestedEntry = new JarEntry(name);
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());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutputStream.putNextEntry(nestedEntry);
jarOutputStream.write(nestedJarData);
jarOutputStream.closeEntry();
}
private static byte[] getNestedJarData() throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2015 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.
@ -31,6 +31,8 @@ import org.springframework.boot.loader.util.AsciiBytes;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
@ -38,6 +40,7 @@ import static org.junit.Assert.assertThat;
* Tests for {@link JarFileArchive}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class JarFileArchiveTests {
@ -57,8 +60,7 @@ public class JarFileArchiveTests {
private void setup(boolean unpackNested) throws Exception {
this.rootJarFile = this.temporaryFolder.newFile();
this.rootJarFileUrl = rootJarFile.toURI().toString();
System.out.println(rootJarFileUrl);
this.rootJarFileUrl = this.rootJarFile.toURI().toString();
TestJarCreator.createTestJar(this.rootJarFile, unpackNested);
this.archive = new JarFileArchive(this.rootJarFile);
}
@ -72,7 +74,7 @@ public class JarFileArchiveTests {
@Test
public void getEntries() throws Exception {
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
assertThat(entries.size(), equalTo(9));
assertThat(entries.size(), equalTo(10));
}
@Test
@ -98,6 +100,31 @@ public class JarFileArchiveTests {
assertThat(nested.getUrl().toString(), endsWith(".jar"));
}
@Test
public void unpackedLocationsAreUniquePerArchive() throws Exception {
setup(true);
Entry entry = getEntriesMap(this.archive).get("nested.jar");
URL firstNested = this.archive.getNestedArchive(entry).getUrl();
setup(true);
entry = getEntriesMap(this.archive).get("nested.jar");
URL secondNested = this.archive.getNestedArchive(entry).getUrl();
assertThat(secondNested, is(not(equalTo(firstNested))));
}
@Test
public void unpackedLocationsFromSameArchiveShareSameParent() throws Exception {
setup(true);
File nested = new File(this.archive
.getNestedArchive(getEntriesMap(this.archive).get("nested.jar")).getUrl()
.toURI());
File anotherNested = new File(
this.archive
.getNestedArchive(
getEntriesMap(this.archive).get("another-nested.jar"))
.getUrl().toURI());
assertThat(nested.getParent(), is(equalTo(anotherNested.getParent())));
}
@Test
public void getFilteredArchive() throws Exception {
Archive filteredArchive = this.archive