Skip to content

Commit 12244b2

Browse files
philwebbmbhavewilkinsona
authored andcommitted
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 <[email protected]> Co-authored-by: Andy Wilkinson <[email protected]>
1 parent 0b716c4 commit 12244b2

File tree

2 files changed

+198
-5
lines changed

2 files changed

+198
-5
lines changed

spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import java.util.List;
3030
import java.util.Map;
3131
import java.util.Properties;
32+
import java.util.function.BiConsumer;
33+
import java.util.function.BiFunction;
3234
import java.util.function.Function;
3335
import java.util.function.Supplier;
3436

@@ -95,6 +97,8 @@ public final class SpringFactoriesLoader {
9597

9698
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
9799

100+
private static final FailureHandler THROWING_HANDLER = FailureHandler.throwing();
101+
98102
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();
99103

100104

@@ -115,7 +119,7 @@ private SpringFactoriesLoader() {
115119
* be loaded or if an error occurs while instantiating any factory
116120
*/
117121
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
118-
return loadFactories(factoryType, classLoader, null);
122+
return loadFactories(factoryType, classLoader, null, null);
119123
}
120124

121125
/**
@@ -135,13 +139,59 @@ public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoa
135139
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
136140
@Nullable ArgumentResolver argumentResolver) {
137141

142+
return loadFactories(factoryType, classLoader, argumentResolver, null);
143+
}
144+
145+
/**
146+
* Load and instantiate the factory implementations of the given type from
147+
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader with custom failure
148+
* handling provided by the given failure handler.
149+
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
150+
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
151+
* discovered for a given factory type, only one instance of the duplicated
152+
* implementation type will be instantiated.
153+
* <p>For any factory implementation class that cannot be loaded or error that occurs while
154+
* instantiating it, the given failure handler is called.
155+
* @param factoryType the interface or abstract class representing the factory
156+
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
157+
* @param failureHandler the FactoryInstantiationFailureHandler to use for handling of factory instantiation failures
158+
* @since 6.0
159+
*/
160+
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
161+
@Nullable FailureHandler failureHandler) {
162+
163+
return loadFactories(factoryType, classLoader, null, failureHandler);
164+
}
165+
166+
/**
167+
* Load and instantiate the factory implementations of the given type from
168+
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given arguments and class loader with custom
169+
* failure handling provided by the given failure handler.
170+
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
171+
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
172+
* discovered for a given factory type, only one instance of the duplicated
173+
* implementation type will be instantiated.
174+
* <p>For any factory implementation class that cannot be loaded or error that occurs while
175+
* instantiating it, the given failure handler is called.
176+
* @param factoryType the interface or abstract class representing the factory
177+
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
178+
* @param argumentResolver strategy used to resolve constructor arguments by their type
179+
* @param failureHandler the FactoryInstantiationFailureHandler to use for handling of factory
180+
* instantiation failures
181+
* @since 6.0
182+
*/
183+
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader,
184+
@Nullable ArgumentResolver argumentResolver, @Nullable FailureHandler failureHandler) {
185+
138186
Assert.notNull(factoryType, "'factoryType' must not be null");
139187
ClassLoader classLoaderToUse = (classLoader != null) ? classLoader : SpringFactoriesLoader.class.getClassLoader();
140188
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
141189
logger.trace(LogMessage.format("Loaded [%s] names: %s", factoryType.getName(), factoryImplementationNames));
142190
List<T> result = new ArrayList<>(factoryImplementationNames.size());
191+
FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_HANDLER;
143192
for (String factoryImplementationName : factoryImplementationNames) {
144-
T factory = instantiateFactory(factoryImplementationName, factoryType, argumentResolver, classLoaderToUse);
193+
T factory = instantiateFactory(factoryImplementationName, factoryType,
194+
argumentResolver, classLoaderToUse, failureHandlerToUse);
145195
if (factory != null) {
146196
result.add(factory);
147197
}
@@ -213,7 +263,7 @@ private static List<String> toDistinctUnmodifiableList(String factoryType, List<
213263
@Nullable
214264
private static <T> T instantiateFactory(String factoryImplementationName,
215265
Class<T> factoryType, @Nullable ArgumentResolver argumentResolver,
216-
ClassLoader classLoader) {
266+
ClassLoader classLoader, FailureHandler failureHandler) {
217267
try {
218268
Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
219269
Assert.isTrue(factoryType.isAssignableFrom(factoryImplementationClass),
@@ -222,8 +272,8 @@ private static <T> T instantiateFactory(String factoryImplementationName,
222272
return factoryInstantiator.instantiate(argumentResolver);
223273
}
224274
catch (Throwable ex) {
225-
throw new IllegalArgumentException("Unable to instantiate factory class [" + factoryImplementationName +
226-
"] for factory type [" + factoryType.getName() + "]", ex);
275+
failureHandler.handleFailure(factoryType, factoryImplementationName, ex);
276+
return null;
227277
}
228278
}
229279

@@ -353,6 +403,75 @@ private static <T> T instantiate(Constructor<T> constructor, KFunction<T> kotlin
353403
}
354404

355405

406+
/**
407+
* Strategy for handling a failure that occurs when instantiating a factory.
408+
*
409+
* @since 6.0
410+
* @see FailureHandler#throwing()
411+
* @see FailureHandler#logging(Log)
412+
*/
413+
@FunctionalInterface
414+
public interface FailureHandler {
415+
416+
/**
417+
* Handle the {@code failure} that occurred when instantiating the {@code factoryImplementationName}
418+
* that was expected to be of the given {@code factoryType}.
419+
* @param factoryType the type of the factory
420+
* @param factoryImplementationName the name of the factory implementation
421+
* @param failure the failure that occurred
422+
* @see #throwing()
423+
* @see #logging
424+
*/
425+
void handleFailure(Class<?> factoryType, String factoryImplementationName, Throwable failure);
426+
427+
/**
428+
* Return a new {@link FailureHandler} that handles
429+
* errors by throwing an {@link IllegalArgumentException}.
430+
* @return a new {@link FailureHandler} instance
431+
*/
432+
static FailureHandler throwing() {
433+
return throwing(IllegalArgumentException::new);
434+
}
435+
436+
/**
437+
* Return a new {@link FailureHandler} that handles
438+
* errors by throwing an exception.
439+
* @param exceptionFactory factory used to create the exception
440+
* @return a new {@link FailureHandler} instance
441+
*/
442+
static FailureHandler throwing(BiFunction<String, Throwable, ? extends RuntimeException> exceptionFactory) {
443+
return handleMessage((message, failure) -> {
444+
throw exceptionFactory.apply(message.get(), failure);
445+
});
446+
}
447+
448+
/**
449+
* Return a new {@link FailureHandler} that handles
450+
* errors by logging trace messages.
451+
* @param logger the logger used to log message
452+
* @return a new {@link FailureHandler} instance
453+
*/
454+
static FailureHandler logging(Log logger) {
455+
return handleMessage((message, failure) -> logger.trace(LogMessage.of(message), failure));
456+
}
457+
458+
/**
459+
* Return a new {@link FailureHandler} that handles
460+
* errors with using a standard formatted message.
461+
* @param messageHandler the message handler used to handle the problem
462+
* @return a new {@link FailureHandler} instance
463+
*/
464+
static FailureHandler handleMessage(BiConsumer<Supplier<String>, Throwable> messageHandler) {
465+
return (factoryType, factoryImplementationName, failure) -> {
466+
Supplier<String> message = () -> "Unable to instantiate factory class [" + factoryImplementationName +
467+
"] for factory type [" + factoryType.getName() + "]";
468+
messageHandler.accept(message, failure);
469+
};
470+
}
471+
472+
}
473+
474+
356475
/**
357476
* Strategy for resolving constructor arguments based on their type.
358477
*

spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,27 @@
2121
import java.net.MalformedURLException;
2222
import java.net.URL;
2323
import java.net.URLClassLoader;
24+
import java.util.ArrayList;
2425
import java.util.List;
2526

27+
import org.apache.commons.logging.Log;
2628
import org.junit.jupiter.api.AfterAll;
2729
import org.junit.jupiter.api.BeforeAll;
2830
import org.junit.jupiter.api.Nested;
2931
import org.junit.jupiter.api.Test;
3032

3133
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
3234
import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator;
35+
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
36+
import org.springframework.core.log.LogMessage;
3337

3438
import static org.assertj.core.api.Assertions.assertThat;
3539
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3640
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
41+
import static org.mockito.ArgumentMatchers.eq;
42+
import static org.mockito.ArgumentMatchers.isA;
43+
import static org.mockito.Mockito.mock;
44+
import static org.mockito.Mockito.verify;
3745

3846
/**
3947
* Tests for {@link SpringFactoriesLoader}.
@@ -95,6 +103,14 @@ void attemptToLoadFactoryOfIncompatibleType() {
95103
+ "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]");
96104
}
97105

106+
@Test
107+
void attemptToLoadFactoryOfIncompatibleTypeWithLoggingFailureHandler() {
108+
Log logger = mock(Log.class);
109+
FailureHandler failureHandler = FailureHandler.logging(logger);
110+
List<String> factories = SpringFactoriesLoader.loadFactories(String.class, null, failureHandler);
111+
assertThat(factories.isEmpty());
112+
}
113+
98114
@Test
99115
void loadFactoryWithNonDefaultConstructor() {
100116
ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
@@ -116,6 +132,64 @@ void loadFactoryWithMultipleConstructors() {
116132
.havingRootCause().withMessageContaining("Class [org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] has no suitable constructor");
117133
}
118134

135+
@Test
136+
void loadFactoryWithMissingArgumentUsingLoggingFailureHandler() {
137+
Log logger = mock(Log.class);
138+
FailureHandler failureHandler = FailureHandler.logging(logger);
139+
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, LimitedClassLoader.multipleArgumentFactories, failureHandler);
140+
assertThat(factories).hasSize(2);
141+
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
142+
assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
143+
}
144+
145+
146+
@Nested
147+
class FailureHandlerTests {
148+
149+
@Test
150+
void throwingReturnsHandlerThatThrowsIllegalArgumentException() {
151+
FailureHandler handler = FailureHandler.throwing();
152+
RuntimeException cause = new RuntimeException();
153+
assertThatIllegalArgumentException().isThrownBy(() -> handler.handleFailure(
154+
DummyFactory.class, MyDummyFactory1.class.getName(),
155+
cause)).withMessageStartingWith("Unable to instantiate factory class").withCause(cause);
156+
}
157+
158+
@Test
159+
void throwingWithFactoryReturnsHandlerThatThrows() {
160+
FailureHandler handler = FailureHandler.throwing(IllegalStateException::new);
161+
RuntimeException cause = new RuntimeException();
162+
assertThatIllegalStateException().isThrownBy(() -> handler.handleFailure(
163+
DummyFactory.class, MyDummyFactory1.class.getName(),
164+
cause)).withMessageStartingWith("Unable to instantiate factory class").withCause(cause);
165+
}
166+
167+
@Test
168+
void loggingReturnsHandlerThatLogs() {
169+
Log logger = mock(Log.class);
170+
FailureHandler handler = FailureHandler.logging(logger);
171+
RuntimeException cause = new RuntimeException();
172+
handler.handleFailure(DummyFactory.class, MyDummyFactory1.class.getName(), cause);
173+
verify(logger).trace(isA(LogMessage.class), eq(cause));
174+
}
175+
176+
@Test
177+
void handleMessageReturnsHandlerThatAcceptsMessage() {
178+
List<Throwable> failures = new ArrayList<>();
179+
List<String> messages = new ArrayList<>();
180+
FailureHandler handler = FailureHandler.handleMessage((message, failure) -> {
181+
failures.add(failure);
182+
messages.add(message.get());
183+
});
184+
RuntimeException cause = new RuntimeException();
185+
handler.handleFailure(DummyFactory.class, MyDummyFactory1.class.getName(), cause);
186+
assertThat(failures).containsExactly(cause);
187+
assertThat(messages).hasSize(1);
188+
assertThat(messages.get(0)).startsWith("Unable to instantiate factory class");
189+
}
190+
191+
}
192+
119193

120194
@Nested
121195
class ArgumentResolverTests {

0 commit comments

Comments
 (0)