Local root directory and jar caching in PathMatchingResourcePatternResolver

Closes gh-21190
This commit is contained in:
Juergen Hoeller 2024-03-08 19:12:14 +01:00
parent 729dc0b671
commit 19fec0633f
2 changed files with 137 additions and 22 deletions

View File

@ -1021,6 +1021,14 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
CachedIntrospectionResults.clearClassLoader(getClassLoader());
}
@Override
public void clearResourceCaches() {
super.clearResourceCaches();
if (this.resourcePatternResolver instanceof PathMatchingResourcePatternResolver pmrpr) {
pmrpr.clearCache();
}
}
/**
* Register a shutdown hook {@linkplain Thread#getName() named}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@ -40,8 +40,11 @@ import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@ -247,6 +250,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private PathMatcher pathMatcher = new AntPathMatcher();
private final Map<String, Resource[]> rootDirCache = new ConcurrentHashMap<>();
private final Map<String, NavigableSet<String>> jarEntryCache = new ConcurrentHashMap<>();
/**
* Create a {@code PathMatchingResourcePatternResolver} with a
@ -355,6 +362,16 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
/**
* Clear the local resource cache, removing all cached classpath/jar structures.
* @since 6.2
*/
public void clearCache() {
this.rootDirCache.clear();
this.jarEntryCache.clear();
}
/**
* Find all class location resources with the given location via the ClassLoader.
* <p>Delegates to {@link #doFindAllClassPathResources(String)}.
@ -567,9 +584,73 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
// Look for pre-cached root dir resources, either a direct match
// or for a parent directory in the same classpath locations.
Resource[] rootDirResources = this.rootDirCache.get(rootDirPath);
String actualRootPath = null;
if (rootDirResources == null) {
// No direct match -> search for parent directory match.
String commonPrefix = null;
String existingPath = null;
boolean commonUnique = true;
for (String path : this.rootDirCache.keySet()) {
String currentPrefix = null;
for (int i = 0; i < path.length(); i++) {
if (i == rootDirPath.length() || path.charAt(i) != rootDirPath.charAt(i)) {
currentPrefix = path.substring(0, path.lastIndexOf('/', i - 1) + 1);
break;
}
}
if (currentPrefix != null) {
// A prefix match found, potentially to be turned into a common parent cache entry.
if (commonPrefix == null || !commonUnique || currentPrefix.length() > commonPrefix.length()) {
commonPrefix = currentPrefix;
existingPath = path;
}
else if (currentPrefix.equals(commonPrefix)) {
commonUnique = false;
}
}
else if (actualRootPath == null || path.length() > actualRootPath.length()) {
// A direct match found for a parent directory -> use it.
rootDirResources = this.rootDirCache.get(path);
actualRootPath = path;
}
}
if (rootDirResources == null & StringUtils.hasLength(commonPrefix)) {
// Try common parent directory as long as it points to the same classpath locations.
rootDirResources = getResources(commonPrefix);
Resource[] existingResources = this.rootDirCache.get(existingPath);
if (existingResources != null && rootDirResources.length == existingResources.length) {
// Replace existing subdirectory cache entry with common parent directory.
this.rootDirCache.remove(existingPath);
this.rootDirCache.put(commonPrefix, rootDirResources);
actualRootPath = commonPrefix;
}
else if (commonPrefix.equals(rootDirPath)) {
// The identified common directory is equal to the currently requested path ->
// worth caching specifically, even if it cannot replace the existing sub-entry.
this.rootDirCache.put(rootDirPath, rootDirResources);
}
else {
// Mismatch: parent directory points to more classpath locations.
rootDirResources = null;
}
}
if (rootDirResources == null) {
// Lookup for specific directory, creating a cache entry for it.
rootDirResources = getResources(rootDirPath);
this.rootDirCache.put(rootDirPath, rootDirResources);
}
}
Set<Resource> result = new LinkedHashSet<>(64);
for (Resource rootDirResource : rootDirResources) {
if (actualRootPath != null && actualRootPath.length() < rootDirPath.length()) {
// Create sub-resource for requested sub-location from cached common root directory.
rootDirResource = rootDirResource.createRelative(rootDirPath.substring(actualRootPath.length()));
}
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
@ -672,10 +753,37 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirUrl, String subPattern)
throws IOException {
String jarFileUrl = null;
String rootEntryPath = null;
String urlFile = rootDirUrl.getFile();
int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
if (separatorIndex == -1) {
separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
}
if (separatorIndex != -1) {
jarFileUrl = urlFile.substring(0, separatorIndex);
rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars
NavigableSet<String> entryCache = this.jarEntryCache.get(jarFileUrl);
if (entryCache != null) {
Set<Resource> result = new LinkedHashSet<>(64);
// Search sorted entries from first entry with rootEntryPath prefix
for (String entryPath : entryCache.tailSet(rootEntryPath, false)) {
if (!entryPath.startsWith(rootEntryPath)) {
// We are beyond the potential matches in the current TreeSet.
break;
}
String relativePath = entryPath.substring(rootEntryPath.length());
if (getPathMatcher().match(subPattern, relativePath)) {
result.add(rootDirResource.createRelative(relativePath));
}
}
return result;
}
}
URLConnection con = rootDirUrl.openConnection();
JarFile jarFile;
String jarFileUrl;
String rootEntryPath;
boolean closeJarFile;
if (con instanceof JarURLConnection jarCon) {
@ -691,15 +799,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
// We'll assume URLs of the format "jar:path!/entry", with the protocol
// being arbitrary as long as following the entry format.
// We'll also handle paths with and without leading "file:" prefix.
String urlFile = rootDirUrl.getFile();
try {
int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
if (separatorIndex == -1) {
separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
}
if (separatorIndex != -1) {
jarFileUrl = urlFile.substring(0, separatorIndex);
rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars
if (jarFileUrl != null) {
jarFile = getJarFile(jarFileUrl);
}
else {
@ -726,10 +827,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
// The Sun JRE does not return a slash here, but BEA JRockit does.
rootEntryPath = rootEntryPath + "/";
}
Set<Resource> result = new LinkedHashSet<>(8);
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
Set<Resource> result = new LinkedHashSet<>(64);
NavigableSet<String> entryCache = new TreeSet<>();
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements(); ) {
JarEntry entry = entries.nextElement();
String entryPath = entry.getName();
entryCache.add(entryPath);
if (entryPath.startsWith(rootEntryPath)) {
String relativePath = entryPath.substring(rootEntryPath.length());
if (getPathMatcher().match(subPattern, relativePath)) {
@ -737,6 +840,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
}
// Cache jar entries in TreeSet for efficient searching on re-encounter.
this.jarEntryCache.put(jarFileUrl, entryCache);
return result;
}
finally {
@ -777,7 +882,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
throws IOException {
Set<Resource> result = new LinkedHashSet<>();
Set<Resource> result = new LinkedHashSet<>(64);
URI rootDirUri;
try {
rootDirUri = rootDirResource.getURI();
@ -886,7 +991,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
* @see PathMatcher#match(String, String)
*/
protected Set<Resource> findAllModulePathResources(String locationPattern) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
Set<Resource> result = new LinkedHashSet<>(64);
// Skip scanning the module path when running in a native image.
if (NativeDetector.inNativeImage()) {
@ -987,7 +1092,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private final String rootPath;
private final Set<Resource> resources = new LinkedHashSet<>();
private final Set<Resource> resources = new LinkedHashSet<>(64);
public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) {
this.subPattern = subPattern;
@ -1000,15 +1105,17 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (Object.class == method.getDeclaringClass()) {
switch(methodName) {
case "equals":
switch (methodName) {
case "equals" -> {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
case "hashCode":
}
case "hashCode" -> {
return System.identityHashCode(proxy);
}
}
}
return switch(methodName) {
return switch (methodName) {
case "getAttributes" -> getAttributes();
case "visit" -> {
visit(args[0]);