Merge pull request #46289 from move-hoon

* pr/46289:
  Polish "Optimize DevTools resource lookup performance"
  Optimize DevTools resource lookup performance

Closes gh-46289
This commit is contained in:
Moritz Halbritter
2025-09-01 10:52:46 +02:00
2 changed files with 40 additions and 34 deletions
@@ -33,7 +33,6 @@ import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFileURLStreamHandler;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceDirectory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.io.AbstractResource;
@@ -125,15 +124,13 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe
private List<Resource> getAdditionalResources(String locationPattern) throws MalformedURLException {
List<Resource> additionalResources = new ArrayList<>();
String trimmedLocationPattern = trimLocationPattern(locationPattern);
for (SourceDirectory sourceDirectory : this.classLoaderFiles.getSourceDirectories()) {
for (Entry<String, ClassLoaderFile> entry : sourceDirectory.getFilesEntrySet()) {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() != Kind.DELETED && this.antPathMatcher.match(trimmedLocationPattern, name)) {
URL url = new URL("reloaded", null, -1, "/" + name, new ClassLoaderFileURLStreamHandler(file));
UrlResource resource = new UrlResource(url);
additionalResources.add(resource);
}
for (Entry<String, ClassLoaderFile> entry : this.classLoaderFiles.getFileEntries()) {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() != Kind.DELETED && this.antPathMatcher.match(trimmedLocationPattern, name)) {
URL url = new URL("reloaded", null, -1, "/" + name, new ClassLoaderFileURLStreamHandler(file));
UrlResource resource = new UrlResource(url);
additionalResources.add(resource);
}
}
return additionalResources;
@@ -149,20 +146,18 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe
}
private boolean isDeleted(Resource resource) {
for (SourceDirectory sourceDirectory : this.classLoaderFiles.getSourceDirectories()) {
for (Entry<String, ClassLoaderFile> entry : sourceDirectory.getFilesEntrySet()) {
try {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() == Kind.DELETED && resource.exists()
&& resource.getURI().toString().endsWith(name)) {
return true;
}
}
catch (IOException ex) {
throw new IllegalStateException("Failed to retrieve URI from '" + resource + "'", ex);
for (Entry<String, ClassLoaderFile> entry : this.classLoaderFiles.getFileEntries()) {
try {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() == Kind.DELETED && resource.exists()
&& resource.getURI().toString().endsWith(name)) {
return true;
}
}
catch (IOException ex) {
throw new IllegalStateException("Failed to retrieve URI from '" + resource + "'", ex);
}
}
return false;
}
@@ -42,15 +42,23 @@ import org.springframework.util.Assert;
*/
public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable {
@Serial
private static final long serialVersionUID = 1;
private final Map<String, SourceDirectory> sourceDirectories;
/**
* A flattened map of all files from all source directories for fast, O(1) lookups.
* The key is the file's relative path, and the value is the ClassLoaderFile.
*/
private final Map<String, ClassLoaderFile> filesByName;
/**
* Create a new {@link ClassLoaderFiles} instance.
*/
public ClassLoaderFiles() {
this.sourceDirectories = new LinkedHashMap<>();
this.filesByName = new LinkedHashMap<>();
}
/**
@@ -60,6 +68,7 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable
public ClassLoaderFiles(ClassLoaderFiles classLoaderFiles) {
Assert.notNull(classLoaderFiles, "'classLoaderFiles' must not be null");
this.sourceDirectories = new LinkedHashMap<>(classLoaderFiles.sourceDirectories);
this.filesByName = new LinkedHashMap<>(classLoaderFiles.filesByName);
}
/**
@@ -97,12 +106,14 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable
Assert.notNull(file, "'file' must not be null");
removeAll(name);
getOrCreateSourceDirectory(sourceDirectory).add(name, file);
this.filesByName.put(name, file);
}
private void removeAll(String name) {
for (SourceDirectory sourceDirectory : this.sourceDirectories.values()) {
sourceDirectory.remove(name);
}
this.filesByName.remove(name);
}
/**
@@ -128,22 +139,22 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable
* @return the size of the collection
*/
public int size() {
int size = 0;
for (SourceDirectory sourceDirectory : this.sourceDirectories.values()) {
size += sourceDirectory.getFiles().size();
}
return size;
return this.filesByName.size();
}
@Override
public @Nullable ClassLoaderFile getFile(String name) {
for (SourceDirectory sourceDirectory : this.sourceDirectories.values()) {
ClassLoaderFile file = sourceDirectory.get(name);
if (file != null) {
return file;
}
}
return null;
return this.filesByName.get(name);
}
/**
* Returns a set of all file entries across all source directories for efficient
* iteration.
* @return a set of all file entries
* @since 4.0.0
*/
public Set<Entry<String, ClassLoaderFile>> getFileEntries() {
return Collections.unmodifiableSet(this.filesByName.entrySet());
}
/**