Browse Source

Optimize DevTools resource lookup performance

The resource resolver in DevTools can cause performance degradation
during application restarts in large projects. Key methods like
isDeleted() and getAdditionalResources() rely on nested loops, leading
to O(n*m) complexity.

This commit refactors ClassLoaderFiles to use a pre-computed, flattened
map. This provides O(1) complexity for direct lookups and allows for
efficient single-loop iteration.

The ClassLoaderFilesResourcePatternResolver is updated to leverage this
new, efficient structure:

- getFile() and size() are improved from O(n) to O(1).
- isDeleted() and getAdditionalResources() are improved from O(n*m) to
  O(m) by eliminating nested loops.
- Data consistency is maintained across all operations.

This optimization significantly improves restart performance with a
minimal memory footprint, while preserving the existing API and
exception handling behavior.

See gh-46289

Signed-off-by: DongHoon Lee <dhl1924@naver.com>
pull/47012/head
DongHoon Lee 6 months ago committed by Moritz Halbritter
parent
commit
3e41807e1d
  1. 38
      module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolver.java
  2. 35
      module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFiles.java

38
module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolver.java

@ -125,15 +125,13 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe @@ -125,15 +125,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 +147,18 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe @@ -149,20 +147,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;
}

35
module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFiles.java

@ -46,11 +46,18 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable @@ -46,11 +46,18 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable
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 +67,7 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable @@ -60,6 +67,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 +105,14 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable @@ -97,12 +105,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 +138,21 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable @@ -128,22 +138,21 @@ 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;
public ClassLoaderFile getFile(String name) {
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
*/
public Set<Entry<String, ClassLoaderFile>> getFileEntries() {
return Collections.unmodifiableSet(this.filesByName.entrySet());
}
/**

Loading…
Cancel
Save