Skip to content

Commit 4cebd9d

Browse files
committed
Fix SpringFactoriesLoader cache key when using default classloader
Update `SpringFactoriesLoader` so that `null` is never used for the cache key. Prior to this commit, calling `forDefaultResourceLocation` with `null` and `ClassUtils.getDefaultClassLoader()` would provide different `SpringFactoriesLoader` instances rather than making use of a single shared cached instance. See gh-28416
1 parent eb50a6f commit 4cebd9d

File tree

2 files changed

+44
-34
lines changed

2 files changed

+44
-34
lines changed

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

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -114,43 +114,17 @@ public class SpringFactoriesLoader {
114114
private final Map<String, List<String>> factories;
115115

116116

117-
private SpringFactoriesLoader(@Nullable ClassLoader classLoader, String resourceLocation) {
118-
this.classLoader = classLoader;
119-
this.factories = loadFactoriesResource((classLoader != null) ? classLoader
120-
: SpringFactoriesLoader.class.getClassLoader(), resourceLocation);
121-
}
122-
117+
/**
118+
* Create a new {@link SpringFactoriesLoader} instance.
119+
* @param classLoader the classloader used to instantiate the factories
120+
* @param factories a map of factory class name to implementation class names
121+
*/
123122
protected SpringFactoriesLoader(@Nullable ClassLoader classLoader, Map<String, List<String>> factories) {
124123
this.classLoader = classLoader;
125124
this.factories = factories;
126125
}
127126

128127

129-
private Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
130-
Map<String, List<String>> result = new LinkedHashMap<>();
131-
try {
132-
Enumeration<URL> urls = classLoader.getResources(resourceLocation);
133-
while (urls.hasMoreElements()) {
134-
UrlResource resource = new UrlResource(urls.nextElement());
135-
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
136-
properties.forEach((name, value) -> {
137-
List<String> implementations = result.computeIfAbsent(((String) name).trim(), key -> new ArrayList<>());
138-
Arrays.stream(StringUtils.commaDelimitedListToStringArray((String) value))
139-
.map(String::trim).forEach(implementations::add);
140-
});
141-
}
142-
result.replaceAll(this::toDistinctUnmodifiableList);
143-
}
144-
catch (IOException ex) {
145-
throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
146-
}
147-
return Collections.unmodifiableMap(result);
148-
}
149-
150-
private List<String> toDistinctUnmodifiableList(String factoryType, List<String> implementations) {
151-
return implementations.stream().distinct().toList();
152-
}
153-
154128
/**
155129
* Load and instantiate the factory implementations of the given type from
156130
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader and
@@ -343,19 +317,47 @@ public static SpringFactoriesLoader forResourceLocation(String resourceLocation)
343317
*/
344318
public static SpringFactoriesLoader forResourceLocation(@Nullable ClassLoader classLoader, String resourceLocation) {
345319
Assert.hasText(resourceLocation, "'resourceLocation' must not be empty");
346-
Map<String, SpringFactoriesLoader> loaders = SpringFactoriesLoader.cache.get(classLoader);
320+
ClassLoader resourceClassLoader = (classLoader != null) ? classLoader
321+
: SpringFactoriesLoader.class.getClassLoader();
322+
Map<String, SpringFactoriesLoader> loaders = SpringFactoriesLoader.cache.get(resourceClassLoader);
347323
if (loaders == null) {
348324
loaders = new ConcurrentReferenceHashMap<>();
349-
SpringFactoriesLoader.cache.put(classLoader, loaders);
325+
SpringFactoriesLoader.cache.put(resourceClassLoader, loaders);
350326
}
351327
SpringFactoriesLoader loader = loaders.get(resourceLocation);
352328
if (loader == null) {
353-
loader = new SpringFactoriesLoader(classLoader, resourceLocation);
329+
Map<String, List<String>> factories = loadFactoriesResource(resourceClassLoader, resourceLocation);
330+
loader = new SpringFactoriesLoader(classLoader, factories);
354331
loaders.put(resourceLocation, loader);
355332
}
356333
return loader;
357334
}
358335

336+
private static Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
337+
Map<String, List<String>> result = new LinkedHashMap<>();
338+
try {
339+
Enumeration<URL> urls = classLoader.getResources(resourceLocation);
340+
while (urls.hasMoreElements()) {
341+
UrlResource resource = new UrlResource(urls.nextElement());
342+
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
343+
properties.forEach((name, value) -> {
344+
List<String> implementations = result.computeIfAbsent(((String) name).trim(), key -> new ArrayList<>());
345+
Arrays.stream(StringUtils.commaDelimitedListToStringArray((String) value))
346+
.map(String::trim).forEach(implementations::add);
347+
});
348+
}
349+
result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
350+
}
351+
catch (IOException ex) {
352+
throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
353+
}
354+
return Collections.unmodifiableMap(result);
355+
}
356+
357+
private static List<String> toDistinctUnmodifiableList(String factoryType, List<String> implementations) {
358+
return implementations.stream().distinct().toList();
359+
}
360+
359361

360362
/**
361363
* Internal instantiator used to create the factory instance.

spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator;
3535
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
3636
import org.springframework.core.log.LogMessage;
37+
import org.springframework.util.ClassUtils;
3738

3839
import static org.assertj.core.api.Assertions.assertThat;
3940
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -169,6 +170,13 @@ void loadForResourceLocationLoadsFactories() {
169170
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
170171
}
171172

173+
@Test
174+
void sameCachedResultIsUsedForDefaultClassLoaderAndNullClassLoader() {
175+
SpringFactoriesLoader forNull = SpringFactoriesLoader.forDefaultResourceLocation(null);
176+
SpringFactoriesLoader forDefault = SpringFactoriesLoader.forDefaultResourceLocation(ClassUtils.getDefaultClassLoader());
177+
assertThat(forNull).isSameAs(forDefault);
178+
}
179+
172180

173181
@Nested
174182
class FailureHandlerTests {

0 commit comments

Comments
 (0)