You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hazelcast 3.6升级3.12.4后多测试执行缓存管理器覆盖异常问题

解决Hazelcast 3.12.4升级后Spring多测试类运行的缓存管理器冲突问题

我之前也踩过类似的Hazelcast升级结合Spring测试的坑,咱们从问题根源到可行方案一步步拆解:

问题根源分析

从Hazelcast 3.9开始,AbstractHazelcastCacheManager的getCache()逻辑发生了关键变化:

  • 在3.6版本中,调用getCache()如果找不到对应缓存会直接返回null;
  • 但3.9+版本会先创建一个CacheProxy实例,并调用cacheProxy.setCacheManager()绑定当前的缓存管理器。

当你的测试套件触发Spring上下文刷新时(比如因为上下文配置不一致导致无法复用,或者@DirtiesContext触发销毁重建),会生成一个新的CacheManager实例。此时后续测试调用initCache()时:

  1. 新的CacheManager调用getCache(),发现Hazelcast实例中已经存在对应缓存;
  2. 尝试将新的CacheManager绑定到已存在的CacheProxy上;
  3. 由于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:58:24