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

使用EventListener与Repository产生循环依赖的解决方案咨询

问题:缓存过期监听器依赖Spring业务服务导致循环依赖

我尝试使用EntryExpiredListener在缓存条目过期时调用其他服务执行操作,但当该监听器调用的服务依赖系统中的任意Repository时,无法将监听器添加到缓存中,出现了Spring容器的循环依赖问题。

循环依赖链

The dependencies of some of the beans in the application context form a cycle:

   filterChainExceptionHandler (field private org.springframework.web.servlet.HandlerExceptionResolver sb.practice.configs.security.filters.FilterChainExceptionHandler.resolver)
      ↓
   org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration
      ↓
   openEntityManagerInViewInterceptorConfigurer defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration$JpaWebConfiguration.class]
      ↓
   openEntityManagerInViewInterceptor defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration$JpaWebConfiguration.class]
┌─────┐
|  cacheManager defined in class path resource [org/springframework/boot/autoconfigure/cache/HazelcastCacheConfiguration.class]
↑     ↓
|  cacheService defined in file [D:\Dev\Locals\pt-spring-boot\target\classes\sb\practice\services\impl\CacheService.class]
↑     ↓
|  activeUserCacheService_Hazelcast defined in file [D:\Dev\Locals\pt-spring-boot\target\classes\sb\practice\services\impl\caches\ActiveUserCacheService_Hazelcast.class]
↑     ↓
|  masterRoleRepository defined in sb.practice.repositories.masters.MasterRoleRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration
↑     ↓
|  (inner bean)#28f48a7c
└─────┘

现有代码

CacheListener.java

@Slf4j
public class CacheListener implements EntryExpiredListener<String, String>{
    
    public ActiveUserCacheService_Hazelcast activeUsersCache;

    @Override
    public void entryExpired(EntryEvent<String, String> event) {
        log.info("entryEvicted. Cache: {}. Value: {}. \nEvent: {} ", event.getName(), event.getOldValue(), event);
    }
}

CacheService.java

@Service
@RequiredArgsConstructor
public class CacheService {

    
    @Value("${hazelcast.cache.cluster.nodes}")
    String[] clusteringNodes;
    
    private final ActiveUserCacheService_Hazelcast activeUserCacheService_Hazelcast;

    public <T> boolean containsKey(long key, String cacheName) {
        IMap<Long, T> map = hazelcastInstance().getMap(cacheName);
        return map.containsKey(key);
    }

    private static MapConfig mapConfig(String name, Integer timeToLive) {

        MapConfig mapConfig = new MapConfig(name);
        mapConfig.setTimeToLiveSeconds(timeToLive);
//
//      EntryListenerConfig entryListenerConfig = new EntryListenerConfig();
//      entryListenerConfig.setImplementation(new CacheListener());
//      mapConfig.addEntryListenerConfig(entryListenerConfig);

        final EvictionConfig evictionConfig = new EvictionConfig();
        evictionConfig.setEvictionPolicy(EvictionPolicy.LRU);
        evictionConfig.setMaxSizePolicy(MaxSizePolicy.PER_NODE);
        evictionConfig.setSize(500);

        mapConfig.setEvictionConfig(evictionConfig);
        return mapConfig;
    }

    @Bean
    HazelcastInstance hazelcastInstance() {

        Config config = new Config();
        CacheConstants.CACHES.forEach((name, timeToLive) -> config.addMapConfig(mapConfig(name, timeToLive)));

        NetworkConfig network = config.getNetworkConfig();
        network.setPort(5701).setPortCount(20);
        network.setPortAutoIncrement(true);

        JoinConfig join = network.getJoin();
        join.getMulticastConfig().setEnabled(false);
        join.getTcpIpConfig().setEnabled(true);

        for (String clusteringNode : clusteringNodes) {
            join.getTcpIpConfig().addMember(clusteringNode);
        }

        return Hazelcast.newHazelcastInstance(config);
    }

}

解决方案

可以将EventListener作为Spring组件并调用业务服务,核心是打破循环依赖链,同时确保监听器由Spring容器管理依赖注入,具体步骤如下:

1. 让CacheListener成为Spring管理的Bean

给监听器添加@Component注解,使用构造注入依赖的业务服务,避免字段注入可能带来的初始化问题:

