The automatic configuration of a cache is amazing for the simple cases, however quickly becomes weird if you decide to use multiple CacheManager
instances.
For example you might want to have a Distributed Cache using Redis, and a Local Caffeine or ConcurrentMap Cache for small items, you'd need the following configuration.
@Configuration
@EnableCaching
public CachingConfig implements CachingConfigurer, ApplicationContextAware {
private ApplicationContext ac;
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory factory, CacheProperties props) {
// Implementing Redis Cache
}
@Bean
public CacheManager caffeineCache(CacheProperties cacheProperties) {
// Implementing Caffeine Cache
}
@Override
@Bean
public CacheResolver cacheResolver() {
var redisCache = ac.getBean("redisCacheManager", CacheManager.class);
var caffeineCache = ac.getBean("caffeineCache", CacheManager.class);
// Implement Resolver
}
@Override
public KeyGenerator keyGenerator() {
return new SimpleKeyGenerator();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.ac= applicationContext;
}
}
From a user perspective there are some parts that are a little verbose compared to other configurations. Highlights would be bringing in the ApplicationContext
, but the class really doesn't care for it, implementing the KeyGenerator
using the same default Spring does (per CachingConfigurer Docs), and then not overriding CachingConfigurer::cacheManager()
for a mixture of reasons.
I personally I think Caching could use a more "Default unless Declared" approach, where we could reduce this down to the following.
@Configuration
@EnableCaching
public CachingConfig {
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory factory, CacheProperties props) {
// Implementing Redis Cache
}
@Bean
public CacheManager caffeineCache(CacheProperties cacheProperties) {
// Implementing Caffeine Cache
}
@Bean
public CacheResolver cacheResolver(
@Qualifier("redisCacheManager") CacheManager redis,
@Qualifier("caffeineCache") CacheManager caffeine
) {
// Implement Resolver
}
}
This removes the KeyGenerator
, swaps ApplicationContext::getBean
with Parameter Injection, and removes the CachingConfigurer
in favor of global bean detection. What other adjustments would the Spring Boot Team be open to?
Comment From: snicoll
The automatic configuration of a cache is amazing for the simple cases, however quickly becomes weird if you decide to use multiple CacheManager instances.
It's a common topic in Spring Boot where users enjoy the automated behavior for configuring the infrastructure but it doesn't expand to multiple instances of the same type, see https://github.com/spring-projects/spring-boot/issues/15732.
From a user perspective there are some parts that are a little verbose compared to other configurations. Highlights would be bringing in the ApplicationContext, but the class really doesn't care for it, implementing the KeyGenerator using the same default Spring does (per CachingConfigurer Docs), and then not overriding CachingConfigurer::cacheManager() for a mixture of reasons.
There's a mix of things here. First of all, you don't have to override keyGenerator
. Then a CacheResolver
is all that's required to give a cache based on a name. From the pure perspective of the cache infrastructure, a cache resolver is all that's required. You can override cacheManager if you want but it will be superseded (per javadoc).
The problem with that stuff is that the infrastructure has to configure early, to be able to wrap things correctly. In your example above, the two cache managers don't need to be technically bean (albeit declaring them as such makes sure that callbacks are honored, typically on shutdown). Having any infrastructure declaring such types is a call for early initialization I am afraid.
I can see how the contract of the contributor makes it harder than it should.
I wonder if the following would work:
@Configuration
class CacheManagersConfig {
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory factory, CacheProperties props) {
// Implementing Redis Cache
}
@Bean
public CacheManager caffeineCache(CacheProperties cacheProperties) {
// Implementing Caffeine Cache
}
}
@Configuration
@Import(CacheManagersConfig.class)
@EnableCaching
class CachingConfig implements CachingConfigurer {
private final ObjectProvider<CacheManager> cacheManagers;
CachingConfig(ObjectProvider<CacheManager> cacheManagers) { ... }
@Override
@Bean
public CacheResolver cacheResolver() {
// Implement Resolver
}
}
(note I've used ObjectProvider
to be concise but you can do the qualifier injection as well).
Comment From: Crain-32
First of all, you don't have to override keyGenerator
The @EnableCaching
Javadoc could use a minor update then.
@EnableCaching
will configure Spring's SimpleKeyGenerator for this purpose, but when implementing CachingConfigurer, a key generator must be provided explicitly.
Threw me off, even though the next line does state
Return
null
or new SimpleKeyGenerator() from this method if no customization is necessary
Returning null
feels incorrect when stating a key generator must be provided explicitly. Maybe the function can be changed to the following?
public interface CachingConfigurer {
@Nullable
default KeyGenerator keyGenerator() { return new SimpleKeyGenerator(); }
}
Although now that conflicts with the @Bean
Requirement.
Going back to the CacheResolver
and some thoughts on it,
From the pure perspective of the cache infrastructure, a cache resolver is all that's required
This is reminding me a bit of the following Spring Security Issue. Although a CacheResolver
is all that is required, the current documentation points towards using CacheManager
, and a CacheManager
is really a CacheResolver
that only uses the Operation's Cache Name to resolve, along with a helper function.
That approach makes sense with this section's note in Spring Boot Docs,
Similarly to key and keyGenerator, the cacheManager and cacheResolver parameters are mutually exclusive.
Maybe this adjust should be made instead?
public interface CacheManager {
Collection<? extends Cache> resolveCaches(CacheOperationInvovationContext<?> context);
Collection<String> getCacheNames();
}
Then using the ConcurrentMapCacheManager
as an example.
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvovationContext<?> context) {
Collection<String> cacheNames = context.getOperation().getCacheNames();
// Unsure if the empty collection check should be in here or the caller
return cacheNames.stream().map(this::getCache).filter(Objects::nonNull).toList();
}
@Nullable
private Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
cache = this.cacheMap.computeIfAbsent(name, this::createConcurrentMapCache);
}
return cache;
}
}
This feels easier to work with, since there is a single contract to work with. Power users could still used named CacheManager
instances as well for special handling.
Comment From: snicoll
Can we focus on the issue as reported please? I'll have a look to the Javadoc and see what needs to be updated but I think I've provided a fairly good route for what you claim is a problem. Please give that a try and report if that works for you.
Comment From: Crain-32
Sorry about that. To make things worse I also realized while writing the reply this was Spring Framework's Repository, not Spring Boot's 🤦🏻♂️. Since the behavior falls into the Cache Auto Configuration, not the Cache Configuration, I'm going to review the CacheAutoConfiguration
some more and move the CacheResolver
bean only working in a CachingConfigurer
implementation to Boot, as that likely stems from there. I'll pop back up in with another issue once I feel like I really understand the CacheResolver
vs CacheManager
Your solution does work, thank you.
Comment From: snicoll
Javadoc has been polished in #33288. The requirement mismatch for KeyGenerator
is because that part of the doc was written before we could use default methods in interfaces.