Local root directory and jar caching in PathMatchingResourcePatternResolver
Closes gh-21190
This commit is contained in:
parent
729dc0b671
commit
19fec0633f
|
@ -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}
|
||||
|
|
|
@ -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]);
|
||||
|
|
Loading…
Reference in New Issue