@Component
@Slf4j
public class CacheListener implements EntryExpiredListener<String, String> {
    
    private final ActiveUserCacheService_Hazelcast activeUsersCache;

    // 构造注入,明确依赖关系
    public CacheListener(ActiveUserCacheService_Hazelcast activeUsersCache) {
        this.activeUsersCache = activeUsersCache;
    }

    @Override
    public void entryExpired(EntryEvent<String, String> event) {
        log.info("entryEvicted. Cache: {}. Value: {}. \nEvent: {} ", event.getName(), event.getOldValue(), event);
        // 直接调用业务服务处理逻辑
        activeUsersCache.handleExpiredUser(event.getOldValue());
    }
}

2. 分离Hazelcast配置与业务服务

将HazelcastInstance的Bean定义移到单独的配置类,避免CacheService同时承担配置定义和业务逻辑的职责,打破循环依赖:

@Configuration
public class HazelcastConfig {
    
    @Value("${hazelcast.cache.cluster.nodes}")
    String[] clusteringNodes;

    @Bean
    HazelcastInstance hazelcastInstance() {
        Config config = new Config();
        CacheConstants.CACHES.forEach((name, timeToLive) -> config.addMapConfig(mapConfig(name, timeToLive)));

        NetworkConfig network = config.getNetworkConfig();
        network.setPort(5701).setPortCount(20);
        network.setPortAutoIncrement(true);

        JoinConfig join = network.getJoin();
        join.getMulticastConfig().setEnabled(false);
        join.getTcpIpConfig().setEnabled(true);

        for (String clusteringNode : clusteringNodes) {
            join.getTcpIpConfig().addMember(clusteringNode);
        }

        return Hazelcast.newHazelcastInstance(config);
    }

    private static MapConfig mapConfig(String name, Integer timeToLive) {
        MapConfig mapConfig = new MapConfig(name);
        mapConfig.setTimeToLiveSeconds(timeToLive);

        final EvictionConfig evictionConfig = new EvictionConfig();
        evictionConfig.setEvictionPolicy(EvictionPolicy.LRU);
        evictionConfig.setMaxSizePolicy(MaxSizePolicy.PER_NODE);
        evictionConfig.setSize(500);

        mapConfig.setEvictionConfig(evictionConfig);
        return mapConfig;
    }
}

3. 重构CacheService

简化CacheService,只保留业务逻辑,依赖Spring注入的HazelcastInstance:

@Service
@RequiredArgsConstructor
public class CacheService {

    private final HazelcastInstance hazelcastInstance;
    private final ActiveUserCacheService_Hazelcast activeUserCacheService_Hazelcast;

    public <T> boolean containsKey(long key, String cacheName) {
        IMap<Long, T> map = hazelcastInstance.getMap(cacheName);
        return map.containsKey(key);
    }
}

4. 动态注册监听器

在Spring容器初始化完成后,动态给Hazelcast的IMap注册监听器,确保所有Bean都已完成初始化:

@Component
public class CacheListenerRegistrar {

    private final HazelcastInstance hazelcastInstance;
    private final CacheListener cacheListener;

    public CacheListenerRegistrar(HazelcastInstance hazelcastInstance, CacheListener cacheListener) {
        this.hazelcastInstance = hazelcastInstance;
        this.cacheListener = cacheListener;
    }

    @PostConstruct
    public void registerListeners() {
        // 针对目标缓存(比如ActiveUser缓存)注册监听器
        IMap<String, String> activeUserMap = hazelcastInstance.getMap(CacheConstants.ACTIVE_USER_CACHE);
        // 第二个参数为true,表示事件中包含旧值
        activeUserMap.addEntryListener(cacheListener, true);
    }
}

原理说明

  • 分离Hazelcast配置与业务服务后,cacheManager不再依赖业务服务链,直接依赖独立的HazelcastInstanceBean,打破了循环依赖链
  • 监听器作为Spring Bean,由容器管理依赖注入,能够正常获取到依赖的业务服务和Repository
  • 动态注册监听器确保了Hazelcast实例和Spring业务Bean都初始化完成后再绑定,避免了初始化顺序冲突

内容的提问来源于stack exchange,提问作者Msh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 10:33:19