Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/main/java/com/goterl/resourceloader/ResourceLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ public class ResourceLoader {
* @throws URISyntaxException If cannot find the resource file.
*/
public File copyToTempDirectory(String relativePath, Class outsideClass) throws IOException, URISyntaxException {
// Fast path: when the resource is a single file reachable through
// the standard ClassLoader API we can read it in O(1) without
// unpacking the host JAR. Unpacking the whole JAR (legacy path
// below) is O(N) in the total entry count of that JAR and on a fat
// / Spring Boot uberjar (~100k entries, several hundred MB) it adds
// 10-20 seconds of pure I/O to startup. The legacy path is
// preserved as a fallback for:
// - directory resources (handled via copyDirectory in the legacy
// path; we explicitly skip them here),
// - nested-jar layouts where getResource returns null
// (e.g. Spring Boot loader's BOOT-INF/lib/<inner>.jar!/...).
File fastPathResult = tryFastPath(relativePath, outsideClass);
if (fastPathResult != null) {
return fastPathResult;
}

// Create a "main" temporary directory in which
// everything can be thrown in.
File mainTempDir = createMainTempDirectory();
Expand All @@ -88,6 +104,70 @@ public File copyToTempDirectory(String relativePath, Class outsideClass) throws
return getFileFromFileSystem(relativePath, mainTempDir);
}

/**
* Attempts to satisfy the resource request via the standard
* {@link ClassLoader#getResource} / {@link ClassLoader#getResourceAsStream}
* APIs, which are O(1) in the resource directory. Returns the extracted
* file on success, or {@code null} when the resource is a directory, is
* not reachable through the classloader, or otherwise needs the legacy
* JAR-extraction path.
*/
private File tryFastPath(String relativePath, Class outsideClass) throws IOException {
if (relativePath == null || relativePath.endsWith("/")) {
return null;
}
String classpathName = relativePath.startsWith("/")
? relativePath.substring(1)
: relativePath;
if (classpathName.isEmpty()) {
return null;
}
ClassLoader cl = (outsideClass != null)
? outsideClass.getClassLoader()
: Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = ClassLoader.getSystemClassLoader();
}
if (cl == null) {
return null;
}
URL resource = cl.getResource(classpathName);
if (resource == null) {
return null;
}
// Skip the fast path when the resource is a directory: directory
// entries on most classloaders yield a synthetic listing stream
// which is not a file we can sensibly copy.
String urlString = resource.toString();
if (urlString.endsWith("/")) {
return null;
}
String protocol = resource.getProtocol();
if ("file".equals(protocol)) {
try {
if (new File(resource.toURI()).isDirectory()) {
return null;
}
} catch (URISyntaxException e) {
return null;
}
}
try (InputStream is = resource.openStream()) {
if (is == null) {
return null;
}
File fastTempDir = createMainTempDirectory();
fastTempDir.mkdirs();
int slash = classpathName.lastIndexOf('/');
String filename = (slash >= 0)
? classpathName.substring(slash + 1)
: classpathName;
File out = new File(fastTempDir, filename);
Files.copy(is, out.toPath(), StandardCopyOption.REPLACE_EXISTING);
return out;
}
}

public File extractFromWithinAJarFile(URL jarPath, File mainTempDir, String relativePath)
throws IOException, URISyntaxException {
if (jarPath == null) {
Expand Down
20 changes: 20 additions & 0 deletions src/test/java/com/goterl/resourceloader/ResourceLoaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ public void nestedExtractTest(String url, String path) throws NoSuchMethodExcept
assertThat(file.exists()).isTrue();
}

@DataProvider(name = "fastPathTestData")
public static Object[][] fastPathTestData() {
return new Object[][] {
{"test1.txt"},
{"/test1.txt"}
};
}

@Test(dataProvider = "fastPathTestData")
public void copyToTempDirectoryFastPathExtractsClasspathResource(String relativePath) throws IOException, URISyntaxException {
ResourceLoader loader = new ResourceLoader();
File file = loader.copyToTempDirectory(relativePath, ResourceLoaderTest.class);

assertThat(file).isNotNull();
assertThat(file.exists()).isTrue();
assertThat(file.isFile()).isTrue();
assertThat(file.getName()).isEqualTo("test1.txt");
assertThat(file.length()).isGreaterThan(0);
}

private static boolean delete(String path) {
File filePath = new File(path);
String[] list = filePath.list();
Expand Down