diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc index aa749f6b78d..b4beba546d8 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc @@ -62,6 +62,15 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize alternative, you can set the same property via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. +As of Spring Framework 7.0, an application context stored in the context cache will be +stopped when it is no longer actively in use and automatically restarted the next time +the context is retrieved from the cache. Specifically, the latter will restart all +auto-startup beans in the application context, effectively restoring the lifecycle state. +This ensures that background processes within the context are not actively running while +the context is not used by tests. For example, JMS listener containers, scheduled tasks, +and any other components in the context that implement `Lifecycle` or `SmartLifecycle` +will be in a "stopped" state until the context is used again by a test. + Since having a large number of application contexts loaded within a given test suite can cause the suite to take an unnecessarily long time to run, it is often beneficial to know exactly how many contexts have been loaded and cached. To view the statistics for 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 0fa050afbb1..0bb844431c6 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 @@ -29,6 +29,11 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode; * {@link org.springframework.test.context.cache.ContextCache ContextCache} * behind the scenes. * + *

As of Spring Framework 7.0, this SPI includes optional support for + * {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and + * {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering} + * context usage. + * *

Note: {@code CacheAwareContextLoaderDelegate} does not extend the * {@link ContextLoader} or {@link SmartContextLoader} interface. * @@ -142,4 +147,38 @@ public interface CacheAwareContextLoaderDelegate { */ void closeContext(MergedContextConfiguration mergedConfig, @Nullable HierarchyMode hierarchyMode); + /** + * Register usage of the {@linkplain ApplicationContext application context} + * for the supplied {@link MergedContextConfiguration} as well as usage of the + * application context for its {@linkplain MergedContextConfiguration#getParent() + * parent}, recursively. + *

This is intended to be invoked whenever a + * {@link org.springframework.test.context.TestExecutionListener TestExecutionListener} + * interacts with the application context(s) on behalf of the supplied test class. + * @param key the context key; never {@code null} + * @param testClass the test class that is using the application context(s) + * @since 7.0 + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + */ + default void registerContextUsage(MergedContextConfiguration key, Class testClass) { + /* no-op */ + } + + /** + * Unregister usage of the {@linkplain ApplicationContext application context} + * for the supplied {@link MergedContextConfiguration} as well as usage of the + * application context for its {@linkplain MergedContextConfiguration#getParent() + * parent}, recursively. + *

This informs the {@code ContextCache} that the application context(s) can + * be safely {@linkplain org.springframework.context.Lifecycle#stop() stopped} + * if no other test classes are actively using the same application context(s). + * @param key the context key; never {@code null} + * @param testClass the test class that is no longer using the application context(s) + * @since 7.0 + * @see #registerContextUsage(MergedContextConfiguration, Class) + */ + default void unregisterContextUsage(MergedContextConfiguration key, Class testClass) { + /* no-op */ + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java index 4b90420d9ac..9e5ce628843 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java @@ -53,7 +53,8 @@ public interface TestContext extends AttributeAccessor, Serializable { * Determine if the {@linkplain ApplicationContext application context} for * this test context is known to be available. *

If this method returns {@code true}, a subsequent invocation of - * {@link #getApplicationContext()} should succeed. + * {@link #getApplicationContext()} or {@link #markApplicationContextUnused()} + * should succeed. *

The default implementation of this method always returns {@code false}. * Custom {@code TestContext} implementations are therefore highly encouraged * to override this method with a more meaningful implementation. Note that @@ -62,6 +63,7 @@ public interface TestContext extends AttributeAccessor, Serializable { * @return {@code true} if the application context has already been loaded * @since 5.2 * @see #getApplicationContext() + * @see #markApplicationContextUnused() */ default boolean hasApplicationContext() { return false; @@ -77,6 +79,7 @@ public interface TestContext extends AttributeAccessor, Serializable { * @throws IllegalStateException if an error occurs while retrieving the * application context * @see #hasApplicationContext() + * @see #markApplicationContextUnused() */ ApplicationContext getApplicationContext(); @@ -128,6 +131,24 @@ public interface TestContext extends AttributeAccessor, Serializable { */ @Nullable Throwable getTestException(); + /** + * Call this method to signal that the {@linkplain #getTestClass() test class} + * is no longer using the {@linkplain ApplicationContext application context} + * associated with this test context. + *

This informs the context cache that the application context can be + * safely {@linkplain org.springframework.context.Lifecycle#stop() stopped} + * if no other test classes are actively using the same application context. + *

This method is intended to be invoked after execution of the test class + * has ended and should not be invoked unless the application context for this + * test context is known to be {@linkplain #hasApplicationContext() available}. + *

This feature is primarily intended for use within the framework. + * @since 7.0 + * @see TestContextManager#afterTestClass() + */ + default void markApplicationContextUnused() { + /* no-op */ + } + /** * Call this method to signal that the {@linkplain ApplicationContext application * context} associated with this test context is dirty and should be diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index a6c5dde3d3f..6b46c4f0042 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -520,10 +520,14 @@ public class TestContextManager { * the first exception. *

Note that listeners will be executed in the opposite order in which they * were registered. + *

As of Spring Framework 7.0, this method also ensures that the application + * context for the current {@link #getTestContext() TestContext} is marked as + * {@linkplain TestContext#markApplicationContextUnused() unused}. * @throws Exception if a registered TestExecutionListener throws an exception * @since 3.0 * @see #getTestExecutionListeners() * @see Throwable#addSuppressed(Throwable) + * @see TestContext#markApplicationContextUnused() */ public void afterTestClass() throws Exception { Class testClass = getTestContext().getTestClass(); @@ -550,6 +554,20 @@ public class TestContextManager { } } + try { + if (getTestContext().hasApplicationContext()) { + getTestContext().markApplicationContextUnused(); + } + } + catch (Throwable ex) { + if (afterTestClassException == null) { + afterTestClassException = ex; + } + else { + afterTestClassException.addSuppressed(ex); + } + } + this.testContextHolder.remove(); if (afterTestClassException != null) { diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java index e92e1a947b1..8840a4655a6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java @@ -35,7 +35,10 @@ import org.springframework.test.context.MergedContextConfiguration; *

As of Spring Framework 6.1, this SPI includes optional support for * {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and * {@linkplain #incrementFailureCount(MergedContextConfiguration) incrementing} - * failure counts. + * failure counts. As of Spring Framework 7.0, this SPI includes optional support for + * {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and + * {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering} + * context usage. * *

Rationale

*

Context caching can have significant performance benefits if context @@ -88,13 +91,19 @@ public interface ContextCache { boolean contains(MergedContextConfiguration key); /** - * Obtain a cached {@code ApplicationContext} for the given key. - *

The {@linkplain #getHitCount() hit} and {@linkplain #getMissCount() miss} - * counts must be updated accordingly. + * Obtain a cached {@link ApplicationContext} for the given key. + *

If the cached application context was previously + * {@linkplain org.springframework.context.Lifecycle#stop() stopped}, it + * must be + * {@linkplain org.springframework.context.support.AbstractApplicationContext#restart() + * restarted}. This applies to parent contexts as well. + *

In addition, the {@linkplain #getHitCount() hit} and + * {@linkplain #getMissCount() miss} counts must be updated accordingly. * @param key the context key (never {@code null}) * @return the corresponding {@code ApplicationContext} instance, or {@code null} * if not found in the cache - * @see #remove + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + * @see #remove(MergedContextConfiguration, HierarchyMode) */ @Nullable ApplicationContext get(MergedContextConfiguration key); @@ -151,6 +160,64 @@ public interface ContextCache { * @see #getFailureCount(MergedContextConfiguration) */ default void incrementFailureCount(MergedContextConfiguration key) { + /* no-op */ + } + + /** + * Register usage of the {@link ApplicationContext} for the supplied + * {@link MergedContextConfiguration} and any of its parents. + *

The default implementation of this method does nothing. Concrete + * implementations are therefore highly encouraged to override this + * method, {@link #unregisterContextUsage(MergedContextConfiguration, Class)}, + * and {@link #getContextUsageCount()} with appropriate behavior. Note that + * the standard {@code ContextContext} implementation in Spring overrides + * these methods appropriately. + * @param key the context key; never {@code null} + * @param testClass the test class that is using the application context(s) + * @since 7.0 + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + * @see #getContextUsageCount() + */ + default void registerContextUsage(MergedContextConfiguration key, Class testClass) { + /* no-op */ + } + + /** + * Unregister usage of the {@link ApplicationContext} for the supplied + * {@link MergedContextConfiguration} and any of its parents. + *

If no other test classes are actively using the same application + * context(s), the application context(s) should be + * {@linkplain org.springframework.context.Lifecycle#stop() stopped}. + *

The default implementation of this method does nothing. Concrete + * implementations are therefore highly encouraged to override this + * method, {@link #registerContextUsage(MergedContextConfiguration, Class)}, + * and {@link #getContextUsageCount()} with appropriate behavior. Note that + * the standard {@code ContextContext} implementation in Spring overrides + * these methods appropriately. + * @param key the context key; never {@code null} + * @param testClass the test class that is no longer using the application context(s) + * @since 7.0 + * @see #registerContextUsage(MergedContextConfiguration, Class) + * @see #getContextUsageCount() + */ + default void unregisterContextUsage(MergedContextConfiguration key, Class testClass) { + /* no-op */ + } + + /** + * Determine the number of contexts within the cache that are currently in use. + *

The default implementation of this method always returns {@code 0}. + * Concrete implementations are therefore highly encouraged to override this + * method, {@link #registerContextUsage(MergedContextConfiguration, Class)}, + * and {@link #unregisterContextUsage(MergedContextConfiguration, Class)} with + * appropriate behavior. Note that the standard {@code ContextContext} + * implementation in Spring overrides these methods appropriately. + * @since 7.0 + * @see #registerContextUsage(MergedContextConfiguration, Class) + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + */ + default int getContextUsageCount() { + return 0; } /** @@ -204,6 +271,7 @@ public interface ContextCache { *