Hazelcast 3.6升级3.12.4后多测试执行缓存管理器覆盖异常问题
我之前也踩过类似的Hazelcast升级结合Spring测试的坑,咱们从问题根源到可行方案一步步拆解:
问题根源分析
从Hazelcast 3.9开始,AbstractHazelcastCacheManager的getCache()逻辑发生了关键变化:
- 在3.6版本中,调用
getCache()如果找不到对应缓存会直接返回null; - 但3.9+版本会先创建一个
CacheProxy实例,并调用cacheProxy.setCacheManager()绑定当前的缓存管理器。
当你的测试套件触发Spring上下文刷新时(比如因为上下文配置不一致导致无法复用,或者@DirtiesContext触发销毁重建),会生成一个新的CacheManager实例。此时后续测试调用initCache()时:
- 新的
CacheManager调用getCache(),发现Hazelcast实例中已经存在对应缓存; - 尝试将新的
CacheManager绑定到已存在的CacheProxy上; - 由于Hazelcast的
CacheProxy不允许覆盖已绑定的缓存管理器(且未校验新旧管理器是否为同一实例),直接抛出IllegalStateException: Cannot overwrite a Cache's CacheManager。
另外,Spring测试上下文默认会复用配置完全一致的上下文,但如果你的测试类存在配置细微差异(比如@ContextConfiguration的class顺序不同、额外的测试配置注解),就会触发上下文重新刷新,这也是问题的诱因之一。
可行解决方案
1. 修改initCache()逻辑,避免不必要的CacheProxy绑定
调整缓存初始化逻辑,先判断缓存是否存在,再决定是否调用getCache()或createCache(),避免触发Hazelcast自动创建CacheProxy:
方案A:通过HazelcastInstance提前判断缓存存在性
public Cache<T, V> initCache(String cacheName, Class<T> keyType, Class<V> valueType) { // 先通过Hazelcast实例判断缓存对应的Map是否存在,跳过getCache()的自动代理创建 HazelcastInstance hazelcastInstance = ((HazelcastCacheManager) manager).getHazelcastInstance(); if (hazelcastInstance.getMap(cacheName) != null) { return manager.getCache(cacheName, keyType, valueType); } // 缓存不存在时再创建 return manager.createCache(cacheName, config); }
方案B:捕获异常并复用已有缓存
如果判断存在性的方式不适用,可以直接捕获IllegalStateException,此时说明缓存已存在只是绑定了旧管理器,直接返回现有缓存即可:
public Cache<T, V> initCache(String cacheName, Class<T> keyType, Class<V> valueType) { try { Cache<T, V> cache = manager.getCache(cacheName, keyType, valueType); if (cache != null) { return cache; } } catch (IllegalStateException e) { // 捕获到覆盖管理器的异常,说明缓存已存在,直接返回无泛型的缓存实例 return manager.getCache(cacheName); } return manager.createCache(cacheName, config); }
2. 确保Spring测试上下文复用,避免不必要的刷新
Spring测试上下文会根据配置的唯一性进行缓存,只要配置完全一致就会复用,不需要重新刷新:
- 统一测试基类:创建一个包含所有基础配置的测试基类,让所有测试类继承它,确保
@WebAppConfiguration和@ContextConfiguration完全一致:@WebAppConfiguration @ContextConfiguration(classes = {AppConfig.class}) public abstract class BaseSpringTest extends AbstractTestNGSpringContextTests { // 基础测试逻辑 } - 谨慎使用
@DirtiesContext:如果必须使用该注解标记测试类污染了上下文,尽量使用@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS),不要用AFTER_EACH_TEST_METHOD,减少上下文重建次数; - 检查配置一致性:确保所有测试类的
@ContextConfiguration的classes属性顺序、额外注解(比如@ActiveProfiles)完全一致,任何细微差异都会导致Spring创建新的上下文。
3. 正确管理HazelcastInstance的生命周期
给HazelcastInstance的Bean指定销毁方法,确保上下文销毁时正确关闭实例,避免残留的CacheProxy导致后续冲突:
@Bean(destroyMethod = "shutdown") public HazelcastInstance hazelcastInstance() { return Hazelcast.newHazelcastInstance(); } @Bean public CacheManager cacheManager() { return new HazelcastCacheManager(hazelcastInstance()); }
这样当上下文不得不刷新时,旧的HazelcastInstance会被彻底关闭,新的实例创建的缓存不会和旧的CacheProxy冲突。
补充:为什么getCache()会重新创建代理?
Hazelcast 3.9+对JCache规范的实现进行了优化,getCache()方法被设计为“提前占位”:当调用getCache()找不到缓存时,会先创建一个CacheProxy绑定当前管理器,后续调用createCache()时会将这个代理关联到实际的缓存实例。这种设计在单上下文环境下没问题,但在Spring测试的多上下文刷新场景下,就会出现新管理器无法绑定旧代理的冲突。
内容的提问来源于stack exchange,提问作者Petre Popescu

