diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java index f0b25d6d3b..f5c3499428 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -42,6 +42,7 @@ import org.apache.commons.logging.LogFactory; * @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME * @see org.springframework.beans.CachedIntrospectionResults#IGNORE_BEANINFO_PROPERTY_NAME * @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME + * @see org.springframework.test.context.cache.ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME */ public abstract class SpringProperties { 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 895109fa59..e79163336e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -26,7 +26,9 @@ import org.springframework.test.context.MergedContextConfiguration; * TestContext Framework. * *

A {@code ContextCache} maintains a cache of {@code ApplicationContexts} - * keyed by {@link MergedContextConfiguration} instances. + * keyed by {@link MergedContextConfiguration} instances, potentially + * configured with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize + * maximum size} and a custom eviction policy. * *

Rationale

*

Context caching can have significant performance benefits if context @@ -40,6 +42,7 @@ import org.springframework.test.context.MergedContextConfiguration; * @author Sam Brannen * @author Juergen Hoeller * @since 4.2 + * @see ContextCacheUtils#retrieveMaxCacheSize() */ public interface ContextCache { @@ -49,6 +52,24 @@ public interface ContextCache { */ public static final String CONTEXT_CACHE_LOGGING_CATEGORY = "org.springframework.test.context.cache"; + /** + * The default maximum size of the context cache: {@value #DEFAULT_MAX_CONTEXT_CACHE_SIZE}. + * @see #MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME + */ + public static final int DEFAULT_MAX_CONTEXT_CACHE_SIZE = 32; + + /** + * System property used to configure the maximum size of the {@link ContextCache} + * as a positive integer. + *

May alternatively be configured via + * {@link org.springframework.core.SpringProperties SpringProperties}. + *

Note that implementations of {@code ContextCache} are not required + * to support a maximum cache size. Consult the documentation of the + * corresponding implementation for details. + * @see #DEFAULT_MAX_CONTEXT_CACHE_SIZE + */ + public static final String MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME = "spring.test.context.cache.maxSize"; + /** * Determine whether there is a cached context for the given key. @@ -59,8 +80,8 @@ public interface ContextCache { /** * Obtain a cached {@code ApplicationContext} for the given key. - *

