Support multiple SpringFactoriesLoader files

Update `SpringFactoriesLoader` so that it can load files from
arbitrary locations. An instance of the loader class itself is now
returned from static factory methods that accept different
locations.

The recent `ArgumentResolver` and `FailureHandler` `loadFactories`
variants are now no longer available as static methods. They are
still available as instance methods.

The `loadFactories` static method remains to provide
back-compatibility with Spring Framework 5.x

See gh-28416
This commit is contained in:
Phillip Webb
2022-04-13 16:39:18 -07:00
parent f17372ebea
commit 58c441f961
5 changed files with 207 additions and 105 deletions

View File

@@ -62,8 +62,10 @@ public class BeanDefinitionsContribution implements BeanFactoryContribution {
}
private static List<BeanRegistrationContributionProvider> initializeProviders(DefaultListableBeanFactory beanFactory) {
List<BeanRegistrationContributionProvider> providers = new ArrayList<>(SpringFactoriesLoader.loadFactories(
BeanRegistrationContributionProvider.class, beanFactory.getBeanClassLoader(), ArgumentResolver.from(type -> type.isInstance(beanFactory) ? beanFactory : null)));
List<BeanRegistrationContributionProvider> providers = new ArrayList<>(
SpringFactoriesLoader.forDefaultResourceLocation(beanFactory.getBeanClassLoader()).load(
BeanRegistrationContributionProvider.class,
ArgumentResolver.from(type -> type.isInstance(beanFactory) ? beanFactory : null)));
providers.add(new DefaultBeanRegistrationContributionProvider(beanFactory));
return providers;
}

View File

