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.