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. + *

+ * Implementation classes must have a single resolvable constructor that will + * be used to create the instance, either: + *

+ * If the resolvable constructor has arguments, a suitable {@link ArgumentResolver + * ArgumentResolver} should be provided. To customize how instantiation failures + * are handled, consider providing a {@link FailureHandler FailureHandler}. * * @author Arjen Poutsma * @author Juergen Hoeller * @author Sam Brannen + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Phillip Webb * @since 3.2 */ public final class SpringFactoriesLoader { @@ -69,6 +100,8 @@ public final class SpringFactoriesLoader { private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); + private static final FailureHandler THROWING_HANDLER = FailureHandler.throwing(); + static final Map>> 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