Filter out duplicates in SpringFactoriesLoader

Prior to this commit, SpringFactoriesLoader discovered all registered
factory implementations for a given factory type even if duplicates
were registered within a single META-INF/spring.factories file or in
multiple such files in the classpath.

This commit updates the internals of SpringFactoriesLoader so that
duplicate registrations are ignored, thereby aligning with the
well-known semantics for java.util.ServiceLoader in this regard.

Closes gh-24985
This commit is contained in:
Sam Brannen
2020-04-28 14:55:37 +02:00
parent afc398333e
commit 4e32615b22
3 changed files with 33 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -21,9 +21,12 @@ import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@@ -34,8 +37,6 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -70,7 +71,7 @@ public final class SpringFactoriesLoader {
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>(); private static final Map<ClassLoader, Map<String, Set<String>>> cache = new ConcurrentReferenceHashMap<>();
private SpringFactoriesLoader() { private SpringFactoriesLoader() {
@@ -83,6 +84,9 @@ public final class SpringFactoriesLoader {
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}. * <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>If a custom instantiation strategy is required, use {@link #loadFactoryNames} * <p>If a custom instantiation strategy is required, use {@link #loadFactoryNames}
* to obtain all registered factory names. * to obtain all registered factory names.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* @param factoryType the interface or abstract class representing the factory * @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @throws IllegalArgumentException if any factory implementation class cannot * @throws IllegalArgumentException if any factory implementation class cannot
@@ -111,6 +115,9 @@ public final class SpringFactoriesLoader {
* Load the fully qualified class names of factory implementations of the * Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader. * class loader.
* <p>As of Spring Framework 5.3, if a particular implementation class name
* is discovered more than once for the given factory type, duplicates will
* be ignored.
* @param factoryType the interface or abstract class representing the factory * @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be * @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default * {@code null} to use the default
@@ -119,38 +126,45 @@ public final class SpringFactoriesLoader {
*/ */
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName(); String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); Set<String> factoryNames = loadSpringFactories(classLoader).get(factoryTypeName);
if (factoryNames == null || factoryNames.isEmpty()) {
return Collections.emptyList();
}
return Collections.unmodifiableList(new ArrayList<>(factoryNames));
} }
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { private static Map<String, Set<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader); Map<String, Set<String>> result = cache.get(classLoader);
if (result != null) { if (result != null) {
return result; return result;
} }
result = new HashMap<>();
try { try {
Enumeration<URL> urls = (classLoader != null ? Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) { while (urls.hasMoreElements()) {
URL url = urls.nextElement(); URL url = urls.nextElement();
UrlResource resource = new UrlResource(url); UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource); Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) { for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim(); String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { String[] factoryImplementationNames =
result.add(factoryTypeName, factoryImplementationName.trim()); StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new LinkedHashSet<String>())
.add(factoryImplementationName.trim());
} }
} }
} }
cache.put(classLoader, result); cache.put(classLoader, result);
return result;
} }
catch (IOException ex) { catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" + throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex); FACTORIES_RESOURCE_LOCATION + "]", ex);
} }
return result;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@@ -34,7 +34,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
class SpringFactoriesLoaderTests { class SpringFactoriesLoaderTests {
@Test @Test
void loadFactoriesInCorrectOrder() { void loadFactoriesWithNoRegisteredImplementations() {
List<Integer> factories = SpringFactoriesLoader.loadFactories(Integer.class, null);
assertThat(factories).isEmpty();
}
@Test
void loadFactoriesInCorrectOrderWithDuplicateRegistrationsPresent() {
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, null); List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, null);
assertThat(factories).hasSize(2); assertThat(factories).hasSize(2);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class); assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);

View File

@@ -1,5 +1,6 @@
org.springframework.core.io.support.DummyFactory =\ org.springframework.core.io.support.DummyFactory =\
org.springframework.core.io.support.MyDummyFactory2, \ org.springframework.core.io.support.MyDummyFactory2, \
org.springframework.core.io.support.MyDummyFactory1, \
org.springframework.core.io.support.MyDummyFactory1 org.springframework.core.io.support.MyDummyFactory1
java.lang.String=\ java.lang.String=\