diff --git a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java
index abba0cbd86..73b87db505 100644
--- a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java
+++ b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,23 +17,40 @@
package org.springframework.core.io.support;
import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import kotlin.jvm.JvmClassMappingKt;
+import kotlin.reflect.KFunction;
+import kotlin.reflect.KParameter;
+import kotlin.reflect.full.KClasses;
+import kotlin.reflect.jvm.KCallablesJvm;
+import kotlin.reflect.jvm.ReflectJvmMapping;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.springframework.core.KotlinDetector;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.UrlResource;
+import org.springframework.core.log.LogMessage;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -52,10 +69,24 @@ import org.springframework.util.StringUtils;
*
* where {@code example.MyService} is the name of the interface, and {@code MyServiceImpl1}
* and {@code MyServiceImpl2} are two implementations.
+ *
>> cache = new ConcurrentReferenceHashMap<>();
@@ -78,10 +111,12 @@ public final class SpringFactoriesLoader {
/**
* Load and instantiate the factory implementations of the given type from
- * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader.
+ * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader and
+ * a default argument resolver that expects a no-arg constructor.
* The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
- *
If a custom instantiation strategy is required, use {@link #loadFactoryNames}
- * to obtain all registered factory names.
+ *
If a custom instantiation strategy is required, use {@code loadFactories}
+ * with a custom {@link ArgumentResolver ArgumentResolver} and/or
+ * {@link FailureHandler FailureHandler}.
*
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.
@@ -89,21 +124,86 @@ public final class SpringFactoriesLoader {
* @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
- * @see #loadFactoryNames
*/
public static List loadFactories(Class factoryType, @Nullable ClassLoader classLoader) {
+ return loadFactories(factoryType, classLoader, null, null);
+ }
+
+ /**
+ * Load and instantiate the factory implementations of the given type from
+ * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader and
+ * argument resolver.
+ * The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
+ *
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 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 List loadFactories(Class factoryType, @Nullable ClassLoader classLoader,
+ @Nullable ArgumentResolver argumentResolver) {
+
+ return loadFactories(factoryType, classLoader, argumentResolver, null);
+ }
+
+ /**
+ * Load and instantiate the factory implementations of the given type from
+ * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader with
+ * custom failure handling provided by the given failure handler.
+ * The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
+ *
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.
+ *
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 List loadFactories(Class factoryType, @Nullable ClassLoader classLoader,
+ @Nullable FailureHandler failureHandler) {
+
+ return loadFactories(factoryType, classLoader, null, failureHandler);
+ }
+
+ /**
+ * Load and instantiate the factory implementations of the given type from
+ * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader,
+ * argument resolver, and custom failure handling provided by the given
+ * failure handler.
+ * The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
+ *
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.
+ *
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 List loadFactories(Class factoryType, @Nullable ClassLoader classLoader,
+ @Nullable ArgumentResolver argumentResolver, @Nullable FailureHandler failureHandler) {
+
Assert.notNull(factoryType, "'factoryType' must not be null");
- ClassLoader classLoaderToUse = classLoader;
- if (classLoaderToUse == null) {
- classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
- }
+ ClassLoader classLoaderToUse = (classLoader != null ? classLoader
+ : SpringFactoriesLoader.class.getClassLoader());
List factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
- if (logger.isTraceEnabled()) {
- logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
- }
+ logger.trace(LogMessage.format("Loaded [%s] names: %s", factoryType.getName(), factoryImplementationNames));
List result = new ArrayList<>(factoryImplementationNames.size());
+ FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_HANDLER;
for (String factoryImplementationName : factoryImplementationNames) {
- result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
+ T factory = instantiateFactory(factoryImplementationName, factoryType,
+ classLoaderToUse, argumentResolver, failureHandlerToUse);
+ if (factory != null) {
+ result.add(factory);
+ }
}
AnnotationAwareOrderComparator.sort(result);
return result;
@@ -123,26 +223,29 @@ public final class SpringFactoriesLoader {
* @see #loadFactories
*/
public static List loadFactoryNames(Class> factoryType, @Nullable ClassLoader classLoader) {
- ClassLoader classLoaderToUse = classLoader;
- if (classLoaderToUse == null) {
- classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
- }
+ ClassLoader classLoaderToUse = (classLoader != null ? classLoader
+ : SpringFactoriesLoader.class.getClassLoader());
String factoryTypeName = factoryType.getName();
- return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
+ return getAllFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
- private static Map> loadSpringFactories(ClassLoader classLoader) {
+ private static Map> getAllFactories(ClassLoader classLoader) {
Map> result = cache.get(classLoader);
if (result != null) {
return result;
}
+ result = loadAllFactories(classLoader);
+ cache.put(classLoader, result);
+ return result;
+ }
+ private static Map> loadAllFactories(ClassLoader classLoader) {
+ Map> result;
result = new HashMap<>();
try {
Enumeration urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
- URL url = urls.nextElement();
- UrlResource resource = new UrlResource(url);
+ UrlResource resource = new UrlResource(urls.nextElement());
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
@@ -154,33 +257,351 @@ public final class SpringFactoriesLoader {
}
}
}
-
- // Replace all lists with unmodifiable lists containing unique elements
- result.replaceAll((factoryType, implementations) -> implementations.stream().distinct().toList());
- cache.put(classLoader, result);
+ result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
- return result;
+ return Collections.unmodifiableMap(result);
}
- @SuppressWarnings("unchecked")
- private static T instantiateFactory(String factoryImplementationName, Class factoryType, ClassLoader classLoader) {
+ private static List toDistinctUnmodifiableList(String factoryType, List implementations) {
+ return implementations.stream().distinct().toList();
+ }
+
+ @Nullable
+ private static T instantiateFactory(String factoryImplementationName,
+ Class factoryType, ClassLoader classLoader, @Nullable ArgumentResolver argumentResolver,
+ FailureHandler failureHandler) {
try {
Class> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
- if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
- throw new IllegalArgumentException(
- "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
- }
- return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance();
+ Assert.isTrue(factoryType.isAssignableFrom(factoryImplementationClass),
+ () -> "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
+ FactoryInstantiator factoryInstantiator = FactoryInstantiator.forClass(factoryImplementationClass);
+ return factoryInstantiator.instantiate(argumentResolver);
}
catch (Throwable ex) {
- throw new IllegalArgumentException(
- "Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]",
- ex);
+ failureHandler.handleFailure(factoryType, factoryImplementationName, ex);
+ return null;
}
}
+
+ /**
+ * Internal instantiator used to create the factory instance.
+ * @param the instance implementation type
+ */
+ static final class FactoryInstantiator {
+
+ private final Constructor constructor;
+
+
+ private FactoryInstantiator(Constructor constructor) {
+ ReflectionUtils.makeAccessible(constructor);
+ this.constructor = constructor;
+ }
+
+ T instantiate(@Nullable ArgumentResolver argumentResolver) throws Exception {
+ Object[] args = resolveArgs(argumentResolver);
+ if (isKotlinType(this.constructor.getDeclaringClass())) {
+ return KotlinDelegate.instantiate(this.constructor, args);
+ }
+ return this.constructor.newInstance(args);
+ }
+
+ private Object[] resolveArgs(@Nullable ArgumentResolver argumentResolver) {
+ Class>[] types = this.constructor.getParameterTypes();
+ return (argumentResolver != null ?
+ Arrays.stream(types).map(argumentResolver::resolve).toArray() :
+ new Object[types.length]);
+ }
+
+ @SuppressWarnings("unchecked")
+ static FactoryInstantiator forClass(Class> factoryImplementationClass) {
+ Constructor> constructor = findConstructor(factoryImplementationClass);
+ Assert.state(constructor != null, "Class [" + factoryImplementationClass.getName() + "] has no suitable constructor");
+ return new FactoryInstantiator<>((Constructor) constructor);
+ }
+
+ @Nullable
+ private static Constructor> findConstructor(Class> factoryImplementationClass) {
+ // Same algorithm as BeanUtils.getResolvableConstructor
+ Constructor> constructor = findPrimaryKotlinConstructor(factoryImplementationClass);
+ constructor = (constructor != null ? constructor :
+ findSingleConstructor(factoryImplementationClass.getConstructors()));
+ constructor = (constructor != null ? constructor :
+ findSingleConstructor(factoryImplementationClass.getDeclaredConstructors()));
+ constructor = (constructor != null ? constructor :
+ findDeclaredConstructor(factoryImplementationClass));
+ return constructor;
+ }
+
+ @Nullable
+ private static Constructor> findPrimaryKotlinConstructor(Class> factoryImplementationClass) {
+ return (isKotlinType(factoryImplementationClass)
+ ? KotlinDelegate.findPrimaryConstructor(factoryImplementationClass) : null);
+ }
+
+ private static boolean isKotlinType(Class> factoryImplementationClass) {
+ return KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(factoryImplementationClass);
+ }
+
+ @Nullable
+ private static Constructor> findSingleConstructor(Constructor>[] constructors) {
+ return (constructors.length == 1 ? constructors[0] : null);
+ }
+
+ @Nullable
+ private static Constructor> findDeclaredConstructor(Class> factoryImplementationClass) {
+ try {
+ return factoryImplementationClass.getDeclaredConstructor();
+ }
+ catch (NoSuchMethodException ex) {
+ return null;
+ }
+ }
+
+ }
+
+ /**
+ * Inner class to avoid a hard dependency on Kotlin at runtime.
+ */
+ private static class KotlinDelegate {
+
+ @Nullable
+ public static Constructor findPrimaryConstructor(Class clazz) {
+ try {
+ KFunction primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz));
+ if (primaryConstructor != null) {
+ Constructor constructor = ReflectJvmMapping.getJavaConstructor(
+ primaryConstructor);
+ Assert.state(constructor != null, () ->
+ "Failed to find Java constructor for Kotlin primary constructor: " + clazz.getName());
+ return constructor;
+ }
+ }
+ catch (UnsupportedOperationException ex) {
+ // ignore
+ }
+ return null;
+ }
+
+ public static T instantiate(Constructor constructor, Object[] args)
+ throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor);
+ if (kotlinConstructor == null) {
+ return constructor.newInstance(args);
+ }
+ makeAccessible(constructor, kotlinConstructor);
+ return instantiate(kotlinConstructor, convertArgs(args, kotlinConstructor.getParameters()));
+ }
+
+ private static void makeAccessible(Constructor constructor,
+ KFunction kotlinConstructor) {
+ if ((!Modifier.isPublic(constructor.getModifiers())
+ || !Modifier.isPublic(constructor.getDeclaringClass().getModifiers()))) {
+ KCallablesJvm.setAccessible(kotlinConstructor, true);
+ }
+ }
+
+ private static Map convertArgs(Object[] args, List parameters) {
+ Map result = CollectionUtils.newHashMap(parameters.size());
+ Assert.isTrue(args.length <= parameters.size(),
+ "Number of provided arguments should be less of equals than number of constructor parameters");
+ for (int i = 0; i < args.length; i++) {
+ if (!parameters.get(i).isOptional() || args[i] != null) {
+ result.put(parameters.get(i), args[i]);
+ }
+ }
+ return result;
+ }
+
+ private static T instantiate(KFunction kotlinConstructor, Map args) {
+ return kotlinConstructor.callBy(args);
+ }
+
+ }
+
+
+ /**
+ * Strategy for resolving constructor arguments based on their type.
+ *
+ * @since 6.0
+ * @see ArgumentResolver#of(Class, Object)
+ * @see ArgumentResolver#ofSupplied(Class, Supplier)
+ * @see ArgumentResolver#from(Function)
+ */
+ @FunctionalInterface
+ public interface ArgumentResolver {
+
+ /**
+ * Resolve the given argument if possible.
+ * @param the argument type
+ * @param type the argument type
+ * @return the resolved argument value or {@code null}
+ */
+ @Nullable
+ T resolve(Class type);
+
+ /**
+ * Create a new composed {@link ArgumentResolver} by combining this resolver
+ * with the given type and value.
+ * @param the argument type
+ * @param type the argument type
+ * @param value the argument value
+ * @return a new composite {@link ArgumentResolver} instance
+ */
+ default ArgumentResolver and(Class type, T value) {
+ return and(ArgumentResolver.of(type, value));
+ }
+
+ /**
+ * Create a new composed {@link ArgumentResolver} by combining this resolver
+ * with the given type and value.
+ * @param the argument type
+ * @param type the argument type
+ * @param valueSupplier the argument value supplier
+ * @return a new composite {@link ArgumentResolver} instance
+ */
+ default ArgumentResolver andSupplied(Class type, Supplier valueSupplier) {
+ return and(ArgumentResolver.ofSupplied(type, valueSupplier));
+ }
+
+ /**
+ * Create a new composed {@link ArgumentResolver} by combining this resolver
+ * with the given resolver.
+ * @param argumentResolver the argument resolver to add
+ * @return a new composite {@link ArgumentResolver} instance
+ */
+ default ArgumentResolver and(ArgumentResolver argumentResolver) {
+ return from(type -> {
+ Object resolved = resolve(type);
+ return (resolved != null) ? resolved : argumentResolver.resolve(type);
+ });
+ }
+
+ /**
+ * Factory method that returns a {@link ArgumentResolver} that always
+ * returns {@code null}.
+ * @return a new {@link ArgumentResolver} instance
+ */
+ static ArgumentResolver none() {
+ return from(type -> null);
+ }
+
+ /**
+ * Factory method that can be used to create a {@link ArgumentResolver}
+ * that resolves only the given type.
+ * @param the argument type
+ * @param type the argument type
+ * @param value the argument value
+ * @return a new {@link ArgumentResolver} instance
+ */
+ static ArgumentResolver of(Class type, T value) {
+ return ofSupplied(type, () -> value);
+ }
+
+ /**
+ * Factory method that can be used to create a {@link ArgumentResolver}
+ * that resolves only the given type.
+ * @param the argument type
+ * @param type the argument type
+ * @param valueSupplier the argument value supplier
+ * @return a new {@link ArgumentResolver} instance
+ */
+ static ArgumentResolver ofSupplied(Class type, Supplier valueSupplier) {
+ return from(candidateType -> (candidateType.equals(type) ? valueSupplier.get() : null));
+ }
+
+ /**
+ * Factory method that creates a new {@link ArgumentResolver} from a
+ * lambda friendly function. The given function is provided with the
+ * argument type and must provide an instance of that type or {@code null}.
+ * @param function the resolver function
+ * @return a new {@link ArgumentResolver} instance backed by the function
+ */
+ static ArgumentResolver from(Function, Object> function) {
+ return new ArgumentResolver() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T resolve(Class type) {
+ return (T) function.apply(type);
+ }
+
+ };
+ }
+
+ }
+
+ /**
+ * Strategy for handling a failure that occurs when instantiating a factory.
+ *
+ * @since 6.0
+ * @see FailureHandler#throwing()
+ * @see FailureHandler#logging(Log)
+ */
+ @FunctionalInterface
+ public interface FailureHandler {
+
+ /**
+ * Handle the {@code failure} that occurred when instantiating the
+ * {@code factoryImplementationName} that was expected to be of the
+ * given {@code factoryType}.
+ * @param factoryType the type of the factory
+ * @param factoryImplementationName the name of the factory implementation
+ * @param failure the failure that occurred
+ * @see #throwing()
+ * @see #logging
+ */
+ void handleFailure(Class> factoryType, String factoryImplementationName, Throwable failure);
+
+ /**
+ * Return a new {@link FailureHandler} that handles
+ * errors by throwing an {@link IllegalArgumentException}.
+ * @return a new {@link FailureHandler} instance
+ */
+ static FailureHandler throwing() {
+ return throwing(IllegalArgumentException::new);
+ }
+
+ /**
+ * Return a new {@link FailureHandler} that handles
+ * errors by throwing an exception.
+ * @param exceptionFactory factory used to create the exception
+ * @return a new {@link FailureHandler} instance
+ */
+ static FailureHandler throwing(BiFunction exceptionFactory) {
+ return handleMessage((message, failure) -> {
+ throw exceptionFactory.apply(message.get(), failure);
+ });
+ }
+
+ /**
+ * Return a new {@link FailureHandler} that handles
+ * errors by logging trace messages.
+ * @param logger the logger used to log message
+ * @return a new {@link FailureHandler} instance
+ */
+ static FailureHandler logging(Log logger) {
+ return handleMessage((message, failure) -> logger.trace(LogMessage.of(message), failure));
+ }
+
+ /**
+ * Return a new {@link FailureHandler} that handles
+ * errors with using a standard formatted message.
+ * @param messageHandler the message handler used to handle the problem
+ * @return a new {@link FailureHandler} instance
+ */
+ static FailureHandler handleMessage(BiConsumer, Throwable> messageHandler) {
+ return (factoryType, factoryImplementationName, failure) -> {
+ Supplier message = () -> "Unable to instantiate factory class [" + factoryImplementationName +
+ "] for factory type [" + factoryType.getName() + "]";
+ messageHandler.accept(message, failure);
+ };
+ }
+
+ }
+
}
diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ConstructorArgsDummyFactory.java b/spring-core/src/test/java/org/springframework/core/io/support/ConstructorArgsDummyFactory.java
new file mode 100644
index 0000000000..42262b8d10
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/core/io/support/ConstructorArgsDummyFactory.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.core.io.support;
+
+/**
+ * Used by {@link SpringFactoriesLoaderTests}.
+ *
+ * @author Andy Wilkinson
+ */
+class ConstructorArgsDummyFactory implements DummyFactory {
+
+ private final String string;
+
+ public ConstructorArgsDummyFactory(String string) {
+ this(string, 0);
+ }
+
+ private ConstructorArgsDummyFactory(String string, int reasonCode) {
+ this.string = string;
+ }
+
+ @Override
+ public String getString() {
+ return this.string;
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/core/io/support/MultipleConstructorArgsDummyFactory.java b/spring-core/src/test/java/org/springframework/core/io/support/MultipleConstructorArgsDummyFactory.java
new file mode 100644
index 0000000000..cf8bc5f61e
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/core/io/support/MultipleConstructorArgsDummyFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.core.io.support;
+
+/**
+ * Used by {@link SpringFactoriesLoaderTests}.
+ *
+ * @author Madhura Bhave
+ */
+class MultipleConstructorArgsDummyFactory implements DummyFactory {
+
+ private final String string;
+
+ private final Integer age;
+
+ MultipleConstructorArgsDummyFactory(String string) {
+ this(string, null);
+ }
+
+ MultipleConstructorArgsDummyFactory(String string, Integer age) {
+ this.string = string;
+ this.age = age;
+ }
+
+
+ @Override
+ public String getString() {
+ return this.string + this.age;
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java
index 913943da5b..c171286faa 100644
--- a/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java
+++ b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,32 @@
package org.springframework.core.io.support;
+import java.io.File;
import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
import java.util.List;
+import org.apache.commons.logging.Log;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
+import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator;
+import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
+import org.springframework.core.log.LogMessage;
+
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
/**
* Tests for {@link SpringFactoriesLoader}.
@@ -32,6 +49,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* @author Arjen Poutsma
* @author Phillip Webb
* @author Sam Brannen
+ * @author Andy Wilkinson
+ * @author Madhura Bhave
*/
class SpringFactoriesLoaderTests {
@@ -43,9 +62,11 @@ class SpringFactoriesLoaderTests {
@AfterAll
static void checkCache() {
- assertThat(SpringFactoriesLoader.cache).hasSize(1);
+ assertThat(SpringFactoriesLoader.cache).hasSize(3);
+ SpringFactoriesLoader.cache.clear();
}
+
@Test
void loadFactoryNames() {
List factoryNames = SpringFactoriesLoader.loadFactoryNames(DummyFactory.class, null);
@@ -82,4 +103,299 @@ class SpringFactoriesLoaderTests {
+ "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]");
}
+ @Test
+ void attemptToLoadFactoryOfIncompatibleTypeWithLoggingFailureHandler() {
+ Log logger = mock(Log.class);
+ FailureHandler failureHandler = FailureHandler.logging(logger);
+ List factories = SpringFactoriesLoader.loadFactories(String.class, null, failureHandler);
+ assertThat(factories).isEmpty();
+ }
+
+ @Test
+ void loadFactoryWithNonDefaultConstructor() {
+ ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
+ List factories = SpringFactoriesLoader.loadFactories(DummyFactory.class,
+ LimitedClassLoader.constructorArgumentFactories, resolver);
+ assertThat(factories).hasSize(3);
+ assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
+ assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
+ assertThat(factories.get(2)).isInstanceOf(ConstructorArgsDummyFactory.class);
+ assertThat(factories).extracting(DummyFactory::getString).containsExactly("Foo", "Bar", "injected");
+ }
+
+ @Test
+ void loadFactoryWithMultipleConstructors() {
+ ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> SpringFactoriesLoader.loadFactories(DummyFactory.class,
+ LimitedClassLoader.multipleArgumentFactories, 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() {
+ Log logger = mock(Log.class);
+ FailureHandler failureHandler = FailureHandler.logging(logger);
+ List factories = SpringFactoriesLoader.loadFactories(
+ DummyFactory.class, LimitedClassLoader.multipleArgumentFactories, failureHandler);
+ assertThat(factories).hasSize(2);
+ assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
+ assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
+ }
+
+
+ @Nested
+ class FailureHandlerTests {
+
+ @Test
+ void throwingReturnsHandlerThatThrowsIllegalArgumentException() {
+ FailureHandler handler = FailureHandler.throwing();
+ RuntimeException cause = new RuntimeException();
+ assertThatIllegalArgumentException().isThrownBy(() -> handler.handleFailure(
+ DummyFactory.class, MyDummyFactory1.class.getName(),
+ cause)).withMessageStartingWith("Unable to instantiate factory class").withCause(cause);
+ }
+
+ @Test
+ void throwingWithFactoryReturnsHandlerThatThrows() {
+ FailureHandler handler = FailureHandler.throwing(IllegalStateException::new);
+ RuntimeException cause = new RuntimeException();
+ assertThatIllegalStateException().isThrownBy(() -> handler.handleFailure(
+ DummyFactory.class, MyDummyFactory1.class.getName(),
+ cause)).withMessageStartingWith("Unable to instantiate factory class").withCause(cause);
+ }
+
+ @Test
+ void loggingReturnsHandlerThatLogs() {
+ Log logger = mock(Log.class);
+ FailureHandler handler = FailureHandler.logging(logger);
+ RuntimeException cause = new RuntimeException();
+ handler.handleFailure(DummyFactory.class, MyDummyFactory1.class.getName(), cause);
+ verify(logger).trace(isA(LogMessage.class), eq(cause));
+ }
+
+ @Test
+ void handleMessageReturnsHandlerThatAcceptsMessage() {
+ List failures = new ArrayList<>();
+ List messages = new ArrayList<>();
+ FailureHandler handler = FailureHandler.handleMessage((message, failure) -> {
+ failures.add(failure);
+ messages.add(message.get());
+ });
+ RuntimeException cause = new RuntimeException();
+ handler.handleFailure(DummyFactory.class, MyDummyFactory1.class.getName(), cause);
+ assertThat(failures).containsExactly(cause);
+ assertThat(messages).hasSize(1);
+ assertThat(messages.get(0)).startsWith("Unable to instantiate factory class");
+ }
+
+ }
+
+
+ @Nested
+ class ArgumentResolverTests {
+
+ @Test
+ void ofValueResolvesValue() {
+ ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test");
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ assertThat(resolver.resolve(String.class)).isNull();
+ assertThat(resolver.resolve(Integer.class)).isNull();
+ }
+
+ @Test
+ void ofValueSupplierResolvesValue() {
+ ArgumentResolver resolver = ArgumentResolver.ofSupplied(CharSequence.class, () -> "test");
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ assertThat(resolver.resolve(String.class)).isNull();
+ assertThat(resolver.resolve(Integer.class)).isNull();
+ }
+
+ @Test
+ void fromAdaptsFunction() {
+ ArgumentResolver resolver = ArgumentResolver.from(
+ type -> CharSequence.class.equals(type) ? "test" : null);
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ assertThat(resolver.resolve(String.class)).isNull();
+ assertThat(resolver.resolve(Integer.class)).isNull();
+ }
+
+ @Test
+ void andValueReturnsComposite() {
+ ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(Integer.class, 123);
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ assertThat(resolver.resolve(String.class)).isNull();
+ assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
+ }
+
+ @Test
+ void andValueWhenSameTypeReturnsCompositeResolvingFirst() {
+ ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(CharSequence.class, "ignore");
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ }
+
+ @Test
+ void andValueSupplierReturnsComposite() {
+ ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").andSupplied(Integer.class, () -> 123);
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ assertThat(resolver.resolve(String.class)).isNull();
+ assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
+ }
+
+ @Test
+ void andValueSupplierWhenSameTypeReturnsCompositeResolvingFirst() {
+ ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").andSupplied(CharSequence.class, () -> "ignore");
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ }
+
+ @Test
+ void andResolverReturnsComposite() {
+ ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(Integer.class, 123);
+ resolver = resolver.and(ArgumentResolver.of(CharSequence.class, "ignore").and(Long.class, 234L));
+ assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
+ assertThat(resolver.resolve(String.class)).isNull();
+ assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
+ assertThat(resolver.resolve(Long.class)).isEqualTo(234L);
+ }
+
+ }
+
+ @Nested
+ class FactoryInstantiatorTests {
+
+ private final ArgumentResolver resolver = ArgumentResolver.of(String.class, "test");
+
+ @Test
+ void defaultConstructorCreatesInstance() throws Exception {
+ Object instance = FactoryInstantiator.forClass(
+ DefaultConstructor.class).instantiate(this.resolver);
+ assertThat(instance).isNotNull();
+ }
+
+ @Test
+ void singleConstructorWithArgumentsCreatesInstance() throws Exception {
+ Object instance = FactoryInstantiator.forClass(
+ SingleConstructor.class).instantiate(this.resolver);
+ assertThat(instance).isNotNull();
+ }
+
+ @Test
+ void multiplePrivateAndSinglePublicConstructorCreatesInstance() throws Exception {
+ Object instance = FactoryInstantiator.forClass(
+ MultiplePrivateAndSinglePublicConstructor.class).instantiate(this.resolver);
+ assertThat(instance).isNotNull();
+ }
+
+ @Test
+ void multiplePackagePrivateAndSinglePublicConstructorCreatesInstance() throws Exception {
+ Object instance = FactoryInstantiator.forClass(
+ MultiplePackagePrivateAndSinglePublicConstructor.class).instantiate(this.resolver);
+ assertThat(instance).isNotNull();
+ }
+
+ @Test
+ void singlePackagePrivateConstructorCreatesInstance() throws Exception {
+ Object instance = FactoryInstantiator.forClass(
+ SinglePackagePrivateConstructor.class).instantiate(this.resolver);
+ assertThat(instance).isNotNull();
+ }
+
+ @Test
+ void singlePrivateConstructorCreatesInstance() throws Exception {
+ Object instance = FactoryInstantiator.forClass(
+ SinglePrivateConstructor.class).instantiate(this.resolver);
+ assertThat(instance).isNotNull();
+ }
+
+ @Test
+ void multiplePackagePrivateConstructorsThrowsException() {
+ assertThatIllegalStateException().isThrownBy(
+ () -> FactoryInstantiator.forClass(MultiplePackagePrivateConstructors.class))
+ .withMessageContaining("has no suitable constructor");
+ }
+
+ static class DefaultConstructor {
+
+ }
+
+ static class SingleConstructor {
+
+ SingleConstructor(String arg) {
+ }
+
+ }
+
+ static class MultiplePrivateAndSinglePublicConstructor {
+
+ public MultiplePrivateAndSinglePublicConstructor(String arg) {
+ this(arg, false);
+ }
+
+ private MultiplePrivateAndSinglePublicConstructor(String arg, boolean extra) {
+ }
+
+ }
+
+ static class MultiplePackagePrivateAndSinglePublicConstructor {
+
+ public MultiplePackagePrivateAndSinglePublicConstructor(String arg) {
+ this(arg, false);
+ }
+
+ MultiplePackagePrivateAndSinglePublicConstructor(String arg, boolean extra) {
+ }
+
+ }
+
+
+ static class SinglePackagePrivateConstructor {
+
+ SinglePackagePrivateConstructor(String arg) {
+ }
+
+ }
+
+ static class SinglePrivateConstructor {
+
+ private SinglePrivateConstructor(String arg) {
+ }
+
+ }
+
+ static class MultiplePackagePrivateConstructors {
+
+ MultiplePackagePrivateConstructors(String arg) {
+ this(arg, false);
+ }
+
+ MultiplePackagePrivateConstructors(String arg, boolean extra) {
+ }
+
+ }
+
+ }
+
+ private static class LimitedClassLoader extends URLClassLoader {
+
+ private static final ClassLoader constructorArgumentFactories = new LimitedClassLoader("constructor-argument-factories");
+
+ private static final ClassLoader multipleArgumentFactories = new LimitedClassLoader("multiple-arguments-factories");
+
+ LimitedClassLoader(String location) {
+ super(new URL[] { toUrl(location) });
+ }
+
+ private static URL toUrl(String location) {
+ try {
+ return new File("src/test/resources/org/springframework/core/io/support/" + location + "/").toURI().toURL();
+ }
+ catch (MalformedURLException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ }
+
}
diff --git a/spring-core/src/test/kotlin/org/springframework/core/io/support/KotlinSpringFactoriesLoaderTests.kt b/spring-core/src/test/kotlin/org/springframework/core/io/support/KotlinSpringFactoriesLoaderTests.kt
new file mode 100644
index 0000000000..64e6f99fb0
--- /dev/null
+++ b/spring-core/src/test/kotlin/org/springframework/core/io/support/KotlinSpringFactoriesLoaderTests.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.core.io.support
+
+import org.assertj.core.api.Assertions.assertThat
+import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator
+import org.junit.jupiter.api.Test
+import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver
+
+/**
+ * Kotlin tests for {@link SpringFactoriesLoader}.
+ *
+ * @author Phillip Webb
+ */
+@Suppress("unused", "UNUSED_PARAMETER", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+class KotlinSpringFactoriesLoaderTests {
+
+ @Test
+ fun `Instantiate immutable data class`() {
+ val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
+ .and(Integer.TYPE, 123)
+ val instantiator = FactoryInstantiator.forClass(Immutable::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isEqualTo(Immutable("test", 123))
+ }
+
+ @Test
+ fun `Instantiate immutable data class with optional parameter and all arguments specified`() {
+ val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
+ val instantiator = FactoryInstantiator.forClass(OneOptionalParameter::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isEqualTo(OneOptionalParameter("test", 12))
+ }
+
+ @Test
+ fun `Instantiate immutable class with optional argument and only mandatory arguments specified`() {
+ val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
+ .and(Integer.TYPE, 345)
+ val instantiator = FactoryInstantiator.forClass(OneOptionalParameter::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isEqualTo(OneOptionalParameter("test", 345))
+ }
+
+ @Test
+ fun `Instantiate immutable class with nullable argument`() {
+ val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
+ val instantiator = FactoryInstantiator.forClass(NullableParameter::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isEqualTo(NullableParameter("test", null))
+ }
+
+ @Test
+ fun `Instantiate class with all optional argument`() {
+ val resolver = ArgumentResolver.none()
+ val instantiator = FactoryInstantiator.forClass(AllOptionalParameters::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isEqualTo(AllOptionalParameters())
+ }
+
+ @Test
+ @Suppress("UsePropertyAccessSyntax")
+ fun `Instantiate class with private constructor`() {
+ val resolver = ArgumentResolver.none()
+ val instantiator = FactoryInstantiator.forClass(PrivateConstructor::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isNotNull()
+ }
+
+ @Test
+ fun `Instantiate class with protected constructor`() {
+ val resolver = ArgumentResolver.none()
+ val instantiator = FactoryInstantiator.forClass(ProtectedConstructor::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isNotNull()
+ }
+
+ @Test
+ fun `Instantiate private class`() {
+ val resolver = ArgumentResolver.none()
+ val instantiator = FactoryInstantiator.forClass(PrivateClass::class.java)
+ val instance = instantiator.instantiate(resolver)
+ assertThat(instance).isNotNull()
+ }
+
+ data class Immutable(val param1: String, val param2: Int)
+
+ data class OneOptionalParameter(val param1: String, val param2: Int = 12)
+
+ data class AllOptionalParameters(var param1: String = "a", var param2: Int = 12)
+
+ data class NullableParameter(val param1: String, val param2: Int?)
+
+ class PrivateConstructor private constructor()
+
+ open class ProtectedConstructor protected constructor()
+
+ private class PrivateClass
+
+}
diff --git a/spring-core/src/test/resources/org/springframework/core/io/support/constructor-argument-factories/META-INF/spring.factories b/spring-core/src/test/resources/org/springframework/core/io/support/constructor-argument-factories/META-INF/spring.factories
new file mode 100644
index 0000000000..2860b476ae
--- /dev/null
+++ b/spring-core/src/test/resources/org/springframework/core/io/support/constructor-argument-factories/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.core.io.support.DummyFactory=\
+org.springframework.core.io.support.ConstructorArgsDummyFactory
diff --git a/spring-core/src/test/resources/org/springframework/core/io/support/multiple-arguments-factories/META-INF/spring.factories b/spring-core/src/test/resources/org/springframework/core/io/support/multiple-arguments-factories/META-INF/spring.factories
new file mode 100644
index 0000000000..a875fee6a6
--- /dev/null
+++ b/spring-core/src/test/resources/org/springframework/core/io/support/multiple-arguments-factories/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.core.io.support.DummyFactory=\
+org.springframework.core.io.support.MultipleConstructorArgsDummyFactory