Search code examples
javaspringhibernateehcachejcache

How to programmatically configure L2 Hibernate caches with ECache?


I'm trying to configure L2+QueryCache for Hibernate 6.1.7 in my Spring 6 app running Ehcache 3.10.x

In my app, Spring-level caches with method-level @Cacheable, @CachePut, @CacheEvict, etc. work fine. I create the caches and they work as expected.

L2 cache also works correctly, but only if I don't create any L2 caches and let Spring create the default ones (missing_cache_strategy: create). This is presentig heap-related problems in production because the default cache settings in Ehcache are allowing infinite heap usage.

How do I create L2 caches with my own configs, from code? I want to avoid the xml Ehcache config at this time, and Ehcache seems to be moving away from it anyway.

This is how I configure Spring-level caches, and it works:

    @Configuration
    @EnableCaching
    public class MyCacheConfig {
      @Bean
      public CacheManager ehCacheManager() {
          CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();

         //Create a Spring-level cache
         var config = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(SomeMethodArg.class, SomeMethodReturnType.class)
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(30))
                .build();
          cacheManager.createCache("my_spr_cache", Eh107Configuration.fromEhcacheCacheConfiguration(config));

          return cacheManager;
      }
    }

Following the Ehcache docs and other sources, I was able to create L2 caches in a similar manner by adding another cache to the bean above:

//Create a L2 cache
var config = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(CacheKeyImplementation.class, AbstractReadWriteAccess.Item.class)
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(30))
                .build();
        
//L2 cache name should be the fully qualified name of the Entity
cacheManager.createCache("com.mydomain.model.MyClass", Eh107Configuration.fromEhcacheCacheConfiguration(config));

And of course, the MyClass Entity is annotated with:

@jakarta.persistence.Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) //Note that some other classes instead use NONSTRICT_READ_WRITE 

Now the cache I made is correctly created at runtime and Ehcache doesn't build a default one. I can confirm this from the startup logs - manually created caches are logged significantly earlier on.

Now the issue: this method is unreliable. Firstly, it seems that changing the ConcurrencyStrategy breaks the cache creation code, for example, switching to NONSTRICT_READ_WRITE on the entity will cause Exceptions:

Exception in thread "(3/16)" java.lang.ClassCastException: Invalid value type, 
expected:  org.hibernate.cache.spi.support.AbstractReadWriteAccess$Item 
but was :  org.hibernate.cache.spi.entry.StandardCacheEntryImpl

The first attempt I made was obviously following the exception literally and changing the value class to StandardCacheEntryImpl. This just causes even more erratic behavior, with the cache behaving fine at low usage but failing when consistent load is applied:

Caused by: java.lang.ClassCastException: Invalid value type, 
expected : org.hibernate.cache.spi.support.AbstractReadWriteAccess$Item 
but was : org.hibernate.cache.spi.support.AbstractReadWriteAccess$SoftLockImpl

If I switch those two classes back and forth, I get the same exception but reversed.

This leads me to believe that I'm using the wrong approach entirely. I can't find any modern and updated question on this, could someone kindly point me in the right direction?


If it can be useful, this is the relevant config in my application.yml:

# [More settings here...]
  jpa:
    # [More settings here...]
    properties:
      jakarta:
        persistence:
          sharedCache:
            #required - enable selective caching mode - only entities with @Cacheable annotation will use L2 cache.
            mode: ENABLE_SELECTIVE
      hibernate:
        cache:
          use_second_level_cache: true
          use_query_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
        jakarta:
          cache:
            missing_cache_strategy: create

Solution

  • For cache keys Hibernate may potentially use three different implementations (please check org.hibernate.cache.spi.CacheKeysFactory interface and it's createCollectionKey, createEntityKey and createNaturalIdKey methods), moreover, Hibernate, allows to override default CacheKeysFactory via hibernate.cache.keys_factory setting, so, technically you have no chance to guess what cache key implementation will be used for particular cache region, moreover cache regions may contain different data, so, Object.class is a valid definition of cache key implementation.

    In case of cache values implementations may also differ:

    • for natural id cache Hibernate stores entity identifiers there
    • for other caches that can be either Map or StandardCacheEntryImpl or implementation of org.hibernate.cache.spi.support.AbstractReadWriteAccess.Lockable

    so, for cache values Object.class is the only valid definition as well.