diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java index 580b8fa2ff2..0a5827713ae 100644 --- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -71,7 +71,7 @@ public interface CacheAwareContextLoaderDelegate { * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} * mechanism, catch any exception thrown by the {@link ContextLoader}, and * delegate to each of the configured failure processors to process the context - * load failure if the thrown exception is an instance of {@link ContextLoadException}. + * load failure if the exception is an instance of {@link ContextLoadException}. *

The cache statistics should be logged by invoking * {@link org.springframework.test.context.cache.ContextCache#logStatistics()}. * @param mergedContextConfiguration the merged context configuration to use diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java index 0e1d421381b..c41719720f9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java @@ -16,8 +16,6 @@ package org.springframework.test.context.cache; -import java.lang.reflect.InvocationTargetException; -import java.util.Collection; import java.util.List; import org.apache.commons.logging.Log; @@ -27,7 +25,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.ApplicationContextFailureProcessor; @@ -39,6 +36,7 @@ import org.springframework.test.context.SmartContextLoader; import org.springframework.test.context.aot.AotContextLoader; import org.springframework.test.context.aot.AotTestContextInitializers; import org.springframework.test.context.aot.TestContextAotException; +import org.springframework.test.context.util.TestContextSpringFactoriesUtils; import org.springframework.util.Assert; /** @@ -50,9 +48,9 @@ import org.springframework.util.Assert; * and provide a custom {@link ContextCache} implementation. * *

As of Spring Framework 6.0, this class loads {@link ApplicationContextFailureProcessor} - * implementations via the {@link SpringFactoriesLoader} mechanism and delegates to - * them in {@link #loadContext(MergedContextConfiguration)} to process context - * load failures. + * implementations via the {@link org.springframework.core.io.support.SpringFactoriesLoader + * SpringFactoriesLoader} mechanism and delegates to them in + * {@link #loadContext(MergedContextConfiguration)} to process context load failures. * * @author Sam Brannen * @since 4.1 @@ -67,8 +65,8 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext */ static final ContextCache defaultContextCache = new DefaultContextCache(); - private List contextFailureProcessors = - loadApplicationContextFailureProcessors(); + private List contextFailureProcessors = TestContextSpringFactoriesUtils + .loadFactoryImplementations(ApplicationContextFailureProcessor.class); private final AotTestContextInitializers aotTestContextInitializers = new AotTestContextInitializers(); @@ -254,71 +252,4 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext return mergedConfig; } - /** - * Get the {@link ApplicationContextFailureProcessor} implementations to use, - * loaded via the {@link SpringFactoriesLoader} mechanism. - * @return the context failure processors to use - * @since 6.0 - */ - private static List loadApplicationContextFailureProcessors() { - SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation( - DefaultCacheAwareContextLoaderDelegate.class.getClassLoader()); - List processors = loader.load(ApplicationContextFailureProcessor.class, - DefaultCacheAwareContextLoaderDelegate::handleInstantiationFailure); - if (logger.isTraceEnabled()) { - logger.trace("Loaded default ApplicationContextFailureProcessor implementations from location [%s]: %s" - .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(processors))); - } - else if (logger.isDebugEnabled()) { - logger.debug("Loaded default ApplicationContextFailureProcessor implementations from location [%s]: %s" - .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classSimpleNames(processors))); - } - return processors; - } - - private static 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 static void logSkippedComponent(Class factoryType, String factoryImplementationName, Throwable ex) { - 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(Collection components) { - return components.stream().map(Object::getClass).map(Class::getName).toList(); - } - - private static List classSimpleNames(Collection components) { - return components.stream().map(Object::getClass).map(Class::getSimpleName).toList(); - } - } 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 40b1f160810..ba0855ff5d7 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 @@ -16,7 +16,6 @@ package org.springframework.test.context.support; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -32,8 +31,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.io.support.SpringFactoriesLoader; -import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler; import org.springframework.lang.Nullable; import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.CacheAwareContextLoaderDelegate; @@ -52,6 +49,7 @@ import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.TestExecutionListeners.MergeMode; +import org.springframework.test.context.util.TestContextSpringFactoriesUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -189,30 +187,15 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot /** * Get the default {@link TestExecutionListener TestExecutionListeners} for * this bootstrapper. + *

The default implementation delegates to + * {@link TestContextSpringFactoriesUtils#loadFactoryImplementations(Class)}. *

This method is invoked by {@link #getTestExecutionListeners()}. - *

The default implementation looks up and instantiates all - * {@code org.springframework.test.context.TestExecutionListener} entries - * configured in all {@code META-INF/spring.factories} files on the classpath. - *

If a particular listener cannot be loaded due to a {@link LinkageError} - * or {@link ClassNotFoundException}, a {@code DEBUG} message will be logged, - * but the associated exception will not be rethrown. A {@link RuntimeException} - * or any other {@link Error} will be rethrown. Any other exception will be - * thrown wrapped in an {@link IllegalStateException}. * @return an unmodifiable list of default {@code TestExecutionListener} * instances * @since 6.0 - * @see SpringFactoriesLoader#forDefaultResourceLocation() - * @see SpringFactoriesLoader#load(Class, FailureHandler) */ protected List getDefaultTestExecutionListeners() { - SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader()); - List listeners = - loader.load(TestExecutionListener.class, this::handleInstantiationFailure); - if (logger.isTraceEnabled()) { - logger.trace("Loaded default TestExecutionListener implementations from location [%s]: %s" - .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(listeners))); - } - return Collections.unmodifiableList(listeners); + return TestContextSpringFactoriesUtils.loadFactoryImplementations(TestExecutionListener.class); } @SuppressWarnings("unchecked") @@ -225,7 +208,14 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot catch (BeanInstantiationException ex) { Throwable cause = ex.getCause(); if (cause instanceof ClassNotFoundException || cause instanceof NoClassDefFoundError) { - logSkippedComponent(TestExecutionListener.class, listenerClass.getName(), cause); + 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(TestExecutionListener.class.getSimpleName(), listenerClass.getName(), + cause.getMessage())); + } } else { throw ex; @@ -409,21 +399,12 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot /** * Get the {@link ContextCustomizerFactory} instances for this bootstrapper. - *

The default implementation uses the {@link SpringFactoriesLoader} mechanism - * for loading factories configured in all {@code META-INF/spring.factories} - * files on the classpath. + *

The default implementation delegates to + * {@link TestContextSpringFactoriesUtils#loadFactoryImplementations(Class)}. * @since 4.3 - * @see SpringFactoriesLoader#loadFactories */ protected List getContextCustomizerFactories() { - SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader()); - List factories = - loader.load(ContextCustomizerFactory.class, this::handleInstantiationFailure); - if (logger.isTraceEnabled()) { - logger.trace("Loaded ContextCustomizerFactory implementations from location [%s]: %s" - .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(factories))); - } - return factories; + return TestContextSpringFactoriesUtils.loadFactoryImplementations(ContextCustomizerFactory.class); } /** @@ -552,53 +533,10 @@ 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 classSimpleNames(Collection components) { return components.stream().map(Object::getClass).map(Class::getSimpleName).toList(); } - private static List classNames(Collection components) { - return components.stream().map(Object::getClass).map(Class::getName).toList(); - } - private static boolean areAllEmpty(Collection... collections) { return Arrays.stream(collections).allMatch(Collection::isEmpty); } diff --git a/spring-test/src/main/java/org/springframework/test/context/util/TestContextFailureHandler.java b/spring-test/src/main/java/org/springframework/test/context/util/TestContextFailureHandler.java new file mode 100644 index 00000000000..0fbe8e4795b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/util/TestContextFailureHandler.java @@ -0,0 +1,68 @@ +/* + * 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.test.context.util; + +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler; + +/** + * Spring factories {@link FailureHandler} used within the Spring TestContext + * Framework. + * + * @author Sam Brannen + * @since 6.0 + */ +class TestContextFailureHandler implements FailureHandler { + + private final Log logger = LogFactory.getLog(TestContextSpringFactoriesUtils.class); + + @Override + public void handleFailure(Class factoryType, String factoryImplementationName, Throwable failure) { + Throwable ex = (failure instanceof InvocationTargetException ite ? ite.getTargetException() : failure); + if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) { + 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())); + } + } + 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); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/util/TestContextSpringFactoriesUtils.java b/spring-test/src/main/java/org/springframework/test/context/util/TestContextSpringFactoriesUtils.java new file mode 100644 index 00000000000..7f67cf6e407 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/util/TestContextSpringFactoriesUtils.java @@ -0,0 +1,83 @@ +/* + * 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.test.context.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler; + +import static org.springframework.core.io.support.SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION; + +/** + * Collection of utilities for working with {@link SpringFactoriesLoader} within + * the Spring TestContext Framework. + * + *

Primarily intended for use within the TestContext framework. + * + * @author Sam Brannen + * @since 6.0 + */ +public abstract class TestContextSpringFactoriesUtils { + + private static final Log logger = LogFactory.getLog(TestContextSpringFactoriesUtils.class); + + + private TestContextSpringFactoriesUtils() { + // no-op + } + + + /** + * Load factory implementations of the given type via the + * {@link SpringFactoriesLoader} mechanism. + *

This method utilizes a custom {@link FailureHandler} and DEBUG/TRACE logging + * that are specific to the needs of the Spring TestContext Framework. + *

Specifically, this method looks up and instantiates all {@code factoryType} + * entries configured in all {@code META-INF/spring.factories} files on the classpath. + *

If a particular factory implementation cannot be loaded due to a {@link LinkageError} + * or {@link ClassNotFoundException}, a {@code DEBUG} message will be logged, + * but the associated exception will not be rethrown. A {@link RuntimeException} + * or any other {@link Error} will be rethrown. Any other exception will be + * thrown wrapped in an {@link IllegalStateException}. + * @param the factory type + * @param factoryType the interface or abstract class representing the factory + * @return an unmodifiable list of factory implementations + * @see SpringFactoriesLoader#forDefaultResourceLocation(ClassLoader) + * @see SpringFactoriesLoader#load(Class, org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler) + */ + public static List loadFactoryImplementations(Class factoryType) { + SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation( + TestContextSpringFactoriesUtils.class.getClassLoader()); + List implementations = loader.load(factoryType, new TestContextFailureHandler()); + if (logger.isTraceEnabled()) { + logger.trace("Loaded %s implementations from location [%s]: %s" + .formatted(factoryType.getSimpleName(), FACTORIES_RESOURCE_LOCATION, classNames(implementations))); + } + return Collections.unmodifiableList(implementations); + } + + private static List classNames(Collection components) { + return components.stream().map(Object::getClass).map(Class::getName).toList(); + } + +}