6

After adding cache2k to my project some @SpringBootTest's stopped working with an error:

java.lang.IllegalStateException: Cache already created: 'cache'

Below I provide the minimal example to reproduce:

Go to start.spring.io and create a simplest Maven project with Cache starter, then add cache2k dependencies:

<properties>
    <java.version>1.8</java.version>
    <cache2k-version>1.2.2.Final</cache2k-version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.cache2k</groupId>
        <artifactId>cache2k-api</artifactId>
        <version>${cache2k-version}</version>
    </dependency>
    <dependency>
        <groupId>org.cache2k</groupId>
        <artifactId>cache2k-core</artifactId>
        <version>${cache2k-version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.cache2k</groupId>
        <artifactId>cache2k-spring</artifactId>
        <version>${cache2k-version}</version>
    </dependency>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Now configure the simplest cache:

@SpringBootApplication
@EnableCaching
public class CachingDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(CachingDemoApplication.class, args);
    }

    @Bean
    public CacheManager springCacheManager() {
        SpringCache2kCacheManager cacheManager = new SpringCache2kCacheManager();
        cacheManager.addCaches(b -> b.name("cache"));
        return cacheManager;
    }

}

And add any service (which we will @MockBean in one of our tests:

@Service
public class SomeService {
    public String getString() {
        System.out.println("Executing service method");
        return "foo";
    }
}

Now two @SpringBootTest tests are required to reproduce the issue:

@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringBootAppTest {

    @Test
    public void getString() {
        System.out.println("Empty test");
    }
}

@RunWith(SpringRunner.class)
@SpringBootTest
public class WithMockedBeanTest {

    @MockBean
    SomeService service;

    @Test
    public void contextLoads() {
    }
}

Notice that the 2nd test has mocked @MockBean. This causes an error (stacktrace below).

Caused by: java.lang.IllegalStateException: Cache already created: 'cache'
    at org.cache2k.core.CacheManagerImpl.newCache(CacheManagerImpl.java:174)
    at org.cache2k.core.InternalCache2kBuilder.buildAsIs(InternalCache2kBuilder.java:239)
    at org.cache2k.core.InternalCache2kBuilder.build(InternalCache2kBuilder.java:182)
    at org.cache2k.core.Cache2kCoreProviderImpl.createCache(Cache2kCoreProviderImpl.java:215)
    at org.cache2k.Cache2kBuilder.build(Cache2kBuilder.java:837)
    at org.cache2k.extra.spring.SpringCache2kCacheManager.buildAndWrap(SpringCache2kCacheManager.java:205)
    at org.cache2k.extra.spring.SpringCache2kCacheManager.lambda$addCache$2(SpringCache2kCacheManager.java:143)
    at java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1853)
    at org.cache2k.extra.spring.SpringCache2kCacheManager.addCache(SpringCache2kCacheManager.java:141)
    at org.cache2k.extra.spring.SpringCache2kCacheManager.addCaches(SpringCache2kCacheManager.java:132)
    at com.example.cachingdemo.CachingDemoApplication.springCacheManager(CachingDemoApplication.java:23)
    at com.example.cachingdemo.CachingDemoApplication$$EnhancerBySpringCGLIB$$2dce99ca.CGLIB$springCacheManager$0(<generated>)
    at com.example.cachingdemo.CachingDemoApplication$$EnhancerBySpringCGLIB$$2dce99ca$$FastClassBySpringCGLIB$$bbd240c0.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363)
    at com.example.cachingdemo.CachingDemoApplication$$EnhancerBySpringCGLIB$$2dce99ca.springCacheManager(<generated>)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
    ... 52 more

If you remove @MockBean, both tests will pass.

How can I avoid this error in my test suite?

Kirill
  • 6,762
  • 4
  • 51
  • 81

3 Answers3

4

Your second test represents a different ApplicationContext altogether so the test framework will initiate a dedicated one for it. If cache2k is stateful (for instance sharing the CacheManager for a given classloader if it already exists), the second context will attempt to create a new CacheManager while the first one is still active.

You either need to flag one of the test as dirty (see @DirtiesContext) which will close the context and shut down the CacheManager, or you can replace the cache infrastructure by an option that does not require all that, see @AutoConfigureCache.

If cache2k works in such a way that it requires you to dirty the context, I'd highly recommend to swap it using the later options.

Stephane Nicoll
  • 31,977
  • 9
  • 97
  • 89
  • Indeed the `org.cache2k.CacheManager` is shared between contexts and I do not see a way to force new instance, because it is obtained using a factory method returning the same `org.cache2k.CacheManager` for the given class loader. Annotating different tests with `@DirtiesContext` will slow down my test suite. Could you please add more info about alternative solution with `@AutoConfigureCache`? How is it supposed to solve the problem? Simply annotation test classes did not work (I guess because my Cache2kManager bean starts earlier) – Kirill Jul 25 '19 at 15:55
  • It doesn't work because you're not using auto-config. I'd move that setup in a profile and just enable cache2k when you run the app. – Stephane Nicoll Jul 26 '19 at 13:35
4

Since I do not want any custom behavior in test, but just want to get rid of this error, the solution is to create CacheManager using unique name like this:

@Bean
public CacheManager springCacheManager() {
    SpringCache2kCacheManager cacheManager = new SpringCache2kCacheManager("spring-" + hashCode());
    cacheManager.addCaches(b -> b.name("cache"));
    return cacheManager;
}
Kirill
  • 6,762
  • 4
  • 51
  • 81
3

I encountered the same error when using cache2k with Spring Dev Tools, and ended up with the following code as the solution:

    @Bean
    public CacheManager cacheManager() {
        SpringCache2kCacheManager cacheManager = new SpringCache2kCacheManager();
        // To avoid the "Caused by: java.lang.IllegalStateException: Cache already created:"
        // error when Spring DevTools is enabled and code reloaded
        if (cacheManager.getCacheNames().stream()
            .filter(name -> name.equals("cache"))
            .count() == 0) {
            cacheManager.addCaches(
                b -> b.name("cache")
            );
        }
        return cacheManager;
    }
ibic
  • 608
  • 1
  • 11
  • 16