The Problem

In most cases with a Spring Boot app, we likely only ever have one cache provider. Spring Boot provides quite a few cache providers out of the box. But what happens if we want to use multiple providers in our application? We explore a solution to this problem in today’s post.

Note: This post assumes working knowledge of spring boot and spring caching

Setting Things Up

The first thing we do is setup Ehcache using JCache. To do this let’s add a few dependencies to the build file for spring caching, ehcache, and the java cache api:

implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.ehcache:ehcache:3.8.0")
implementation("javax.cache:cache-api")

Next, we let spring know where to find our jcache config in the application.yml:

spring:
  cache:
    jcache:
      config: classpath:ehcache.xml

Lastly, we enable caching and setup a small class that uses our configured cache:

@Configuration
@EnableCaching 
class CacheConfig

// Found in BookService.kt
@Cacheable(cacheNames = ["books"])
fun getBooks(author: String) = books.getOrDefault(author, emptyList())

Now that we’ve got ehcache configured, let’s set up redis. Like before, we start off by modifying the dependencies:

implementation("org.springframework.boot:spring-boot-starter-data-redis")

By including this starter, Spring Boot configures all of the necessary components in order to use redis. With our application in this state, if we bootRun and check out the auto-configuration report (http://localhost:8080/actuator/conditions) we see something like this:


{
  "RedisCacheConfiguration": {
    "notMatched": [
      {
        "condition": "OnBeanCondition",
        "message": "@ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) found beans of type 'org.springframework.cache.CacheManager' cacheManager"
      }
    ]
  }
}

Unfortunately, this means that Spring Boot won’t be autoconfiguring our RedisCacheManager bean for us since it already sees a Bean of type CacheManager from the Ehcache/JCache configuration.

The Solution

So how do we fix this? Well this is one of those cases where we need to take over the configuration from Spring Boot. To do this we create a more complex CacheConfig class that declares two CacheManager Beans:

@Bean
@Primary
override fun cacheManager(): CacheManager =
        RedisCacheManager.builder(redisConnectionFactory).build()

@Bean
fun jCacheCacheManager(): JCacheCacheManager {
    val cachingProvider = Caching.getCachingProvider()
    val configLocation = cacheProperties().resolveConfigLocation(cacheProperties().jcache.config)
    val cacheManager = cachingProvider.getCacheManager(configLocation.uri, classLoader)
    jCacheManagerCustomizers.orderedStream().forEach { it.customize(cacheManager) }
    return JCacheCacheManager(cacheManager)
}

Now that we have the two cache managers defined, let’s update the BookService to include a new cached method:

@Cacheable(cacheNames = ["books"], cacheManager = "jCacheCacheManager")
fun getBooks(author: String) = books.getOrDefault(author, emptyList())

@Cacheable(cacheNames = ["authors"], key = "'all-authors'")
fun getAuthors() = listOf(books.keys.toTypedArray())

Notice that getBooks specifies its CacheManager while getAuthors does not. This means that getBooks uses the jCacheCacheManager Bean while getAuthors uses the Primary CacheManager Bean which we defined to be a RedisCacheManager. We can see this in action in the tests. And there we have it, a working Spring Boot app with two cache providers!

Things to Look Out For

  • If we’re migrating from previously having only one provider to multiple providers, then we may have specified the application property spring.cache.type in order to force the usage of a specific cache type. When combining multiple providers, we want to avoid that.
  • We’re now owning the configuration of our CacheManager Beans, so this means some of the behavior might differ from the vanilla autoconfiguration. We especially want to be careful if we rely on Customizers because we now need to manually apply those.
  • We want to make sure that whichever provider we would like to have as the default is the Primary CacheManager Bean to save ourselves the effort of having to constantly specify the manager. This way various cached items need to opt into the secondary manager.

Check out the example code on github to see the complete example!

Thanks for reading!