From 0b716c4f90473c29bef1289db7d8c814f60a0726 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 15 Feb 2022 15:34:38 -0800 Subject: [PATCH 1/3] 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 From 12244b2e51bfde11209211c3ee56eef905cd96a7 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 15 Feb 2022 15:38:55 -0800 Subject: [PATCH 2/3] Provide more control over factory failure handling Add an additional `FactoryInstantiationFailureHandler` strategy interface to `SpringFactoriesLoader` to allows instantiation failures to be handled on a per-factory bases. For example, to log trace messages for only factories that can't be created the following can be used: FactoryInstantiationFailureHandler.logging(logger); If no `FactoryInstantiationFailureHandler` instance is supplied then `FactoryInstantiationFailureHandler.throwing()` is used which provides back-compatible behavior by throwing an `IllegalArgumentException`. See gh-28057 Co-authored-by: Madhura Bhave Co-authored-by: Andy Wilkinson --- .../io/support/SpringFactoriesLoader.java | 129 +++++++++++++++++- .../support/SpringFactoriesLoaderTests.java | 74 ++++++++++ 2 files changed, 198 insertions(+), 5 deletions(-) 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 639d00068c..0b97b10ccc 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 @@ -29,6 +29,8 @@ 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; @@ -95,6 +97,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<>(); @@ -115,7 +119,7 @@ public final class SpringFactoriesLoader { * be loaded or if an error occurs while instantiating any factory */ public static List loadFactories(Class factoryType, @Nullable ClassLoader classLoader) { - return loadFactories(factoryType, classLoader, null); + return loadFactories(factoryType, classLoader, null, null); } /** @@ -135,13 +139,59 @@ public final class SpringFactoriesLoader { 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 the FactoryInstantiationFailureHandler to use for handling of 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 arguments and 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 argumentResolver strategy used to resolve constructor arguments by their type + * @param failureHandler the FactoryInstantiationFailureHandler to use for handling of 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 != null) ? classLoader : SpringFactoriesLoader.class.getClassLoader(); List factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse); 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) { - T factory = instantiateFactory(factoryImplementationName, factoryType, argumentResolver, classLoaderToUse); + T factory = instantiateFactory(factoryImplementationName, factoryType, + argumentResolver, classLoaderToUse, failureHandlerToUse); if (factory != null) { result.add(factory); } @@ -213,7 +263,7 @@ public final class SpringFactoriesLoader { @Nullable private static T instantiateFactory(String factoryImplementationName, Class factoryType, @Nullable ArgumentResolver argumentResolver, - ClassLoader classLoader) { + ClassLoader classLoader, FailureHandler failureHandler) { try { Class factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader); Assert.isTrue(factoryType.isAssignableFrom(factoryImplementationClass), @@ -222,8 +272,8 @@ public final class SpringFactoriesLoader { 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; } } @@ -353,6 +403,75 @@ public final class SpringFactoriesLoader { } + /** + * 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); + }; + } + + } + + /** * Strategy for resolving constructor arguments based on their type. * 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 86c8256acf..80d931a43b 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 @@ -21,8 +21,10 @@ 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; @@ -30,10 +32,16 @@ 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}. @@ -95,6 +103,14 @@ 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"); @@ -116,6 +132,64 @@ class SpringFactoriesLoaderTests { .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 { From da45bd2dfdb9b98b41520b8049e941fe1b272171 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 15 Mar 2022 20:10:24 +0100 Subject: [PATCH 3/3] Polish contribution See gh-28057 --- .../io/support/SpringFactoriesLoader.java | 225 ++++++++++-------- .../support/SpringFactoriesLoaderTests.java | 13 +- .../KotlinSpringFactoriesLoaderTests.kt | 2 +- .../META-INF/spring.factories | 2 +- .../META-INF/spring.factories | 2 +- 5 files changed, 134 insertions(+), 110 deletions(-) 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 0b97b10ccc..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 @@ -70,13 +70,16 @@ 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: + * Implementation classes must have a single resolvable constructor that will + * be used to create the instance, either: *

    *
  • a primary or single constructor
  • *
  • a single public constructor
  • *
  • the default constructor
  • *
