0

I'm working on a Spring Boot 2.4.1 project using spring-boot-data-jpa and hazelcast. I'm trying to set up a distributed map with a read through to a database. I've implemented com.hazelcast.map.MapLoader, but when I try to run the application it fails to start because of a circular dependency. It seems the JpaRepository needs the HazelcastInstance to be available first, but the HazelcastInstance needs the MapLoader which in turn needs the JpaRepository to be ready. At least that's what it looks like from the logs and this post.

Does anybody know how to fix this issue?

pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.1</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

...

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast-all</artifactId>
    <version>4.1.1</version>
</dependency>

Config:

@Configuration
public class HZConfig {
    
    @Bean
    Config config(RVOLoader rvoLoader) {}
        Config config = new Config();
        config.getMapConfig("rvoMap").getMapStoreConfig().setImplementation(rvoLoader);
        config.getMapConfig("rvoMap").getMapStoreConfig()
            .setInitialLoadMode(MapStoreConfig.InitialLoadMode.EAGER);

        return config;
    }

    @Bean
    HazelcastInstance hazelcastInstance(Config config) {
        HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
        return hz;
    }
}

MapLoader:

@Component
public class MyResourceMapLoader implements MapLoader<Long, MyResource> {
    
    private final MyResourceRepository repo;

    public MyResourceMapLoader(MyResourceRepository repo) {
        this.repo = repo;
    }

    @Override
    public MyResource load(Long key) {
        return this.repo.findById(key).orElse(null);
    }

    @Override
    public Map<Long, MyResource> loadAll(Collection<Long> keys) {
        Map<Long, MyResource> myResourceMap = new HashMap<>();
        for (Long key : keys) {
            MyResource myResource = this.load(key);
            if (myResource != null) {
                myResourceMap.put(key, myResource);
            }
        }
        return myResourceMap;
    }

    @Override
    public Iterable<Long> loadAllKeys() {
        return this.repo.findAllIds();
    }
}   

JpaRepository:

@Repository
public interface MyResourceRepository extends JpaRepository<MyResource, Long> {

    List<MyResource> findAll();

    @Query("SELECT m.id from MyResource m")
    Iterable<Long> findAllIds();

}
CeeTee
  • 778
  • 1
  • 9
  • 17

1 Answers1

2

One solution,

@Bean
public Config config(YourMapStoreFactory yourMapStoreFactory) {
    ...
    config.getMapConfig("rvoMap").getMapStoreConfig().setFactoryImplementation(yourMapStoreFactory);

Make the Map Store Factory a component, not the Map Store

@Component
public class YourMapStoreFactory implements MapStoreFactory {

    @Autowired
    private MyResourceRepository repo;

    public MapLoader newMapStore(String mapName, Properties properties) {
        if (mapName.equals("rvoMap") {
            return new MyResourceMapLoader(repo);
Neil Stevenson
  • 3,060
  • 9
  • 11
  • I'm afraid this just moves the circular dependency issue from the MapLoader implementation to the MapStoreFactory implementation – CeeTee Jan 16 '21 at 20:38
  • If you still get a circular dependency, can you post the Spring log that shows the circle ? – Neil Stevenson Jan 17 '21 at 14:42
  • Here is the log: The dependencies of some of the beans in the application context form a cycle: – CeeTee Jan 20 '21 at 15:58
  • ┌─────┐ | hazelcastInstance defined in class path resource [com/dev/app/imdg/IMDGConfig.class] ↑ ↓ | config defined in class path resource [com/dev/app/imdg/IMDGConfig.class] ↑ ↓ – CeeTee Jan 20 '21 at 15:59
  • | MyResourceMapStoreFactory (field com.dev.app.repo.MyResourceJpaRepository com.dev.app.imdg.MyResourceMapStoreFactory.myResourceJpaRepository) ↑ ↓ | myResourceJpaRepository defined in com.dev.app.repo.MyResourceJpaRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration ↑ ↓ | (inner bean)#91b13742 └─────┘ – CeeTee Jan 20 '21 at 15:59
  • Could the versions of Spring be different in our projects? Maybe something has changed regarding the order of initiation and JpaRepos now expect the hazelcast instance to be available if it is on the classpath? – CeeTee Jan 20 '21 at 16:01
  • Try removing your `HazelcastInstance` bean definition. If there is a `Config` bean, then Spring Boot will build the Hazelcast instance for you. See [here](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastInstanceFactory.java#L87). For me, that's enough to stop the circle. – Neil Stevenson Jan 21 '21 at 11:40
  • Using the Spring auto-configured HazelcastInstance bean moves the circular dependency into the auto-configured bean: ┌─────┐ | hazelcastInstance defined in class path resource [org/springframework/boot/autoconfigure/hazelcast/HazelcastServerConfiguration$HazelcastServerConfigConfiguration.class]. I've put in a temporary fix by lazily setting the Repository using ApplicationContextAwage interface – CeeTee Jan 22 '21 at 14:04
  • It's fixable, it's just a Spring dependency circle. You'd need to post the current versions of the classes listed in the circle. Probably just your `MyResourceMapLoader`, `HZConfig` and `IMDGConfig` ones – Neil Stevenson Jan 25 '21 at 08:35