From 0b716c4f90473c29bef1289db7d8c814f60a0726 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 15 Feb 2022 15:34:38 -0800 Subject: [PATCH] Allow flexible constructor arguments in factory implementations Update `SpringFactoriesLoader` so that factory implementation classes can have a constructor with arguments that are resolved dynamically. Arguments are resolved using a `ArgumentResolver` interface that is passed to the `loadFactories` method. This strategy interface is intentionally simple and only allows resolution based on the argument type. A number of convenience methods are provided to allow resolvers to be built. For example: ArgumentResolver.of(String.class, "tests") .and(Integer.class, 123); Factory implementation classes must have a non-ambiguous constructor in order to be instantiated. The `SpringFactoriesLoader` uses the same algorithm as `BeanUtils.getResolvableConstructor`. See gh-28057 Co-authored-by: Madhura Bhave Co-authored-by: Andy Wilkinson --- .../io/support/SpringFactoriesLoader.java | 351 ++++++++++++++++-- .../support/ConstructorArgsDummyFactory.java | 41 ++ .../MultipleConstructorArgsDummyFactory.java | 45 +++ .../support/SpringFactoriesLoaderTests.java | 243 +++++++++++- .../KotlinSpringFactoriesLoaderTests.kt | 113 ++++++ .../META-INF/spring.factories | 2 + .../META-INF/spring.factories | 2 + 7 files changed, 760 insertions(+), 37 deletions(-) create mode 100644 spring-core/src/test/java/org/springframework/core/io/support/ConstructorArgsDummyFactory.java create mode 100644 spring-core/src/test/java/org/springframework/core/io/support/MultipleConstructorArgsDummyFactory.java create mode 100644 spring-core/src/test/kotlin/org/springframework/core/io/support/KotlinSpringFactoriesLoaderTests.kt create mode 100644 spring-core/src/test/resources/org/springframework/core/io/support/constructor-argument-factories/META-INF/spring.factories create mode 100644 spring-core/src/test/resources/org/springframework/core/io/support/multiple-arguments-factories/META-INF/spring.factories 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..639d00068c 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,38 @@ 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.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 +67,21 @@ 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 use + * to create the instance, either: + *

    + *
  • a primary or single constructor
  • + *
  • a single public constructor
  • + *
  • the default constructor
  • + *
* * @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 { @@ -80,8 +106,6 @@ public final class SpringFactoriesLoader { * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader. *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

If a custom instantiation strategy is required, use {@link #loadFactoryNames} - * to obtain all registered factory names. *

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 +113,38 @@ 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); + } + + /** + * Load and instantiate the factory implementations of the given type from + * {@value #FACTORIES_RESOURCE_LOCATION}, using the given argument resolver and class loader. + *

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) { + 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()); for (String factoryImplementationName : factoryImplementationNames) { - result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse)); + T factory = instantiateFactory(factoryImplementationName, factoryType, argumentResolver, classLoaderToUse); + if (factory != null) { + result.add(factory); + } } AnnotationAwareOrderComparator.sort(result); return result; @@ -123,26 +164,28 @@ 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 +197,271 @@ 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, @Nullable ArgumentResolver argumentResolver, + ClassLoader classLoader) { 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); + throw new IllegalArgumentException("Unable to instantiate factory class [" + factoryImplementationName + + "] for factory type [" + factoryType.getName() + "]", ex); } } + + /** + * 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(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); + } + + 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; + } + + 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); + } + + private static Constructor findSingleConstructor(Constructor[] constructors) { + return (constructors.length == 1) ? constructors[0] : null; + } + + 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) { + } + 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(constructor, 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(Constructor constructor, 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, (Supplier) () -> 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); + } + + }; + } + + } + } 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..86c8256acf 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,24 @@ 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.List; 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 static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link SpringFactoriesLoader}. @@ -32,6 +41,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 +54,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 +95,230 @@ class SpringFactoriesLoaderTests { + "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]"); } + @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"); + } + + + @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() throws Exception { + 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..e5d9c88197 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/io/support/KotlinSpringFactoriesLoaderTests.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2019 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..671d136dee --- /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 \ No newline at end of file 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..5eff214a4c --- /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 \ No newline at end of file