Merge pull request #28057 from philwebb

* pr/28057:
  Polish contribution
  Provide more control over factory failure handling
  Allow flexible constructor arguments in factory implementations

Closes gh-28057
This commit is contained in:
Stephane Nicoll
2022-03-15 20:48:32 +01:00
7 changed files with 978 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,23 +17,40 @@
package org.springframework.core.io.support;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.full.KClasses;
import kotlin.reflect.jvm.KCallablesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.KotlinDetector;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.UrlResource;
import org.springframework.core.log.LogMessage;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -52,10 +69,24 @@ import org.springframework.util.StringUtils;
*
* where {@code example.MyService} is the name of the interface, and {@code MyServiceImpl1}
* and {@code MyServiceImpl2} are two implementations.
* <p>
* Implementation classes <b>must</b> have a single resolvable constructor that will
* be used to create the instance, either:
* <ul>
* <li>a primary or single constructor</li>
* <li>a single public constructor</li>
* <li>the default constructor</li>
* </ul>
* If the resolvable constructor has arguments, a suitable {@link ArgumentResolver
* ArgumentResolver} should be provided. To customize how instantiation failures
* are handled, consider providing a {@link FailureHandler FailureHandler}.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Sam Brannen
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Phillip Webb
* @since 3.2
*/
public final class SpringFactoriesLoader {
@@ -69,6 +100,8 @@ public final class SpringFactoriesLoader {
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
private static final FailureHandler THROWING_HANDLER = FailureHandler.throwing();
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();
@@ -78,10 +111,12 @@ public final class SpringFactoriesLoader {
/**
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader.
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader and
* a default argument resolver that expects a no-arg constructor.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>If a custom instantiation strategy is required, use {@link #loadFactoryNames}
* to obtain all registered factory names.
* <p>If a custom instantiation strategy is required, use {@code loadFactories}
* with a custom {@link ArgumentResolver ArgumentResolver} and/or
* {@link FailureHandler FailureHandler}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
@@ -89,21 +124,86 @@ public final class SpringFactoriesLoader {
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
* @see #loadFactoryNames
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
return loadFactories(factoryType, classLoader, null, null);
}
/**
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader and
* argument resolver.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @param argumentResolver strategy used to resolve constructor arguments by their type
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
* @since 6.0
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
@Nullable ArgumentResolver argumentResolver) {
return loadFactories(factoryType, classLoader, argumentResolver, null);
}
/**
* 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.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* <p>For any factory implementation class that cannot be loaded or error that occurs while
* instantiating it, the given failure handler is called.
* @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @param failureHandler strategy used to handle factory instantiation failures
* @since 6.0
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
@Nullable FailureHandler failureHandler) {
return loadFactories(factoryType, classLoader, null, failureHandler);
}
/**
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader,
* argument resolver, and custom failure handling provided by the given
* failure handler.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* <p>For any factory implementation class that cannot be loaded or error that occurs while
* instantiating it, the given failure handler is called.
* @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @param argumentResolver strategy used to resolve constructor arguments by their type
* @param failureHandler strategy used to handle factory instantiation failures
* @since 6.0
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
@Nullable ArgumentResolver argumentResolver, @Nullable FailureHandler failureHandler) {
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<String> 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<T> result = new ArrayList<>(factoryImplementationNames.size());
FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_HANDLER;
for (String factoryImplementationName : factoryImplementationNames) {
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
T factory = instantiateFactory(factoryImplementationName, factoryType,
classLoaderToUse, argumentResolver, failureHandlerToUse);
if (factory != null) {
result.add(factory);
}
}
AnnotationAwareOrderComparator.sort(result);
return result;
@@ -123,26 +223,29 @@ public final class SpringFactoriesLoader {
* @see #loadFactories
*/
public static List<String> 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<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
private static Map<String, List<String>> getAllFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = loadAllFactories(classLoader);
cache.put(classLoader, result);
return result;
}
private static Map<String, List<String>> loadAllFactories(ClassLoader classLoader) {
Map<String, List<String>> result;
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
UrlResource resource = new UrlResource(urls.nextElement());
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
@@ -154,33 +257,351 @@ public final class SpringFactoriesLoader {
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct().toList());
cache.put(classLoader, result);
result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
return Collections.unmodifiableMap(result);
}
@SuppressWarnings("unchecked")
private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
private static List<String> toDistinctUnmodifiableList(String factoryType, List<String> implementations) {
return implementations.stream().distinct().toList();
}
@Nullable
private static <T> T instantiateFactory(String factoryImplementationName,
Class<T> factoryType, ClassLoader classLoader, @Nullable ArgumentResolver argumentResolver,
FailureHandler failureHandler) {
try {
Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
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<T> factoryInstantiator = FactoryInstantiator.forClass(factoryImplementationClass);
return factoryInstantiator.instantiate(argumentResolver);
}
catch (Throwable ex) {
throw new IllegalArgumentException(
"Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]",
ex);
failureHandler.handleFailure(factoryType, factoryImplementationName, ex);
return null;
}
}
/**
* Internal instantiator used to create the factory instance.
* @param <T> the instance implementation type
*/
static final class FactoryInstantiator<T> {
private final Constructor<T> constructor;
private FactoryInstantiator(Constructor<T> constructor) {
ReflectionUtils.makeAccessible(constructor);
this.constructor = constructor;
}
T instantiate(@Nullable ArgumentResolver argumentResolver) throws Exception {
Object[] args = resolveArgs(argumentResolver);
if (isKotlinType(this.constructor.getDeclaringClass())) {
return KotlinDelegate.instantiate(this.constructor, args);
}
return this.constructor.newInstance(args);
}
private Object[] resolveArgs(@Nullable ArgumentResolver argumentResolver) {
Class<?>[] types = this.constructor.getParameterTypes();
return (argumentResolver != null ?
Arrays.stream(types).map(argumentResolver::resolve).toArray() :
new Object[types.length]);
}
@SuppressWarnings("unchecked")
static <T> FactoryInstantiator<T> forClass(Class<?> factoryImplementationClass) {
Constructor<?> constructor = findConstructor(factoryImplementationClass);
Assert.state(constructor != null, "Class [" + factoryImplementationClass.getName() + "] has no suitable constructor");
return new FactoryInstantiator<>((Constructor<T>) constructor);
}
@Nullable
private static Constructor<?> findConstructor(Class<?> factoryImplementationClass) {
// Same algorithm as BeanUtils.getResolvableConstructor
Constructor<?> constructor = findPrimaryKotlinConstructor(factoryImplementationClass);
constructor = (constructor != null ? constructor :
findSingleConstructor(factoryImplementationClass.getConstructors()));
constructor = (constructor != null ? constructor :
findSingleConstructor(factoryImplementationClass.getDeclaredConstructors()));
constructor = (constructor != null ? constructor :
findDeclaredConstructor(factoryImplementationClass));
return constructor;
}
@Nullable
private static Constructor<?> findPrimaryKotlinConstructor(Class<?> factoryImplementationClass) {
return (isKotlinType(factoryImplementationClass)
? KotlinDelegate.findPrimaryConstructor(factoryImplementationClass) : null);
}
private static boolean isKotlinType(Class<?> factoryImplementationClass) {
return KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(factoryImplementationClass);
}
@Nullable
private static Constructor<?> findSingleConstructor(Constructor<?>[] constructors) {
return (constructors.length == 1 ? constructors[0] : null);
}
@Nullable
private static Constructor<?> findDeclaredConstructor(Class<?> factoryImplementationClass) {
try {
return factoryImplementationClass.getDeclaredConstructor();
}
catch (NoSuchMethodException ex) {
return null;
}
}
}
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
*/
private static class KotlinDelegate {
@Nullable
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
try {
KFunction<T> primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz));
if (primaryConstructor != null) {
Constructor<T> constructor = ReflectJvmMapping.getJavaConstructor(
primaryConstructor);
Assert.state(constructor != null, () ->
"Failed to find Java constructor for Kotlin primary constructor: " + clazz.getName());
return constructor;
}
}
catch (UnsupportedOperationException ex) {
// ignore
}
return null;
}
public static <T> T instantiate(Constructor<T> constructor, Object[] args)
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
KFunction<T> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor);
if (kotlinConstructor == null) {
return constructor.newInstance(args);
}
makeAccessible(constructor, kotlinConstructor);
return instantiate(kotlinConstructor, convertArgs(args, kotlinConstructor.getParameters()));
}
private static <T> void makeAccessible(Constructor<T> constructor,
KFunction<T> kotlinConstructor) {
if ((!Modifier.isPublic(constructor.getModifiers())
|| !Modifier.isPublic(constructor.getDeclaringClass().getModifiers()))) {
KCallablesJvm.setAccessible(kotlinConstructor, true);
}
}
private static Map<KParameter, Object> convertArgs(Object[] args, List<KParameter> parameters) {
Map<KParameter, Object> 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> T instantiate(KFunction<T> kotlinConstructor, Map<KParameter, Object> 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 <T> the argument type
* @param type the argument type
* @return the resolved argument value or {@code null}
*/
@Nullable
<T> T resolve(Class<T> type);
/**
* Create a new composed {@link ArgumentResolver} by combining this resolver
* with the given type and value.
* @param <T> the argument type
* @param type the argument type
* @param value the argument value
* @return a new composite {@link ArgumentResolver} instance
*/
default <T> ArgumentResolver and(Class<T> 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 <T> the argument type
* @param type the argument type
* @param valueSupplier the argument value supplier
* @return a new composite {@link ArgumentResolver} instance
*/
default <T> ArgumentResolver andSupplied(Class<T> type, Supplier<T> 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 <T> the argument type
* @param type the argument type
* @param value the argument value
* @return a new {@link ArgumentResolver} instance
*/
static <T> ArgumentResolver of(Class<T> type, T value) {
return ofSupplied(type, () -> value);
}
/**
* Factory method that can be used to create a {@link ArgumentResolver}
* that resolves only the given type.
* @param <T> the argument type
* @param type the argument type
* @param valueSupplier the argument value supplier
* @return a new {@link ArgumentResolver} instance
*/
static <T> ArgumentResolver ofSupplied(Class<T> type, Supplier<T> 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<Class<?>, Object> function) {
return new ArgumentResolver() {
@SuppressWarnings("unchecked")
@Override
public <T> T resolve(Class<T> type) {
return (T) function.apply(type);
}
};
}
}
/**
* Strategy for handling a failure that occurs when instantiating a factory.
*
* @since 6.0
* @see FailureHandler#throwing()
* @see FailureHandler#logging(Log)
*/
@FunctionalInterface
public interface FailureHandler {
/**
* Handle the {@code failure} that occurred when instantiating the
* {@code factoryImplementationName} that was expected to be of the
* given {@code factoryType}.
* @param factoryType the type of the factory
* @param factoryImplementationName the name of the factory implementation
* @param failure the failure that occurred
* @see #throwing()
* @see #logging
*/
void handleFailure(Class<?> factoryType, String factoryImplementationName, Throwable failure);
/**
* Return a new {@link FailureHandler} that handles
* errors by throwing an {@link IllegalArgumentException}.
* @return a new {@link FailureHandler} instance
*/
static FailureHandler throwing() {
return throwing(IllegalArgumentException::new);
}
/**
* Return a new {@link FailureHandler} that handles
* errors by throwing an exception.
* @param exceptionFactory factory used to create the exception
* @return a new {@link FailureHandler} instance
*/
static FailureHandler throwing(BiFunction<String, Throwable, ? extends RuntimeException> 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<Supplier<String>, Throwable> messageHandler) {
return (factoryType, factoryImplementationName, failure) -> {
Supplier<String> message = () -> "Unable to instantiate factory class [" + factoryImplementationName +
"] for factory type [" + factoryType.getName() + "]";
messageHandler.accept(message, failure);
};
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,32 @@
package org.springframework.core.io.support;
import java.io.File;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator;
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
import org.springframework.core.log.LogMessage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link SpringFactoriesLoader}.
@@ -32,6 +49,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* @author Arjen Poutsma
* @author Phillip Webb
* @author Sam Brannen
* @author Andy Wilkinson
* @author Madhura Bhave
*/
class SpringFactoriesLoaderTests {
@@ -43,9 +62,11 @@ class SpringFactoriesLoaderTests {
@AfterAll
static void checkCache() {
assertThat(SpringFactoriesLoader.cache).hasSize(1);
assertThat(SpringFactoriesLoader.cache).hasSize(3);
SpringFactoriesLoader.cache.clear();
}
@Test
void loadFactoryNames() {
List<String> factoryNames = SpringFactoriesLoader.loadFactoryNames(DummyFactory.class, null);
@@ -82,4 +103,299 @@ class SpringFactoriesLoaderTests {
+ "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]");
}
@Test
void attemptToLoadFactoryOfIncompatibleTypeWithLoggingFailureHandler() {
Log logger = mock(Log.class);
FailureHandler failureHandler = FailureHandler.logging(logger);
List<String> factories = SpringFactoriesLoader.loadFactories(String.class, null, failureHandler);
assertThat(factories).isEmpty();
}
@Test
void loadFactoryWithNonDefaultConstructor() {
ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(DummyFactory.class,
LimitedClassLoader.constructorArgumentFactories, resolver);
assertThat(factories).hasSize(3);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
assertThat(factories.get(2)).isInstanceOf(ConstructorArgsDummyFactory.class);
assertThat(factories).extracting(DummyFactory::getString).containsExactly("Foo", "Bar", "injected");
}
@Test
void loadFactoryWithMultipleConstructors() {
ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
assertThatIllegalArgumentException()
.isThrownBy(() -> SpringFactoriesLoader.loadFactories(DummyFactory.class,
LimitedClassLoader.multipleArgumentFactories, resolver))
.withMessageContaining("Unable to instantiate factory class "
+ "[org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] for factory type [org.springframework.core.io.support.DummyFactory]")
.havingRootCause().withMessageContaining("Class [org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] has no suitable constructor");
}
@Test
void loadFactoryWithMissingArgumentUsingLoggingFailureHandler() {
Log logger = mock(Log.class);
FailureHandler failureHandler = FailureHandler.logging(logger);
List<DummyFactory> 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<Throwable> failures = new ArrayList<>();
List<String> messages = new ArrayList<>();
FailureHandler handler = FailureHandler.handleMessage((message, failure) -> {
failures.add(failure);
messages.add(message.get());
});
RuntimeException cause = new RuntimeException();
handler.handleFailure(DummyFactory.class, MyDummyFactory1.class.getName(), cause);
assertThat(failures).containsExactly(cause);
assertThat(messages).hasSize(1);
assertThat(messages.get(0)).startsWith("Unable to instantiate factory class");
}
}
@Nested
class ArgumentResolverTests {
@Test
void ofValueResolvesValue() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isNull();
}
@Test
void ofValueSupplierResolvesValue() {
ArgumentResolver resolver = ArgumentResolver.ofSupplied(CharSequence.class, () -> "test");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isNull();
}
@Test
void fromAdaptsFunction() {
ArgumentResolver resolver = ArgumentResolver.from(
type -> CharSequence.class.equals(type) ? "test" : null);
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isNull();
}
@Test
void andValueReturnsComposite() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(Integer.class, 123);
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
}
@Test
void andValueWhenSameTypeReturnsCompositeResolvingFirst() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(CharSequence.class, "ignore");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
}
@Test
void andValueSupplierReturnsComposite() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").andSupplied(Integer.class, () -> 123);
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
}
@Test
void andValueSupplierWhenSameTypeReturnsCompositeResolvingFirst() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").andSupplied(CharSequence.class, () -> "ignore");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
}
@Test
void andResolverReturnsComposite() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(Integer.class, 123);
resolver = resolver.and(ArgumentResolver.of(CharSequence.class, "ignore").and(Long.class, 234L));
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
assertThat(resolver.resolve(Long.class)).isEqualTo(234L);
}
}
@Nested
class FactoryInstantiatorTests {
private final ArgumentResolver resolver = ArgumentResolver.of(String.class, "test");
@Test
void defaultConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
DefaultConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void singleConstructorWithArgumentsCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
SingleConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void multiplePrivateAndSinglePublicConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
MultiplePrivateAndSinglePublicConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void multiplePackagePrivateAndSinglePublicConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
MultiplePackagePrivateAndSinglePublicConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void singlePackagePrivateConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
SinglePackagePrivateConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void singlePrivateConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
SinglePrivateConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void multiplePackagePrivateConstructorsThrowsException() {
assertThatIllegalStateException().isThrownBy(
() -> FactoryInstantiator.forClass(MultiplePackagePrivateConstructors.class))
.withMessageContaining("has no suitable constructor");
}
static class DefaultConstructor {
}
static class SingleConstructor {
SingleConstructor(String arg) {
}
}
static class MultiplePrivateAndSinglePublicConstructor {
public MultiplePrivateAndSinglePublicConstructor(String arg) {
this(arg, false);
}
private MultiplePrivateAndSinglePublicConstructor(String arg, boolean extra) {
}
}
static class MultiplePackagePrivateAndSinglePublicConstructor {
public MultiplePackagePrivateAndSinglePublicConstructor(String arg) {
this(arg, false);
}
MultiplePackagePrivateAndSinglePublicConstructor(String arg, boolean extra) {
}
}
static class SinglePackagePrivateConstructor {
SinglePackagePrivateConstructor(String arg) {
}
}
static class SinglePrivateConstructor {
private SinglePrivateConstructor(String arg) {
}
}
static class MultiplePackagePrivateConstructors {
MultiplePackagePrivateConstructors(String arg) {
this(arg, false);
}
MultiplePackagePrivateConstructors(String arg, boolean extra) {
}
}
}
private static class LimitedClassLoader extends URLClassLoader {
private static final ClassLoader constructorArgumentFactories = new LimitedClassLoader("constructor-argument-factories");
private static final ClassLoader multipleArgumentFactories = new LimitedClassLoader("multiple-arguments-factories");
LimitedClassLoader(String location) {
super(new URL[] { toUrl(location) });
}
private static URL toUrl(String location) {
try {
return new File("src/test/resources/org/springframework/core/io/support/" + location + "/").toURI().toURL();
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.io.support
import org.assertj.core.api.Assertions.assertThat
import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator
import org.junit.jupiter.api.Test
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver
/**
* Kotlin tests for {@link SpringFactoriesLoader}.
*
* @author Phillip Webb
*/
@Suppress("unused", "UNUSED_PARAMETER", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
class KotlinSpringFactoriesLoaderTests {
@Test
fun `Instantiate immutable data class`() {
val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
.and(Integer.TYPE, 123)
val instantiator = FactoryInstantiator.forClass<Immutable>(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>(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>(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>(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>(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>(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>(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>(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
}

View File

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

View File

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