How to prevent creation of caches using default values with Spring and Hibernate

I played a lot with JCache connectors lately. To plug Ehcache 3 to different things.

I noticed one really dangerous thing. Frameworks tend to spontaneously create caches that were not explicitly defined. I think it is coming from the JCache spirit that there should be a default cache configuration. It is nonetheless a bad idea.

Let’s look at two examples

Spring

Caching in Spring is implemented by spring-cache. To plug JCache to spring-cache you use the JCacheCacheManager. By default when a cache isn’t available in the CacheManager, Spring calls JCacheCacheManager.getMissingCache. So far so good.

The default implementation for this method returns null when a cache doesn’t exist. This null will then be handled at higher levels to throw a nice exception.

If you want to explicitly support spontaneous cache creation, getMissingCache is where you should put your creation code.

However, watch out if you do that. You might lose track of all the existing caches. And please, never do the following.

@Override
protected Cache getMissingCache(String name) {
    Cache cache = super.getMissingCache(name);
    if(cache == null) {
        return new JCacheCache(cacheManager.createCache(name, new MutableConfiguration<>()));
    }
    return cache;
}

It returns a cache configured using default. It is never what you want. I will repeat that because you might think I don’t mean it. You might think defaults are cool. It is never what you want.

Then, as usual, Spring tries to be nice with us. So if you enable caching (@EnableCaching), that the JSR-107 API is in the classpath and that you do not expose any CacheManager, Spring will create one for you.

The JCacheCacheConfiguration will get a default JSR-107 CacheManager and add a list of caches taken from the cache property spring.cache.cache-names. These caches are by default created using a new MutableConfiguration<>(). As we said above, this is not a correctly configured cache.

The solution is to expose the wanted cache configuration in a bean.

@Bean
public javax.cache.configuration.Configuration<Object, Object> cacheConfiguration() {
    CacheConfiguration<Object, Object> cacheConfiguration = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder
            .newResourcePoolsBuilder().heap(100))
        .build();
    javax.cache.configuration.Configuration<Object, Object> configuration =
        Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfiguration);
    return configuration;
}

This bean will be magically used as cache default. You should always do this and never let `new MutableConfiguration<>() be used.

An alternative (preferred in fact, thanks Stéphane Nicoll for the tip), it to use a JCacheManagerCustomizer instead of the spring.cache.cache-names property. Like this:

@Bean
public JCacheManagerCustomizer cacheManagerCustomizer() {
    return cm -> {
        Configuration<Object, Object> cacheConfiguration = createCacheConfiguration();
        cm.createCache("vets", cacheConfiguration);
    };
}

private Configuration<Object, Object> createCacheConfiguration() {
    return Eh107Configuration.fromEhcacheCacheConfiguration(CacheConfigurationBuilder
        .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder
            .newResourcePoolsBuilder().heap(100)));
}

Hibernate

To use JCache with Hibernate we need to use the JCacheRegionFactory. The problem with JCacheRegionFactory is that by default if a cache is not found, it will spontaneously create a cache by passing new MutableConfiguration(). This means that instead of using a properly configured cache, you end up with some random default configuration (infinite on heap for Ehcache).

This is really bad because it is pretty hard to detect.

What I recommend in this case is, again, to override the default. In the latest Hibernate versions (5.2.8+) (thanks to HHH-1783, we can do the following

@Override
protected Cache<Object, Object> createCache(String regionName, Properties properties, CacheDataDescription metadata) {
    throw new IllegalArgumentException("Unknown hibernate cache: " + regionName);
}

In older versions, sadly, there is no straightforward cache creation method to override. The best we have is newDefaultConfig which provides the default configuration. One sad thing is that you don’t have the actual cache name here. You will need to debug to know it.

@Override
protected Configuration<Object, Object> newDefaultConfig(Properties properties, CacheDataDescription metadata) {
    throw new IllegalArgumentException("Unknown hibernate cache: " + metadata);
}

Again, an alternative solution would be to provide a meaningful default cache configuration in this method.

Conclusion

I do understand that frameworks do not like to fail with exceptions. This helps the feeling that they are working out of the box.

But I still think silently not caching or providing random default configuration is dangerous. Using my two workarounds should prevent a lot of headaches.