+ * 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 @@ -108,8 +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 {@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. @@ -124,7 +131,8 @@ public final class SpringFactoriesLoader { /** * Load and instantiate the factory implementations of the given type from - * {@value #FACTORIES_RESOURCE_LOCATION}, using the given argument resolver and class loader. + * {@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 @@ -144,8 +152,8 @@ public final class SpringFactoriesLoader { /** * 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. + * {@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 @@ -154,7 +162,7 @@ public final class SpringFactoriesLoader { * 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 the FactoryInstantiationFailureHandler to use for handling of factory instantiation failures + * @param failureHandler strategy used to handle factory instantiation failures * @since 6.0 */ public static List loadFactories(Class factoryType, @Nullable ClassLoader classLoader, @@ -165,8 +173,9 @@ public final class SpringFactoriesLoader { /** * Load and instantiate the factory implementations of the given type from - * {@value #FACTORIES_RESOURCE_LOCATION}, using the given arguments and class loader with custom - * failure handling provided by the given failure handler. + * {@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 @@ -176,22 +185,22 @@ public final class SpringFactoriesLoader { * @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 the FactoryInstantiationFailureHandler to use for handling of factory - * instantiation failures + * @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 != null) ? classLoader : SpringFactoriesLoader.class.getClassLoader(); + ClassLoader classLoaderToUse = (classLoader != null ? classLoader + : SpringFactoriesLoader.class.getClassLoader()); List factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse); 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) { T factory = instantiateFactory(factoryImplementationName, factoryType, - argumentResolver, classLoaderToUse, failureHandlerToUse); + classLoaderToUse, argumentResolver, failureHandlerToUse); if (factory != null) { result.add(factory); } @@ -214,7 +223,8 @@ public final class SpringFactoriesLoader { * @see #loadFactories */ public static List loadFactoryNames(Class factoryType, @Nullable ClassLoader classLoader) { - ClassLoader classLoaderToUse = (classLoader != null) ? classLoader : SpringFactoriesLoader.class.getClassLoader(); + ClassLoader classLoaderToUse = (classLoader != null ? classLoader + : SpringFactoriesLoader.class.getClassLoader()); String factoryTypeName = factoryType.getName(); return getAllFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList()); } @@ -262,8 +272,8 @@ public final class SpringFactoriesLoader { @Nullable private static T instantiateFactory(String factoryImplementationName, - Class factoryType, @Nullable ArgumentResolver argumentResolver, - ClassLoader classLoader, FailureHandler failureHandler) { + Class factoryType, ClassLoader classLoader, @Nullable ArgumentResolver argumentResolver, + FailureHandler failureHandler) { try { Class factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader); Assert.isTrue(factoryType.isAssignableFrom(factoryImplementationClass), @@ -292,7 +302,7 @@ public final class SpringFactoriesLoader { this.constructor = constructor; } - T instantiate(ArgumentResolver argumentResolver) throws Exception { + T instantiate(@Nullable ArgumentResolver argumentResolver) throws Exception { Object[] args = resolveArgs(argumentResolver); if (isKotlinType(this.constructor.getDeclaringClass())) { return KotlinDelegate.instantiate(this.constructor, args); @@ -302,39 +312,47 @@ public final class SpringFactoriesLoader { private Object[] resolveArgs(@Nullable ArgumentResolver argumentResolver) { Class[] types = this.constructor.getParameterTypes(); - return (argumentResolver != null) ? + return (argumentResolver != null ? Arrays.stream(types).map(argumentResolver::resolve).toArray() : - new Object[types.length]; + 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"); + 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); + 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; + 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; + return (constructors.length == 1 ? constructors[0] : null); } + @Nullable private static Constructor findDeclaredConstructor(Class factoryImplementationClass) { try { return factoryImplementationClass.getDeclaredConstructor(); @@ -359,27 +377,30 @@ public final class SpringFactoriesLoader { Constructor constructor = ReflectJvmMapping.getJavaConstructor( primaryConstructor); Assert.state(constructor != null, () -> - "Failed to find Java constructor for Kotlin primary constructor: " + clazz.getName()); + "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 { + 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())); + 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()))) { + if ((!Modifier.isPublic(constructor.getModifiers()) + || !Modifier.isPublic(constructor.getDeclaringClass().getModifiers()))) { KCallablesJvm.setAccessible(kotlinConstructor, true); } } @@ -388,7 +409,7 @@ public final class SpringFactoriesLoader { 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++) { + for (int i = 0; i < args.length; i++) { if (!parameters.get(i).isOptional() || args[i] != null) { result.put(parameters.get(i), args[i]); } @@ -396,82 +417,13 @@ public final class SpringFactoriesLoader { return result; } - private static T instantiate(Constructor constructor, KFunction kotlinConstructor, Map args) { + private static T instantiate(KFunction kotlinConstructor, Map args) { return kotlinConstructor.callBy(args); } } - /** - * 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); - }; - } - - } - - /** * Strategy for resolving constructor arguments based on their type. * @@ -547,7 +499,7 @@ public final class SpringFactoriesLoader { * @return a new {@link ArgumentResolver} instance */ static ArgumentResolver of(Class type, T value) { - return ofSupplied(type, (Supplier) () -> value); + return ofSupplied(type, () -> value); } /** @@ -559,7 +511,7 @@ public final class SpringFactoriesLoader { * @return a new {@link ArgumentResolver} instance */ static ArgumentResolver ofSupplied(Class type, Supplier valueSupplier) { - return from(candidateType -> candidateType.equals(type) ? valueSupplier.get() : null); + return from(candidateType -> (candidateType.equals(type) ? valueSupplier.get() : null)); } /** @@ -583,4 +535,73 @@ public final class SpringFactoriesLoader { } + /** + * 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/SpringFactoriesLoaderTests.java b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java index 80d931a43b..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 @@ -108,13 +108,14 @@ class SpringFactoriesLoaderTests { Log logger = mock(Log.class); FailureHandler failureHandler = FailureHandler.logging(logger); List factories = SpringFactoriesLoader.loadFactories(String.class, null, failureHandler); - assertThat(factories.isEmpty()); + assertThat(factories).isEmpty(); } @Test void loadFactoryWithNonDefaultConstructor() { ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected"); - List factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, LimitedClassLoader.constructorArgumentFactories, resolver); + 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); @@ -126,7 +127,8 @@ class SpringFactoriesLoaderTests { void loadFactoryWithMultipleConstructors() { ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected"); assertThatIllegalArgumentException() - .isThrownBy(() -> SpringFactoriesLoader.loadFactories(DummyFactory.class, LimitedClassLoader.multipleArgumentFactories, resolver)) + .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"); @@ -136,7 +138,8 @@ class SpringFactoriesLoaderTests { void loadFactoryWithMissingArgumentUsingLoggingFailureHandler() { Log logger = mock(Log.class); FailureHandler failureHandler = FailureHandler.logging(logger); - List factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, LimitedClassLoader.multipleArgumentFactories, failureHandler); + 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); @@ -307,7 +310,7 @@ class SpringFactoriesLoaderTests { } @Test - void multiplePackagePrivateConstructorsThrowsException() throws Exception { + void multiplePackagePrivateConstructorsThrowsException() { assertThatIllegalStateException().isThrownBy( () -> FactoryInstantiator.forClass(MultiplePackagePrivateConstructors.class)) .withMessageContaining("has no suitable constructor"); 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 index e5d9c88197..64e6f99fb0 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. 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 index 671d136dee..2860b476ae 100644 --- 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 @@ -1,2 +1,2 @@ org.springframework.core.io.support.DummyFactory=\ -org.springframework.core.io.support.ConstructorArgsDummyFactory \ No newline at end of file +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 index 5eff214a4c..a875fee6a6 100644 --- 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 @@ -1,2 +1,2 @@ org.springframework.core.io.support.DummyFactory=\ -org.springframework.core.io.support.MultipleConstructorArgsDummyFactory \ No newline at end of file +org.springframework.core.io.support.MultipleConstructorArgsDummyFactory