上下文缓存概述
Context Caching 是 Spring 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
(orWebApplicationContext
) 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.
首次初始化的上下文在缓存之后,会在相同测试套件(test suite)的相同且唯一(unique)的上下文配置中复用。任一条件未满足时,则会引起新上下文的初始化,并放入缓存中备用。
缓存的上下文数量超过上限导致早期缓存的上下文被驱逐后,也可能导致新的上下文初始化过程。
缓存大小及清理策略
在 spring-framework 的 spring-test
模块中有一个 ContextCache 接口, 并提供了 DefaultContextCache 默认实现。其中的私有属性 contextMap
即测试所使用的上下文的缓存:
private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
Collections.synchronizedMap(new LruCache(32, 0.75f));
缓存 Map 初始化及默认的大小是 32
(DEFAULT_MAX_CONTEXT_CACHE_SIZE),可通过 spring.test.context.cache.maxSize 参数调整缓存的最大数量。
缓存采用 LRU(least recently used,最近最少使用)策略清理,缓存命中相关统计信息可以通过将 org.springframework.test.context.cache
包的日志级别设置为 DEBUG
在日志中查看。
缓存 Map 所使用的 key,即为上下文缓存的唯一标识。
缓存唯一标识
CacheAwareContextLoaderDelegate 负责通过 ContextCache
加载或清除缓存的上下文。其默认实现 DefaultCacheAwareContextLoaderDelegate 在 loadContext
方法中操作 ContextCache
提供的类似 Map
的 get
和 put
方法,控制缓存的读取和放入。
ContextCache
使用 MergedContextConfiguration 作为缓存的唯一标识,用来判断是否可以复用已缓存的上下文。
MergedContextConfiguration
覆盖了基类的 equals 和 hashCode 方法,如下内容都一致的两个 MergedContextConfiguration
被认为是相等的:
locations
(from@ContextConfiguration
)classes
(from@ContextConfiguration
)contextInitializerClasses
(from@ContextConfiguration
)contextCustomizers
(fromContextCustomizerFactory
) – 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
)
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 {...}