From 92b582701edc9a14a59064042d508373c36355a7 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 28 Aug 2022 17:40:29 +0200 Subject: [PATCH] Skip ContextCustomizerFactory that cannot be loaded With this commit, if a ContextCustomizerFactory cannot be loaded due to a ClassNotFoundException or NoClassDefFoundError, we skip it and log a DEBUG message. This aligns with the "skipping" logic already in place for TestExecutionListeners. Closes gh-29034 --- .../AbstractTestContextBootstrapper.java | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index d127e46e03..8a1ddc5a27 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -200,8 +200,9 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot * @see SpringFactoriesLoader#load(Class, FailureHandler) */ protected List getDefaultTestExecutionListeners() { - List listeners = SpringFactoriesLoader.forDefaultResourceLocation() - .load(TestExecutionListener.class, this::handleListenerInstantiationFailure); + SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader()); + List listeners = + loader.load(TestExecutionListener.class, this::handleInstantiationFailure); if (logger.isDebugEnabled()) { logger.debug("Loaded default TestExecutionListener implementations from location [%s]: %s" .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(listeners))); @@ -219,7 +220,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot catch (BeanInstantiationException ex) { Throwable cause = ex.getCause(); if (cause instanceof ClassNotFoundException || cause instanceof NoClassDefFoundError) { - logSkippedListener(listenerClass.getName(), cause); + logSkippedComponent(TestExecutionListener.class, listenerClass.getName(), cause); } else { throw ex; @@ -229,46 +230,6 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot return listeners; } - private void handleListenerInstantiationFailure( - Class factoryType, String listenerClassName, Throwable failure) { - - Throwable ex = (failure instanceof InvocationTargetException ite ? - ite.getTargetException() : failure); - - if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) { - logSkippedListener(listenerClassName, ex); - } - else if (ex instanceof LinkageError) { - if (logger.isDebugEnabled()) { - logger.debug(""" - Could not load default TestExecutionListener [%s]. Specify custom \ - listener classes or make the default listener classes available.""" - .formatted(listenerClassName), ex); - } - } - else { - if (ex instanceof RuntimeException runtimeException) { - throw runtimeException; - } - if (ex instanceof Error error) { - throw error; - } - throw new IllegalStateException( - "Failed to load default TestExecutionListener [%s].".formatted(listenerClassName), ex); - } - } - - private void logSkippedListener(String listenerClassName, Throwable ex) { - // TestExecutionListener not applicable due to a missing dependency - if (logger.isDebugEnabled()) { - logger.debug(""" - Skipping candidate TestExecutionListener [%s] due to a missing dependency. \ - Specify custom listener classes or make the default listener classes \ - and their required dependencies available. Offending class: [%s]""" - .formatted(listenerClassName, ex.getMessage())); - } - } - /** * {@inheritDoc} */ @@ -440,8 +401,9 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot * @see SpringFactoriesLoader#loadFactories */ protected List getContextCustomizerFactories() { + SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader()); List factories = - SpringFactoriesLoader.loadFactories(ContextCustomizerFactory.class, getClass().getClassLoader()); + loader.load(ContextCustomizerFactory.class, this::handleInstantiationFailure); if (logger.isDebugEnabled()) { logger.debug("Loaded ContextCustomizerFactory implementations from location [%s]: %s" .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(factories))); @@ -570,6 +532,45 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot } + private void handleInstantiationFailure( + Class factoryType, String factoryImplementationName, Throwable failure) { + + Throwable ex = (failure instanceof InvocationTargetException ite ? + ite.getTargetException() : failure); + if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) { + logSkippedComponent(factoryType, factoryImplementationName, ex); + } + else if (ex instanceof LinkageError) { + if (logger.isDebugEnabled()) { + logger.debug(""" + Could not load %1$s [%2$s]. Specify custom %1$s classes or make the default %1$s classes \ + available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex); + } + } + else { + if (ex instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (ex instanceof Error error) { + throw error; + } + throw new IllegalStateException( + "Failed to load %s [%s].".formatted(factoryType.getSimpleName(), factoryImplementationName), ex); + } + } + + private void logSkippedComponent(Class factoryType, String factoryImplementationName, Throwable ex) { + // TestExecutionListener/ContextCustomizerFactory not applicable due to a missing dependency + if (logger.isDebugEnabled()) { + logger.debug(""" + Skipping candidate %1$s [%2$s] due to a missing dependency. \ + Specify custom %1$s classes or make the default %1$s classes \ + and their required dependencies available. Offending class: [%3$s]""" + .formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage())); + } + } + + private static List classNames(List components) { return components.stream().map(Object::getClass).map(Class::getName).toList(); }