Spring Boot 2.7.0 has updated the dependency of Ehcache to 3.10.0. Unfortunately, this version causes an IllegalArgumentException at org.springframework.cache.interceptor.AbstractCacheResolver.resolveCaches. With 3.9.9 there are no problems. Spring Boot 2.6 with Ehcache 3.10.0 does not work either. I don`t think this is a Spring Boot or Framework problem, but a problem at Ehcache. However, since this version does not work, Spring Boot should consider downgrading to 3.9.9.

java.lang.IllegalArgumentException: Cannot find cache named 'myKey' for Builder[public java.lang.String com.example.demo.ServiceWithCaching.cacheableAction(java.lang.String)] caches=[myKey] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
    at org.springframework.cache.interceptor.AbstractCacheResolver.resolveCaches(AbstractCacheResolver.java:92)
    at org.springframework.cache.interceptor.CacheAspectSupport.getCaches(CacheAspectSupport.java:252)
    at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContext.<init>(CacheAspectSupport.java:724)
    at org.springframework.cache.interceptor.CacheAspectSupport.getOperationContext(CacheAspectSupport.java:265)
    at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContexts.<init>(CacheAspectSupport.java:615)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:345)
    at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:64)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
    at com.example.demo.ServiceWithCaching$$EnhancerBySpringCGLIB$$cff0191c.cacheableAction(<generated>)
    at com.example.demo.ServiceWithCachingTests.cacheableAction(ServiceWithCachingTests.java:16)
...

A sample project to reproduce this problem is attached. demo.zip

Comment From: pruidong

I followed the code and found that this problem belongs to the Spring Framework. The difference between the two versions is that 3.10.0 uses the instance of AbstractCacheManager.java to get the cache, but the judgment (missingCache != null) is added when getting the cache, As a result, the cache instance cannot be obtained. In 3.9.9, the instance of ConcurrentMapCacheManager.java is used to obtain the cache, and this method can successfully obtain the cache.

I don't understand the reason for this change, but I think you can file an issue to Spring Framework.

Ehcache 3.10.0:

AbstractCacheResolver.java-> cacheManager(property)-> JCacheCacheManager.java->AbstractCacheManager.java::getCache()

// AbstractCacheManager.java
//
public Cache getCache(String name) {
        // Quick check for existing cache...
        Cache cache = this.cacheMap.get(name);
        if (cache != null) {
            return cache;
        }
                // note: Ehcache 3.10.0, where the missingCache is empty, so the Ehcache instance cannot be obtained.

        // The provider may support on-demand cache creation...
        Cache missingCache = getMissingCache(name);
        if (missingCache != null) {
            // Fully synchronize now for missing cache registration
            synchronized (this.cacheMap) {
                cache = this.cacheMap.get(name);
                if (cache == null) {
                    cache = decorateCache(missingCache);
                    this.cacheMap.put(name, cache);
                    updateCacheNames(name);
                }
            }
        }
        return cache;
    }

Ehcache 3.9.9:

AbstractCacheResolver.java-> cacheManager(property)-> ConcurrentMapCacheManager.java::getCache()

// ConcurrentMapCacheManager.java
//
public Cache getCache(String name) {
    Cache cache = this.cacheMap.get(name);
    if (cache == null && this.dynamic) {
        synchronized (this.cacheMap) {
            cache = this.cacheMap.get(name);
            if (cache == null) {
                cache = createConcurrentMapCache(name);
                this.cacheMap.put(name, cache);
            }
        }
    }
    return cache;
}

Comment From: snicoll

I don`t think this is a Spring Boot or Framework problem, but a problem at Ehcache. However, since this version does not work, Spring Boot should consider downgrading to 3.9.9.

Thanks for the sample. There is no configuration whatsoever in that sample so you rely on the fact that the underlying cache provider creates a cache on-the-fly if it does not exist. I've added the following to your sample and it works as expected:

spring.cache.cache-names=myKey

The cache abstraction relies on the underlying implementation. If they decide not to create a default cache for you, this exception is to be expected. However, I didn't see a particular issue in the 3.10.0 changelog about that change so perhaps worth reaching out to them about it?

Comment From: sephiroth-j

@pruidong , thanks for your analysis.

I filled an issue at Ehcache to clarify if this is an intended change.

Comment From: sephiroth-j

@snicoll , um, would be a valid workaround if Spring Boot didn't try to create caches that already exist or at least handle this situation correctly. (Happens with multiple unit tests) 😢

java.lang.IllegalStateException: Failed to load ApplicationContext
...
Caused by: javax.cache.CacheException: A Cache named [myKey] already exists
    at org.ehcache.jsr107.Eh107CacheManager.createCache(Eh107CacheManager.java:207) ~[ehcache-3.10.0.jar:3.10.0]
    at org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration.jCacheCacheManager(JCacheCacheConfiguration.java:87) ~[spring-boot-autoconfigure-2.7.0.jar:2.7.0]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.3.20.jar:5.3.20]
    ... 108 common frames omitted

see also https://www.javadoc.io/doc/javax.cache/cache-api/latest/javax/cache/CacheManager.html#createCache-java.lang.String-C-

CacheException - if there was an error configuring the Cache, which includes trying to create a cache that already exists.

Comment From: snicoll

um, would be a valid workaround if Spring Boot didn't try to create caches that already exist or at least handle this situation correctly. (Happens with multiple unit tests)

If we try to create a cache again, that's because the underlying cache provider told us it didn't exist. I assume this also works fine with EhCache 3.9? Given that zero code has been changed on our side, perhaps worth considering that something in the provider has changed?

If not, then that's an entire different problem and shouldn't be discussed in this issue. You may want to dirties your context has EhCache might keep the underlying cache manager around (i.e. outside the test application context).

Comment From: sephiroth-j

You are right, its working fine with Ehcache 3.9. The exception happens during the initialization process when no CacheManager exists. No further checks are performed about the existence of a cache afterwards. It is a fair assumption to create the caches unconditionally when there is no CacheManager. But due to concurrency, it is possible that at time t0 a thread-0 detects that no CacheManager exists, at t1 another thread-1 has created the cache manager and the cache, so that at t2 the creation of the same cache by thread-0 will fail because it already exists now.

https://github.com/spring-projects/spring-boot/blob/47516b50c39bd6ea924a1f6720ce6d4a71088651/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java#L83-L90

I will create a new issue for that once I have a demo project to reproduce the issue.