Skip to content

Commit 3e41807

Browse files
move-hoonmhalbritter
authored andcommitted
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 <[email protected]>
1 parent 4761e13 commit 3e41807

File tree

2 files changed

+39
-34
lines changed

2 files changed

+39
-34
lines changed

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

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,13 @@ public Resource[] getResources(String locationPattern) throws IOException {
125125
private List<Resource> getAdditionalResources(String locationPattern) throws MalformedURLException {
126126
List<Resource> additionalResources = new ArrayList<>();
127127
String trimmedLocationPattern = trimLocationPattern(locationPattern);
128-
for (SourceDirectory sourceDirectory : this.classLoaderFiles.getSourceDirectories()) {
129-
for (Entry<String, ClassLoaderFile> entry : sourceDirectory.getFilesEntrySet()) {
130-
String name = entry.getKey();
131-
ClassLoaderFile file = entry.getValue();
132-
if (file.getKind() != Kind.DELETED && this.antPathMatcher.match(trimmedLocationPattern, name)) {
133-
URL url = new URL("reloaded", null, -1, "/" + name, new ClassLoaderFileURLStreamHandler(file));
134-
UrlResource resource = new UrlResource(url);
135-
additionalResources.add(resource);
136-
}
128+
for (Entry<String, ClassLoaderFile> entry : this.classLoaderFiles.getFileEntries()) {
129+
String name = entry.getKey();
130+
ClassLoaderFile file = entry.getValue();
131+
if (file.getKind() != Kind.DELETED && this.antPathMatcher.match(trimmedLocationPattern, name)) {
132+
URL url = new URL("reloaded", null, -1, "/" + name, new ClassLoaderFileURLStreamHandler(file));
133+
UrlResource resource = new UrlResource(url);
134+
additionalResources.add(resource);
137135
}
138136
}
139137
return additionalResources;
@@ -149,20 +147,18 @@ private String trimLocationPattern(String pattern) {
149147
}
150148

151149
private boolean isDeleted(Resource resource) {
152-
for (SourceDirectory sourceDirectory : this.classLoaderFiles.getSourceDirectories()) {
153-
for (Entry<String, ClassLoaderFile> entry : sourceDirectory.getFilesEntrySet()) {
154-
try {
155-
String name = entry.getKey();
156-
ClassLoaderFile file = entry.getValue();
157-
if (file.getKind() == Kind.DELETED && resource.exists()
158-
&& resource.getURI().toString().endsWith(name)) {
159-
return true;
160-
}
161-
}
162-
catch (IOException ex) {
163-
throw new IllegalStateException("Failed to retrieve URI from '" + resource + "'", ex);
150+
for (Entry<String, ClassLoaderFile> entry : this.classLoaderFiles.getFileEntries()) {
151+
try {
152+
String name = entry.getKey();
153+
ClassLoaderFile file = entry.getValue();
154+
if (file.getKind() == Kind.DELETED && resource.exists()
155+
&& resource.getURI().toString().endsWith(name)) {
156+
return true;
164157
}
165158
}
159+
catch (IOException ex) {
160+
throw new IllegalStateException("Failed to retrieve URI from '" + resource + "'", ex);
161+
}
166162
}
167163
return false;
168164
}

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

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,18 @@ public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable
4646

4747
private final Map<String, SourceDirectory> sourceDirectories;
4848

49+
/**
50+
* A flattened map of all files from all source directories for fast, O(1) lookups.
51+
* The key is the file's relative path, and the value is the ClassLoaderFile.
52+
*/
53+
private final Map<String, ClassLoaderFile> filesByName;
54+
4955
/**
5056
* Create a new {@link ClassLoaderFiles} instance.
5157
*/
5258
public ClassLoaderFiles() {
5359
this.sourceDirectories = new LinkedHashMap<>();
60+
this.filesByName = new LinkedHashMap<>();
5461
}
5562

5663
/**
@@ -60,6 +67,7 @@ public ClassLoaderFiles() {
6067
public ClassLoaderFiles(ClassLoaderFiles classLoaderFiles) {
6168
Assert.notNull(classLoaderFiles, "'classLoaderFiles' must not be null");
6269
this.sourceDirectories = new LinkedHashMap<>(classLoaderFiles.sourceDirectories);
70+
this.filesByName = new LinkedHashMap<>(classLoaderFiles.filesByName);
6371
}
6472

6573
/**
@@ -97,12 +105,14 @@ public void addFile(String sourceDirectory, String name, ClassLoaderFile file) {
97105
Assert.notNull(file, "'file' must not be null");
98106
removeAll(name);
99107
getOrCreateSourceDirectory(sourceDirectory).add(name, file);
108+
this.filesByName.put(name, file);
100109
}
101110

102111
private void removeAll(String name) {
103112
for (SourceDirectory sourceDirectory : this.sourceDirectories.values()) {
104113
sourceDirectory.remove(name);
105114
}
115+
this.filesByName.remove(name);
106116
}
107117

108118
/**
@@ -128,22 +138,21 @@ public Collection<SourceDirectory> getSourceDirectories() {
128138
* @return the size of the collection
129139
*/
130140
public int size() {
131-
int size = 0;
132-
for (SourceDirectory sourceDirectory : this.sourceDirectories.values()) {
133-
size += sourceDirectory.getFiles().size();
134-
}
135-
return size;
141+
return this.filesByName.size();
136142
}
137143

138144
@Override
139-
public @Nullable ClassLoaderFile getFile(String name) {
140-
for (SourceDirectory sourceDirectory : this.sourceDirectories.values()) {
141-
ClassLoaderFile file = sourceDirectory.get(name);
142-
if (file != null) {
143-
return file;
144-
}
145-
}
146-
return null;
145+
public ClassLoaderFile getFile(String name) {
146+
return this.filesByName.get(name);
147+
}
148+
149+
/**
150+
* Returns a set of all file entries across all source directories for efficient
151+
* iteration.
152+
* @return a set of all file entries
153+
*/
154+
public Set<Entry<String, ClassLoaderFile>> getFileEntries() {
155+
return Collections.unmodifiableSet(this.filesByName.entrySet());
147156
}
148157

149158
/**

0 commit comments

Comments
 (0)