The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts - * must be updated accordingly. + *

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 @@ -70,7 +91,7 @@ public interface ContextCache { /** * Explicitly add an {@code ApplicationContext} instance to the cache - * under the given key. + * under the given key, potentially honoring a custom eviction policy. * @param key the context key (never {@code null}) * @param context the {@code ApplicationContext} instance (never {@code null}) */ @@ -80,9 +101,10 @@ public interface ContextCache { * Remove the context with the given key from the cache and explicitly * {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close} * it if it is an instance of {@code ConfigurableApplicationContext}. - *

Generally speaking, this method should be called if the state of - * a singleton bean has been modified, potentially affecting future - * interaction with the context. + *

Generally speaking, this method should be called to properly evict + * a context from the cache (e.g., due to a custom eviction policy) or if + * the state of a singleton bean has been modified, potentially affecting + * future interaction with the context. *

In addition, the semantics of the supplied {@code HierarchyMode} must * be honored. See the Javadoc for {@link HierarchyMode} for details. * @param key the context key; never {@code null} diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java new file mode 100644 index 0000000000..c4df35ce1b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.cache; + +import org.springframework.core.SpringProperties; +import org.springframework.util.StringUtils; + +/** + * Collection of utilities for working with {@link ContextCache ContextCaches}. + * + * @author Sam Brannen + * @since 4.3 + */ +public abstract class ContextCacheUtils { + + private ContextCacheUtils() { + /* no-op */ + } + + + /** + * Retrieve the maximum size of the {@link ContextCache}. + *

Uses {@link SpringProperties} to retrieve a system property or Spring + * property named {@code spring.test.context.cache.maxSize}. + *

Falls back to the value of the {@link ContextCache#DEFAULT_MAX_CONTEXT_CACHE_SIZE} + * if no such property has been set or if the property is not an integer. + * @return the maximum size of the context cache + * @see ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME + */ + public static int retrieveMaxCacheSize() { + try { + String maxSize = SpringProperties.getProperty(ContextCache.MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME); + if (StringUtils.hasText(maxSize)) { + return Integer.parseInt(maxSize.trim()); + } + } + catch (Exception ex) { + /* ignore */ + } + + // Fallback + return ContextCache.DEFAULT_MAX_CONTEXT_CACHE_SIZE; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java index 0967813594..e6ae3897be 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,7 +17,9 @@ package org.springframework.test.context.cache; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -37,12 +39,18 @@ import org.springframework.util.Assert; /** * Default implementation of the {@link ContextCache} API. * - *

Uses {@link ConcurrentHashMap ConcurrentHashMaps} to cache - * {@link ApplicationContext} and {@link MergedContextConfiguration} instances. + *

Uses a synchronized {@link Map} configured with a maximum size + * and a least recently used (LRU) eviction policy to cache + * {@link ApplicationContext} instances. + * + *

The maximum size may be supplied as a {@linkplain #DefaultContextCache(int) + * constructor argument} or set via a system property or Spring property named + * {@code spring.test.context.cache.maxSize}. * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 + * @see ContextCacheUtils#retrieveMaxCacheSize() */ public class DefaultContextCache implements ContextCache { @@ -52,7 +60,7 @@ public class DefaultContextCache implements ContextCache { * Map of context keys to Spring {@code ApplicationContext} instances. */ private final Map contextMap = - new ConcurrentHashMap(64); + Collections.synchronizedMap(new LruCache(32, 0.75f)); /** * Map of parent keys to sets of children keys, representing a top-down tree @@ -61,13 +69,41 @@ public class DefaultContextCache implements ContextCache { * of other contexts. */ private final Map> hierarchyMap = - new ConcurrentHashMap>(64); + new ConcurrentHashMap>(32); + + private final int maxSize; private final AtomicInteger hitCount = new AtomicInteger(); private final AtomicInteger missCount = new AtomicInteger(); + /** + * Create a new {@code DefaultContextCache} using the maximum cache size + * obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}. + * @since 4.3 + * @see #DefaultContextCache(int) + * @see ContextCacheUtils#retrieveMaxCacheSize() + */ + public DefaultContextCache() { + this(ContextCacheUtils.retrieveMaxCacheSize()); + } + + /** + * Create a new {@code DefaultContextCache} using the supplied maximum + * cache size. + * @param maxSize the maximum cache size + * @throws IllegalArgumentException if the supplied {@code maxSize} value + * is not positive + * @since 4.3 + * @see #DefaultContextCache() + */ + public DefaultContextCache(int maxSize) { + Assert.isTrue(maxSize > 0, "maxSize must be positive"); + this.maxSize = maxSize; + } + + /** * {@inheritDoc} */ @@ -181,6 +217,13 @@ public class DefaultContextCache implements ContextCache { return this.contextMap.size(); } + /** + * Get the maximum size of this cache. + */ + public int getMaxSize() { + return this.maxSize; + } + /** * {@inheritDoc} */ @@ -210,7 +253,7 @@ public class DefaultContextCache implements ContextCache { */ @Override public void reset() { - synchronized (contextMap) { + synchronized (this.contextMap) { clear(); clearStatistics(); } @@ -221,7 +264,7 @@ public class DefaultContextCache implements ContextCache { */ @Override public void clear() { - synchronized (contextMap) { + synchronized (this.contextMap) { this.contextMap.clear(); this.hierarchyMap.clear(); } @@ -232,7 +275,7 @@ public class DefaultContextCache implements ContextCache { */ @Override public void clearStatistics() { - synchronized (contextMap) { + synchronized (this.contextMap) { this.hitCount.set(0); this.missCount.set(0); } @@ -259,10 +302,46 @@ public class DefaultContextCache implements ContextCache { public String toString() { return new ToStringCreator(this) .append("size", size()) + .append("maxSize", getMaxSize()) .append("parentContextCount", getParentContextCount()) .append("hitCount", getHitCount()) .append("missCount", getMissCount()) .toString(); } + + /** + * Simple cache implementation based on {@link LinkedHashMap} with a maximum + * size and a least recently used (LRU) eviction policy that + * properly closes application contexts. + * + * @author Sam Brannen + * @since 4.3 + */ + @SuppressWarnings("serial") + private class LruCache extends LinkedHashMap { + + /** + * Create a new {@code LruCache} with the supplied initial capacity and + * load factor. + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + */ + LruCache(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor, true); + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (this.size() > DefaultContextCache.this.getMaxSize()) { + // Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally + // invoke java.util.Map.remove(Object, Object). + DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL); + } + + // Return false since we invoke a custom eviction algorithm. + return false; + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java index 5d518d3c8f..4624fcb74d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -42,6 +42,7 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.*; * @author Sam Brannen * @author Michail Nikolaev * @since 3.1 + * @see LruContextCacheTests * @see SpringRunnerContextCacheTests */ public class ContextCacheTests { diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java new file mode 100644 index 0000000000..ac6cc72332 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.cache; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.SpringProperties; + +import static org.junit.Assert.*; +import static org.springframework.test.context.cache.ContextCacheUtils.*; +import static org.springframework.test.context.cache.ContextCache.*; + +/** + * Unit tests for {@link ContextCacheUtils}. + * + * @author Sam Brannen + * @since 4.3 + */ +public class ContextCacheUtilsTests { + + @Before + @After + public void clearProperties() { + System.clearProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME); + SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, null); + } + + @Test + public void retrieveMaxCacheSizeFromDefault() { + assertDefaultValue(); + } + + @Test + public void retrieveMaxCacheSizeFromBogusSystemProperty() { + System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "bogus"); + assertDefaultValue(); + } + + @Test + public void retrieveMaxCacheSizeFromBogusSpringProperty() { + SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "bogus"); + assertDefaultValue(); + } + + @Test + public void retrieveMaxCacheSizeFromDecimalSpringProperty() { + SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "3.14"); + assertDefaultValue(); + } + + @Test + public void retrieveMaxCacheSizeFromSystemProperty() { + System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "42"); + assertEquals(42, retrieveMaxCacheSize()); + } + + @Test + public void retrieveMaxCacheSizeFromSystemPropertyContainingWhitespace() { + System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "42\t"); + assertEquals(42, retrieveMaxCacheSize()); + } + + @Test + public void retrieveMaxCacheSizeFromSpringProperty() { + SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "99"); + assertEquals(99, retrieveMaxCacheSize()); + } + + private static void assertDefaultValue() { + assertEquals(DEFAULT_MAX_CONTEXT_CACHE_SIZE, retrieveMaxCacheSize()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/LruContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/LruContextCacheTests.java new file mode 100644 index 0000000000..4ae97b614c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/cache/LruContextCacheTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.cache; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.util.ReflectionTestUtils; + +import static java.util.Arrays.*; +import static java.util.stream.Collectors.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the LRU eviction policy in {@link DefaultContextCache}. + * + * @author Sam Brannen + * @since 4.3 + * @see ContextCacheTests + */ +public class LruContextCacheTests { + + private static final MergedContextConfiguration abcConfig = config(Abc.class); + private static final MergedContextConfiguration fooConfig = config(Foo.class); + private static final MergedContextConfiguration barConfig = config(Bar.class); + private static final MergedContextConfiguration bazConfig = config(Baz.class); + + + private final ConfigurableApplicationContext abcContext = mock(ConfigurableApplicationContext.class); + private final ConfigurableApplicationContext fooContext = mock(ConfigurableApplicationContext.class); + private final ConfigurableApplicationContext barContext = mock(ConfigurableApplicationContext.class); + private final ConfigurableApplicationContext bazContext = mock(ConfigurableApplicationContext.class); + + + @Test(expected = IllegalArgumentException.class) + public void maxCacheSizeNegativeOne() { + new DefaultContextCache(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void maxCacheSizeZero() { + new DefaultContextCache(0); + } + + @Test + public void maxCacheSizeOne() { + DefaultContextCache cache = new DefaultContextCache(1); + assertEquals(0, cache.size()); + assertEquals(1, cache.getMaxSize()); + + cache.put(fooConfig, fooContext); + assertCacheContents(cache, "Foo"); + + cache.put(fooConfig, fooContext); + assertCacheContents(cache, "Foo"); + + cache.put(barConfig, barContext); + assertCacheContents(cache, "Bar"); + + cache.put(fooConfig, fooContext); + assertCacheContents(cache, "Foo"); + } + + @Test + public void maxCacheSizeThree() { + DefaultContextCache cache = new DefaultContextCache(3); + assertEquals(0, cache.size()); + assertEquals(3, cache.getMaxSize()); + + cache.put(fooConfig, fooContext); + assertCacheContents(cache, "Foo"); + + cache.put(fooConfig, fooContext); + assertCacheContents(cache, "Foo"); + + cache.put(barConfig, barContext); + assertCacheContents(cache, "Foo", "Bar"); + + cache.put(bazConfig, bazContext); + assertCacheContents(cache, "Foo", "Bar", "Baz"); + + cache.put(abcConfig, abcContext); + assertCacheContents(cache, "Bar", "Baz", "Abc"); + } + + @Test + public void ensureLruOrderingIsUpdated() { + DefaultContextCache cache = new DefaultContextCache(3); + + // Note: when a new entry is added it is considered the MRU entry and inserted at the tail. + cache.put(fooConfig, fooContext); + cache.put(barConfig, barContext); + cache.put(bazConfig, bazContext); + assertCacheContents(cache, "Foo", "Bar", "Baz"); + + // Note: the MRU entry is moved to the tail when accessed. + cache.get(fooConfig); + assertCacheContents(cache, "Bar", "Baz", "Foo"); + + cache.get(barConfig); + assertCacheContents(cache, "Baz", "Foo", "Bar"); + + cache.get(bazConfig); + assertCacheContents(cache, "Foo", "Bar", "Baz"); + + cache.get(barConfig); + assertCacheContents(cache, "Foo", "Baz", "Bar"); + } + + @Test + public void ensureEvictedContextsAreClosed() { + DefaultContextCache cache = new DefaultContextCache(2); + + cache.put(fooConfig, fooContext); + cache.put(barConfig, barContext); + assertCacheContents(cache, "Foo", "Bar"); + + cache.put(bazConfig, bazContext); + assertCacheContents(cache, "Bar", "Baz"); + verify(fooContext, times(1)).close(); + + cache.put(abcConfig, abcContext); + assertCacheContents(cache, "Baz", "Abc"); + verify(barContext, times(1)).close(); + + verify(abcContext, never()).close(); + verify(bazContext, never()).close(); + } + + + private static MergedContextConfiguration config(Class clazz) { + return new MergedContextConfiguration(null, null, new Class[] { clazz }, null, null); + } + + @SuppressWarnings("unchecked") + private static void assertCacheContents(DefaultContextCache cache, String... expectedNames) { + + Map contextMap = + (Map) ReflectionTestUtils.getField(cache, "contextMap"); + + // @formatter:off + List actualNames = contextMap.keySet().stream() + .map(cfg -> cfg.getClasses()[0]) + .map(Class::getSimpleName) + .collect(toList()); + // @formatter:on + + assertEquals(asList(expectedNames), actualNames); + } + + + private static class Abc {} + private static class Foo {} + private static class Bar {} + private static class Baz {} + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringRunnerContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/SpringRunnerContextCacheTests.java index 44962e7660..bde994dc0f 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/SpringRunnerContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/SpringRunnerContextCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -45,6 +45,7 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.*; * @author Juergen Hoeller * @since 2.5 * @see ContextCacheTests + * @see LruContextCacheTests */ @RunWith(SpringJUnit4ClassRunner.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) diff --git a/src/asciidoc/whats-new.adoc b/src/asciidoc/whats-new.adoc index 15643ccb26..03fa76f07d 100644 --- a/src/asciidoc/whats-new.adoc +++ b/src/asciidoc/whats-new.adoc @@ -696,6 +696,10 @@ Spring 4.3 also improves the caching abstraction as follows: XML files, Groovy scripts, or `@Configuration` classes are detected. * `@Transactional` test methods are no longer required to be `public` (in TestNG and JUnit 5). * `@BeforeTransaction` and `@AfterTransaction` methods are no longer required to be `public`. +* The `ApplicationContext` cache in the _Spring TestContext Framework_ is now bounded with a + default maximum size of 32 and a _least recently used_ eviction policy. The maximum size + can be configured by setting a JVM system property or Spring property called + `spring.test.context.cache.maxSize`. * New `ContextCustomizer` API for customizing a test `ApplicationContext` _after_ bean definitions have been loaded into the context but _before_ the context has been refreshed. Customizers can be registered globally by third parties, foregoing the need to implement a