SPRING 一月 26, 2025

Spring Test 模块中的上下文缓存特性

文章字数 8.7k 阅读约需 8 mins. 阅读次数

上下文缓存概述

Context CachingSpring Framework 中的 Spring TestContext Framework 所提供的 Context Management 上下文管理能力对测试所需使用的应用上下文的缓存支持,以减少初始化相同的应用上下文导致的时间浪费。

初始化多个 context 对构建时间的影响

当执行测试用例时,若未能完全复用缓存中的 context,将会无谓的拖慢测试阶段的耗时,进而影响快速反馈的效果。

那么初始化多个 context 会对构建时间产生多大的影响呢?

这个问题会因环境而异:不同的初始化次数、容器中初始化的不同的 bean,都会产生不同的结果。

举两个例子直观感受一下:

模块 多个上下文 一个上下文
A 40+s 10+s
B 60+s 13+s

模块 A 在测试阶段会在缓存中创建两个 context,构建模块 A 耗时大约为 40+s,在将测试用例所使用的 context 调整为一个后,构建模块 A 耗时大约为 10+s。

Spring Boot 应用可以通过观察日志中打印的 banner 次数统计初始化上下文的次数。

初始化多个 context 的原因

Once the TestContext framework loads an ApplicationContext (or WebApplicationContext) for a test, that context is cached and reused for all subsequent tests that declare the same unique context configuration within the same test suite.

—— Context Caching

首次初始化的上下文在缓存之后,会在相同测试套件(test suite)的相同且唯一(unique)的上下文配置中复用。任一条件未满足时,则会引起新上下文的初始化,并放入缓存中备用。

缓存的上下文数量超过上限导致早期缓存的上下文被驱逐后,也可能导致新的上下文初始化过程。

缓存大小及清理策略

spring-frameworkspring-test 模块中有一个 ContextCache 接口, 并提供了 DefaultContextCache 默认实现。其中的私有属性 contextMap 即测试所使用的上下文的缓存:

private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
            Collections.synchronizedMap(new LruCache(32, 0.75f));

缓存 Map 初始化及默认的大小是 32DEFAULT_MAX_CONTEXT_CACHE_SIZE),可通过 spring.test.context.cache.maxSize 参数调整缓存的最大数量。

缓存采用 LRU(least recently used,最近最少使用)策略清理,缓存命中相关统计信息可以通过将 org.springframework.test.context.cache 包的日志级别设置为 DEBUG 在日志中查看。

缓存 Map 所使用的 key,即为上下文缓存的唯一标识。

缓存唯一标识

CacheAwareContextLoaderDelegate 负责通过 ContextCache 加载或清除缓存的上下文。其默认实现 DefaultCacheAwareContextLoaderDelegateloadContext 方法中操作 ContextCache 提供的类似 Mapgetput 方法,控制缓存的读取和放入。

ContextCache 使用 MergedContextConfiguration 作为缓存的唯一标识,用来判断是否可以复用已缓存的上下文。

MergedContextConfiguration 覆盖了基类的 equalshashCode 方法,如下内容都一致的两个 MergedContextConfiguration 被认为是相等的:

  • locations (from @ContextConfiguration)
  • classes (from @ContextConfiguration)
  • contextInitializerClasses (from @ContextConfiguration)
  • contextCustomizers (from ContextCustomizerFactory) – this includes @DynamicPropertySource methods as well as various features from Spring Boot’s testing support such as @MockBean and @SpyBean.
  • contextLoader (from @ContextConfiguration)
  • parent (from @ContextHierarchy)
  • activeProfiles (from @ActiveProfiles)
  • propertySourceDescriptors (from @TestPropertySource)
  • propertySourceProperties (from @TestPropertySource)
  • resourceBasePath (from @WebAppConfiguration)

—— Context Caching

resourceBasePath 是在 WebMergedContextConfiguration 中比较的。

DefaultCacheAwareContextLoaderDelegate 加载新的 context 后,会在 DEBUG 级别打印日志:Storing ApplicationContext in cache under key ...,并将新的 context 追加至 contextCache。

测试套件

DefaultCacheAwareContextLoaderDelegate 使用静态变量初始化上下文缓存:

    /**
     * Default static cache of Spring application contexts.
     */
    static final ContextCache defaultContextCache = new DefaultContextCache();

所以运行在不同进程中的测试,无法共享上下文缓存。故,测试套件(test suite),在这里指的是运行在相同 JVM 中的所有测试用例集合。

如何避免初始化多个 context

关键就是保证缓存的 key 是相同的,即测试用例所使用的 MergedContextConfiguration 是一致的。

Context not being reused in tests when MockBeans are used 中给出了一个解决由在不同的测试用例中使用 @MockBean 导致的 context 未被复用的例子,思路是创建一个抽象基类,将所有需要使用 @MockBean 的定义在基类中统一定义,供所有测试用例使用,以达到 contextCustomizers 及其他 MergedContextConfiguration 中属性完全一致的效果:

@RunWith(SpringRunner.class)
@WebMvcTest
public abstract class AbstractTest {
    protected @MockBean FooBarService service;
}

public class FooTest extends AbstractTest {...}
0%