Skip to content

Commit 19fec06

Browse files
committed
Local root directory and jar caching in PathMatchingResourcePatternResolver
Closes gh-21190
1 parent 729dc0b commit 19fec06

File tree

2 files changed

+137
-22
lines changed

2 files changed

+137
-22
lines changed

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java

+8
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,14 @@ protected void resetCommonCaches() {
10211021
CachedIntrospectionResults.clearClassLoader(getClassLoader());
10221022
}
10231023

1024+
@Override
1025+
public void clearResourceCaches() {
1026+
super.clearResourceCaches();
1027+
if (this.resourcePatternResolver instanceof PathMatchingResourcePatternResolver pmrpr) {
1028+
pmrpr.clearCache();
1029+
}
1030+
}
1031+
10241032

10251033
/**
10261034
* Register a shutdown hook {@linkplain Thread#getName() named}

spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java

+129-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,8 +40,11 @@
4040
import java.util.Enumeration;
4141
import java.util.LinkedHashSet;
4242
import java.util.Map;
43+
import java.util.NavigableSet;
4344
import java.util.Objects;
4445
import java.util.Set;
46+
import java.util.TreeSet;
47+
import java.util.concurrent.ConcurrentHashMap;
4548
import java.util.function.Predicate;
4649
import java.util.jar.JarEntry;
4750
import java.util.jar.JarFile;
@@ -247,6 +250,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
247250

248251
private PathMatcher pathMatcher = new AntPathMatcher();
249252

253+
private final Map<String, Resource[]> rootDirCache = new ConcurrentHashMap<>();
254+
255+
private final Map<String, NavigableSet<String>> jarEntryCache = new ConcurrentHashMap<>();
256+
250257

251258
/**
252259
* Create a {@code PathMatchingResourcePatternResolver} with a
@@ -355,6 +362,16 @@ public Resource[] getResources(String locationPattern) throws IOException {
355362
}
356363
}
357364

365+
/**
366+
* Clear the local resource cache, removing all cached classpath/jar structures.
367+
* @since 6.2
368+
*/
369+
public void clearCache() {
370+
this.rootDirCache.clear();
371+
this.jarEntryCache.clear();
372+
}
373+
374+
358375
/**
359376
* Find all class location resources with the given location via the ClassLoader.
360377
* <p>Delegates to {@link #doFindAllClassPathResources(String)}.
@@ -567,9 +584,73 @@ private boolean hasDuplicate(String filePath, Set<Resource> result) {
567584
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
568585
String rootDirPath = determineRootDir(locationPattern);
569586
String subPattern = locationPattern.substring(rootDirPath.length());
570-
Resource[] rootDirResources = getResources(rootDirPath);
571-
Set<Resource> result = new LinkedHashSet<>(16);
587+
588+
// Look for pre-cached root dir resources, either a direct match
589+
// or for a parent directory in the same classpath locations.
590+
Resource[] rootDirResources = this.rootDirCache.get(rootDirPath);
591+
String actualRootPath = null;
592+
if (rootDirResources == null) {
593+
// No direct match -> search for parent directory match.
594+
String commonPrefix = null;
595+
String existingPath = null;
596+
boolean commonUnique = true;
597+
for (String path : this.rootDirCache.keySet()) {
598+
String currentPrefix = null;
599+
for (int i = 0; i < path.length(); i++) {
600+
if (i == rootDirPath.length() || path.charAt(i) != rootDirPath.charAt(i)) {
601+
currentPrefix = path.substring(0, path.lastIndexOf('/', i - 1) + 1);
602+
break;
603+
}
604+
}
605+
if (currentPrefix != null) {
606+
// A prefix match found, potentially to be turned into a common parent cache entry.
607+
if (commonPrefix == null || !commonUnique || currentPrefix.length() > commonPrefix.length()) {
608+
commonPrefix = currentPrefix;
609+
existingPath = path;
610+
}
611+
else if (currentPrefix.equals(commonPrefix)) {
612+
commonUnique = false;
613+
}
614+
}
615+
else if (actualRootPath == null || path.length() > actualRootPath.length()) {
616+
// A direct match found for a parent directory -> use it.
617+
rootDirResources = this.rootDirCache.get(path);
618+
actualRootPath = path;
619+
}
620+
}
621+
if (rootDirResources == null & StringUtils.hasLength(commonPrefix)) {
622+
// Try common parent directory as long as it points to the same classpath locations.
623+
rootDirResources = getResources(commonPrefix);
624+
Resource[] existingResources = this.rootDirCache.get(existingPath);
625+
if (existingResources != null && rootDirResources.length == existingResources.length) {
626+
// Replace existing subdirectory cache entry with common parent directory.
627+
this.rootDirCache.remove(existingPath);
628+
this.rootDirCache.put(commonPrefix, rootDirResources);
629+
actualRootPath = commonPrefix;
630+
}
631+
else if (commonPrefix.equals(rootDirPath)) {
632+
// The identified common directory is equal to the currently requested path ->
633+
// worth caching specifically, even if it cannot replace the existing sub-entry.
634+
this.rootDirCache.put(rootDirPath, rootDirResources);
635+
}
636+
else {
637+
// Mismatch: parent directory points to more classpath locations.
638+
rootDirResources = null;
639+
}
640+
}
641+
if (rootDirResources == null) {
642+
// Lookup for specific directory, creating a cache entry for it.
643+
rootDirResources = getResources(rootDirPath);
644+
this.rootDirCache.put(rootDirPath, rootDirResources);
645+
}
646+
}
647+
648+
Set<Resource> result = new LinkedHashSet<>(64);
572649
for (Resource rootDirResource : rootDirResources) {
650+
if (actualRootPath != null && actualRootPath.length() < rootDirPath.length()) {
651+
// Create sub-resource for requested sub-location from cached common root directory.
652+
rootDirResource = rootDirResource.createRelative(rootDirPath.substring(actualRootPath.length()));
653+
}
573654
rootDirResource = resolveRootDirResource(rootDirResource);
574655
URL rootDirUrl = rootDirResource.getURL();
575656
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
@@ -672,10 +753,37 @@ protected boolean isJarResource(Resource resource) throws IOException {
672753
protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirUrl, String subPattern)
673754
throws IOException {
674755

756+
String jarFileUrl = null;
757+
String rootEntryPath = null;
758+
759+
String urlFile = rootDirUrl.getFile();
760+
int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
761+
if (separatorIndex == -1) {
762+
separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
763+
}
764+
if (separatorIndex != -1) {
765+
jarFileUrl = urlFile.substring(0, separatorIndex);
766+
rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars
767+
NavigableSet<String> entryCache = this.jarEntryCache.get(jarFileUrl);
768+
if (entryCache != null) {
769+
Set<Resource> result = new LinkedHashSet<>(64);
770+
// Search sorted entries from first entry with rootEntryPath prefix
771+
for (String entryPath : entryCache.tailSet(rootEntryPath, false)) {
772+
if (!entryPath.startsWith(rootEntryPath)) {
773+
// We are beyond the potential matches in the current TreeSet.
774+
break;
775+
}
776+
String relativePath = entryPath.substring(rootEntryPath.length());
777+
if (getPathMatcher().match(subPattern, relativePath)) {
778+
result.add(rootDirResource.createRelative(relativePath));
779+
}
780+
}
781+
return result;
782+
}
783+
}
784+
675785
URLConnection con = rootDirUrl.openConnection();
676786
JarFile jarFile;
677-
String jarFileUrl;
678-
String rootEntryPath;
679787
boolean closeJarFile;
680788

681789
if (con instanceof JarURLConnection jarCon) {
@@ -691,15 +799,8 @@ protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource,
691799
// We'll assume URLs of the format "jar:path!/entry", with the protocol
692800
// being arbitrary as long as following the entry format.
693801
// We'll also handle paths with and without leading "file:" prefix.
694-
String urlFile = rootDirUrl.getFile();
695802
try {
696-
int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
697-
if (separatorIndex == -1) {
698-
separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
699-
}
700-
if (separatorIndex != -1) {
701-
jarFileUrl = urlFile.substring(0, separatorIndex);
702-
rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars
803+
if (jarFileUrl != null) {
703804
jarFile = getJarFile(jarFileUrl);
704805
}
705806
else {
@@ -726,17 +827,21 @@ protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource,
726827
// The Sun JRE does not return a slash here, but BEA JRockit does.
727828
rootEntryPath = rootEntryPath + "/";
728829
}
729-
Set<Resource> result = new LinkedHashSet<>(8);
730-
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
830+
Set<Resource> result = new LinkedHashSet<>(64);
831+
NavigableSet<String> entryCache = new TreeSet<>();
832+
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements(); ) {
731833
JarEntry entry = entries.nextElement();
732834
String entryPath = entry.getName();
835+
entryCache.add(entryPath);
733836
if (entryPath.startsWith(rootEntryPath)) {
734837
String relativePath = entryPath.substring(rootEntryPath.length());
735838
if (getPathMatcher().match(subPattern, relativePath)) {
736839
result.add(rootDirResource.createRelative(relativePath));
737840
}
738841
}
739842
}
843+
// Cache jar entries in TreeSet for efficient searching on re-encounter.
844+
this.jarEntryCache.put(jarFileUrl, entryCache);
740845
return result;
741846
}
742847
finally {
@@ -777,7 +882,7 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException {
777882
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
778883
throws IOException {
779884

780-
Set<Resource> result = new LinkedHashSet<>();
885+
Set<Resource> result = new LinkedHashSet<>(64);
781886
URI rootDirUri;
782887
try {
783888
rootDirUri = rootDirResource.getURI();
@@ -886,7 +991,7 @@ protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource
886991
* @see PathMatcher#match(String, String)
887992
*/
888993
protected Set<Resource> findAllModulePathResources(String locationPattern) throws IOException {
889-
Set<Resource> result = new LinkedHashSet<>(16);
994+
Set<Resource> result = new LinkedHashSet<>(64);
890995

891996
// Skip scanning the module path when running in a native image.
892997
if (NativeDetector.inNativeImage()) {
@@ -987,7 +1092,7 @@ private static class PatternVirtualFileVisitor implements InvocationHandler {
9871092

9881093
private final String rootPath;
9891094

990-
private final Set<Resource> resources = new LinkedHashSet<>();
1095+
private final Set<Resource> resources = new LinkedHashSet<>(64);
9911096

9921097
public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) {
9931098
this.subPattern = subPattern;
@@ -1000,15 +1105,17 @@ public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher
10001105
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
10011106
String methodName = method.getName();
10021107
if (Object.class == method.getDeclaringClass()) {
1003-
switch(methodName) {
1004-
case "equals":
1108+
switch (methodName) {
1109+
case "equals" -> {
10051110
// Only consider equal when proxies are identical.
10061111
return (proxy == args[0]);
1007-
case "hashCode":
1112+
}
1113+
case "hashCode" -> {
10081114
return System.identityHashCode(proxy);
1115+
}
10091116
}
10101117
}
1011-
return switch(methodName) {
1118+
return switch (methodName) {
10121119
case "getAttributes" -> getAttributes();
10131120
case "visit" -> {
10141121
visit(args[0]);

0 commit comments

Comments
 (0)