@@ -25,7 +25,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -89,7 +89,7 @@ import org.springframework.util.StringUtils;
* @author Phillip Webb
* @since 3.2
*/
public final class SpringFactoriesLoader {
public class SpringFactoriesLoader {
/**
* The location to look for factories.
@@ -97,17 +97,59 @@ public final class SpringFactoriesLoader {
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static final ArgumentResolver NO_ARGUMENT_RESOLVER = null;
private static final FailureHandler NO_FAILURE_HANDLER = null;
private static final FailureHandler THROWING_FAILURE_HANDLER = FailureHandler.throwing();
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
private static final FailureHandler THROWING_HANDLER = FailureHandler.throwing();
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();
static final Map<ClassLoader, Map<String, SpringFactoriesLoader>> cache = new ConcurrentReferenceHashMap<>();
private SpringFactoriesLoader() {
@Nullable
private final ClassLoader classLoader;
private final Map<String, List<String>> factories;
private SpringFactoriesLoader(@Nullable ClassLoader classLoader, String resourceLocation) {
this.classLoader = classLoader;
this.factories = loadFactoriesResource((classLoader != null) ? classLoader
: SpringFactoriesLoader.class.getClassLoader(), resourceLocation);
}
protected SpringFactoriesLoader(@Nullable ClassLoader classLoader, Map<String, List<String>> factories) {
this.classLoader = classLoader;
this.factories = factories;
}
private Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
Map<String, List<String>> result = new LinkedHashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(resourceLocation);
while (urls.hasMoreElements()) {
UrlResource resource = new UrlResource(urls.nextElement());
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
properties.forEach((name, value) -> {
List<String> implementations = result.computeIfAbsent(((String) name).trim(), key -> new ArrayList<>());
Arrays.stream(StringUtils.commaDelimitedListToStringArray((String) value))
.map(String::trim).forEach(implementations::add);
});
}
result.replaceAll(this::toDistinctUnmodifiableList);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
}
return Collections.unmodifiableMap(result);
}
private List<String> toDistinctUnmodifiableList(String factoryType, List<String> implementations) {
return implementations.stream().distinct().toList();
}
/**
* Load and instantiate the factory implementations of the given type from
@@ -121,12 +163,11 @@ public final class SpringFactoriesLoader {
* 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 classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
return loadFactories(factoryType, classLoader, null, null);
public <T> List<T> load(Class<T> factoryType) {
return load(factoryType, NO_ARGUMENT_RESOLVER, NO_FAILURE_HANDLER);
}
/**
@@ -138,16 +179,13 @@ public final class SpringFactoriesLoader {
* 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 classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @param argumentResolver strategy used to resolve constructor arguments by their type
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
* @since 6.0
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
@Nullable ArgumentResolver argumentResolver) {
return loadFactories(factoryType, classLoader, argumentResolver, null);
public <T> List<T> load(Class<T> factoryType, @Nullable ArgumentResolver argumentResolver) {
return load(factoryType, argumentResolver, NO_FAILURE_HANDLER);
}
/**
@@ -161,14 +199,11 @@ public final class SpringFactoriesLoader {
* <p>For any factory implementation class that cannot be loaded or error that occurs while
* instantiating it, the given failure handler is called.
* @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 failureHandler strategy used to handle factory instantiation failures
* @since 6.0
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
@Nullable FailureHandler failureHandler) {
return loadFactories(factoryType, classLoader, null, failureHandler);
public <T> List<T> load(Class<T> factoryType, @Nullable FailureHandler failureHandler) {
return load(factoryType, NO_ARGUMENT_RESOLVER, failureHandler);
}
/**
@@ -183,24 +218,18 @@ public final class SpringFactoriesLoader {
* <p>For any factory implementation class that cannot be loaded or error that occurs while
* instantiating it, the given failure handler is called.
* @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 argumentResolver strategy used to resolve constructor arguments by their type
* @param failureHandler strategy used to handle factory instantiation failures
* @since 6.0
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
@Nullable ArgumentResolver argumentResolver, @Nullable FailureHandler failureHandler) {
public <T> List<T> load(Class<T> factoryType, @Nullable ArgumentResolver argumentResolver, @Nullable FailureHandler failureHandler) {
Assert.notNull(factoryType, "'factoryType' must not be null");
ClassLoader classLoaderToUse = (classLoader != null ? classLoader
: SpringFactoriesLoader.class.getClassLoader());
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
logger.trace(LogMessage.format("Loaded [%s] names: %s", factoryType.getName(), factoryImplementationNames));
List<T> result = new ArrayList<>(factoryImplementationNames.size());
FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_HANDLER;
for (String factoryImplementationName : factoryImplementationNames) {
T factory = instantiateFactory(factoryImplementationName, factoryType,
classLoaderToUse, argumentResolver, failureHandlerToUse);
List<String> implementationNames = loadFactoryNames(factoryType);
logger.trace(LogMessage.format("Loaded [%s] names: %s", factoryType.getName(), implementationNames));
List<T> result = new ArrayList<>(implementationNames.size());
FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_FAILURE_HANDLER;
for (String implementationName : implementationNames) {
T factory = instantiateFactory(implementationName, factoryType, argumentResolver, failureHandlerToUse);
if (factory != null) {
result.add(factory);
}
@@ -209,6 +238,44 @@ public final class SpringFactoriesLoader {
return result;
}
private List<String> loadFactoryNames(Class<?> factoryType) {
return this.factories.getOrDefault(factoryType.getName(), Collections.emptyList());
}
@Nullable
protected <T> T instantiateFactory(String implementationName, Class<T> type, @Nullable ArgumentResolver argumentResolver, FailureHandler failureHandler) {
try {
Class<?> factoryImplementationClass = ClassUtils.forName(implementationName, this.classLoader);
Assert.isTrue(type.isAssignableFrom(factoryImplementationClass),
() -> "Class [" + implementationName + "] is not assignable to factory type [" + type.getName() + "]");
FactoryInstantiator<T> factoryInstantiator = FactoryInstantiator.forClass(factoryImplementationClass);
return factoryInstantiator.instantiate(argumentResolver);
}
catch (Throwable ex) {
failureHandler.handleFailure(type, implementationName, ex);
return null;
}
}
/**
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <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.
* <p>For more advanced factory loading with {@link ArgumentResolver} or
* {@link FailureHandler} support use {@link #forDefaultResourceLocation(ClassLoader)}
* to obtain a {@link SpringFactoriesLoader} instance.
* @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)
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
return forDefaultResourceLocation(classLoader).load(factoryType);
}
/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
@@ -223,68 +290,70 @@ public final class SpringFactoriesLoader {
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = (classLoader != null ? classLoader
: SpringFactoriesLoader.class.getClassLoader());
String factoryTypeName = factoryType.getName();
return getAllFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
return forDefaultResourceLocation(classLoader).loadFactoryNames(factoryType);
}
private static Map<String, List<String>> getAllFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = loadAllFactories(classLoader);
cache.put(classLoader, result);
return result;
/**
* Return a {@link SpringFactoriesLoader} instance that will load and
* instantiate the factory implementations from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the default class loader.
* @return a {@link SpringFactoriesLoader} instance
* @since 6.0
* @see #forDefaultResourceLocation(ClassLoader)
*/
public static SpringFactoriesLoader forDefaultResourceLocation() {
return forDefaultResourceLocation(null);
}
private static Map<String, List<String>> loadAllFactories(ClassLoader classLoader) {
Map<String, List<String>> result;
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
UrlResource resource = new UrlResource(urls.nextElement());
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return Collections.unmodifiableMap(result);
/**
* Return a {@link SpringFactoriesLoader} instance that will load and
* instantiate the factory implementations from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader.
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @return a {@link SpringFactoriesLoader} instance
* @since 6.0
* @see #forDefaultResourceLocation()
*/
public static SpringFactoriesLoader forDefaultResourceLocation(@Nullable ClassLoader classLoader) {
return forResourceLocation(classLoader, FACTORIES_RESOURCE_LOCATION);
}
private static List<String> toDistinctUnmodifiableList(String factoryType, List<String> implementations) {
return implementations.stream().distinct().toList();
/**
* Return a {@link SpringFactoriesLoader} instance that will load and
* instantiate the factory implementations from the given location, using
* the default class loader.
* @return a {@link SpringFactoriesLoader} instance
* @since 6.0
* @see #forResourceLocation(ClassLoader, String)
*/
public static SpringFactoriesLoader forResourceLocation(String resourceLocation) {
return forResourceLocation(null, resourceLocation);
}
@Nullable
private static <T> T instantiateFactory(String factoryImplementationName,
Class<T> factoryType, ClassLoader classLoader, @Nullable ArgumentResolver argumentResolver,
FailureHandler failureHandler) {
try {
Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
Assert.isTrue(factoryType.isAssignableFrom(factoryImplementationClass),
() -> "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
FactoryInstantiator<T> factoryInstantiator = FactoryInstantiator.forClass(factoryImplementationClass);
return factoryInstantiator.instantiate(argumentResolver);
/**
* Return a {@link SpringFactoriesLoader} instance that will load and
* instantiate the factory implementations from the given location, using
* the given class loader.
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @return a {@link SpringFactoriesLoader} instance
* @since 6.0
* @see #forResourceLocation(String)
*/
public static SpringFactoriesLoader forResourceLocation(@Nullable ClassLoader classLoader, String resourceLocation) {
Assert.hasText(resourceLocation, "'resourceLocation' must not be empty");
Map<String, SpringFactoriesLoader> loaders = SpringFactoriesLoader.cache.get(classLoader);
if (loaders == null) {
loaders = new ConcurrentReferenceHashMap<>();
SpringFactoriesLoader.cache.put(classLoader, loaders);
}
catch (Throwable ex) {
failureHandler.handleFailure(factoryType, factoryImplementationName, ex);
return null;
SpringFactoriesLoader loader = loaders.get(resourceLocation);
if (loader == null) {
loader = new SpringFactoriesLoader(classLoader, resourceLocation);
loaders.put(resourceLocation, loader);
}
return loader;
}
@@ -302,6 +371,7 @@ public final class SpringFactoriesLoader {
this.constructor = constructor;
}
T instantiate(@Nullable ArgumentResolver argumentResolver) throws Exception {
Object[] args = resolveArgs(argumentResolver);
if (isKotlinType(this.constructor.getDeclaringClass())) {
@@ -364,6 +434,7 @@ public final class SpringFactoriesLoader {
}
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
*/
@@ -535,6 +606,7 @@ public final class SpringFactoriesLoader {
}
/**
* Strategy for handling a failure that occurs when instantiating a factory.
*

View File

@@ -74,48 +74,48 @@ class SpringFactoriesLoaderTests {
}
@Test
void loadFactoriesWithNoRegisteredImplementations() {
List<Integer> factories = SpringFactoriesLoader.loadFactories(Integer.class, null);
void loadWhenNoRegisteredImplementationsReturnsEmptyList() {
List<Integer> factories = SpringFactoriesLoader.forDefaultResourceLocation().load(Integer.class);
assertThat(factories).isEmpty();
}
@Test
void loadFactoriesInCorrectOrderWithDuplicateRegistrationsPresent() {
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, null);
void loadWhenDuplicateRegistrationsPresentReturnsListInCorrectOrder() {
List<DummyFactory> factories = SpringFactoriesLoader.forDefaultResourceLocation().load(DummyFactory.class);
assertThat(factories).hasSize(2);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
}
@Test
void loadPackagePrivateFactory() {
void loadWhenPackagePrivateFactory() {
List<DummyPackagePrivateFactory> factories =
SpringFactoriesLoader.loadFactories(DummyPackagePrivateFactory.class, null);
SpringFactoriesLoader.forDefaultResourceLocation().load(DummyPackagePrivateFactory.class);
assertThat(factories).hasSize(1);
assertThat(Modifier.isPublic(factories.get(0).getClass().getModifiers())).isFalse();
}
@Test
void attemptToLoadFactoryOfIncompatibleType() {
void loadWhenIncompatibleTypeThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> SpringFactoriesLoader.loadFactories(String.class, null))
.isThrownBy(() -> SpringFactoriesLoader.forDefaultResourceLocation().load(String.class))
.withMessageContaining("Unable to instantiate factory class "
+ "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]");
}
@Test
void attemptToLoadFactoryOfIncompatibleTypeWithLoggingFailureHandler() {
void loadWithLoggingFailureHandlerWhenIncompatibleTypeReturnsEmptyList() {
Log logger = mock(Log.class);
FailureHandler failureHandler = FailureHandler.logging(logger);
List<String> factories = SpringFactoriesLoader.loadFactories(String.class, null, failureHandler);
List<String> factories = SpringFactoriesLoader.forDefaultResourceLocation().load(String.class, failureHandler);
assertThat(factories).isEmpty();
}
@Test
void loadFactoryWithNonDefaultConstructor() {
void loadWithArgumentResolverWhenNoDefaultConstructor() {
ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(DummyFactory.class,
LimitedClassLoader.constructorArgumentFactories, resolver);
List<DummyFactory> factories = SpringFactoriesLoader.forDefaultResourceLocation(LimitedClassLoader.constructorArgumentFactories)
.load(DummyFactory.class, resolver);
assertThat(factories).hasSize(3);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
@@ -124,27 +124,51 @@ class SpringFactoriesLoaderTests {
}
@Test
void loadFactoryWithMultipleConstructors() {
void loadWhenMultipleConstructorsThrowsException() {
ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
assertThatIllegalArgumentException()
.isThrownBy(() -> SpringFactoriesLoader.loadFactories(DummyFactory.class,
LimitedClassLoader.multipleArgumentFactories, resolver))
.isThrownBy(() -> SpringFactoriesLoader.forDefaultResourceLocation(LimitedClassLoader.multipleArgumentFactories)
.load(DummyFactory.class, resolver))
.withMessageContaining("Unable to instantiate factory class "
+ "[org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] for factory type [org.springframework.core.io.support.DummyFactory]")
.havingRootCause().withMessageContaining("Class [org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] has no suitable constructor");
}
@Test
void loadFactoryWithMissingArgumentUsingLoggingFailureHandler() {
void loadWithLoggingFailureHandlerWhenMissingArgumentDropsItem() {
Log logger = mock(Log.class);
FailureHandler failureHandler = FailureHandler.logging(logger);
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(
DummyFactory.class, LimitedClassLoader.multipleArgumentFactories, failureHandler);
List<DummyFactory> factories = SpringFactoriesLoader.forDefaultResourceLocation(LimitedClassLoader.multipleArgumentFactories)
.load(DummyFactory.class, failureHandler);
assertThat(factories).hasSize(2);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
}
@Test
void loadFactoriesLoadsFromDefaultLocation() {
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(
DummyFactory.class, null);
assertThat(factories).hasSize(2);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
}
@Test
void loadForResourceLocationWhenLocationDoesNotExistReturnsEmptyList() {
List<DummyFactory> factories = SpringFactoriesLoader.forResourceLocation(
"META-INF/missing/missing-spring.factories").load(DummyFactory.class);
assertThat(factories).isEmpty();
}
@Test
void loadForResourceLocationLoadsFactories() {
List<DummyFactory> factories = SpringFactoriesLoader.forResourceLocation(
"META-INF/custom/custom-spring.factories").load(DummyFactory.class);
assertThat(factories).hasSize(1);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
}
@Nested
class FailureHandlerTests {

View File

@@ -0,0 +1,2 @@
org.springframework.core.io.support.DummyFactory =\
org.springframework.core.io.support.MyDummyFactory1

View File

@@ -0,0 +1,2 @@
org.springframework.core.io.support.DummyFactory =\
org.springframework.core.io.support.MyDummyFactory1