In this article we will look into how to use Spring’s caching framework to add basic caching support to our application. Later we will extend it to work with Redis cache.

Table of Contents

Entire code for article can be found at – https://github.com/chatterjeesunit/spring-boot-demo-02-caching

1.0 Pre-requisites

  • You need a basic Spring boot application, already set up.
  • You should have some familiarity with Spring dependency Injection, Rest APIs, caching etc.
  • The second half of this document, will integrate to Redis cache. In case you are not familiar to Redis and want to learn about it, you can refer to Redis Cache tutorial.

2.0 Adding Dependencies

Now that you have your basic Spring boot application up and running, lets go and add dependencies for caching.

Open build.gradle file, and uncomment out following dependency

compile('org.springframework.boot:spring-boot-starter-cache')

Once you do this, your build.gradle file should look like this.

buildscript {
   ext {
      springBootVersion = '2.0.1.RELEASE'
   }
   repositories {
      mavenCentral()
   }
   dependencies {
      classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
   }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.dev'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
   mavenCentral()
}


dependencies {
// compile('org.springframework.boot:spring-boot-starter-aop')
   compile('org.springframework.boot:spring-boot-starter-data-jpa')
// compile('org.springframework.boot:spring-boot-starter-data-redis')
   compile('org.springframework.boot:spring-boot-starter-cache')
// compile('org.springframework.boot:spring-boot-starter-security')
   compile('org.springframework.boot:spring-boot-starter-web')
   runtime('mysql:mysql-connector-java')
   compileOnly('org.projectlombok:lombok')
   testCompile('org.springframework.boot:spring-boot-starter-test')
   testCompile('org.springframework.security:spring-security-test')
}

3.0 Spring Caching Support

Before we deep dive into how to add caching support, lets understand little about Spring’s caching support.

  • Spring provides a caching abstraction using which you can add caching transparently and easily to your Spring application.
  • Caching abstraction can be used on top of methods.
    • Basically before a method is executed, Spring framework will check if the method data is already cached
    • If yes, then it will fetch the data from the cache.
    • Else it will execute the method and cache the data
  • It also provides abstractions to update or remove data from the cache.

The cache abstraction does not provide an actual store and relies on abstraction materialized by the org.springframework.cache.Cache and org.springframework.cache.CacheManager interfaces.

If you have not defined a bean of type CacheManager or a CacheResolver named cacheResolver , then Spring Boot tries to detect the following providers (in the indicated order):

  1. Generic
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine
  9. Simple

3.1 Spring caching annotations

3.1.1 @Cacheable

This is used to identify the methods whose result should be put in the cache. e.g

@Cacheable("persons")
public Person getPerson(Long personId)

Above annotation will add the Person returned by the method into a cache name "persons". We can also add the result to multiple caches. e.g

@Cacheable("persons", "employees")
public Person getPerson(Long personId)

Above code will cache the person object in two different caches – "persons" and "employees".

3.1.1.1 Cache Keys

Now we know that caches are key-value type of stores. What is the cache key in above caching examples?

Out of the box, the caching abstraction uses a simple KeyGenerator based on the following algorithm:

  • If no params are given, return SimpleKey.EMPTY.
  • If only one param is given, return that instance.
  • If more the one param is given, return a SimpleKey containing all parameters.

So in below example the Long personId is the key for the cache.

@Cacheable("persons")
public Person getPerson(Long personId)

But in below example the Key is a SimpleKey of all three params – countryId, regionId, personId.

@Cacheable("persons")
public Person getPerson(long countryId, long regionId, long personId)

This approach works well for most use-cases – as long as parameters have natural keys and implement valid hashCode() and equals() methods.

We can also add a custom Key Generator by implementing the org.springframework.cache.interceptor.KeyGenerator interface.

@Cacheable annotation allows the user to specify how the key is generated through its key attribute. The developer can use SpEL to pick the arguments of interest (or their nested properties.

e.g In below example we specify the person’s emailAddress as the key for the cache

@Cacheable(value = "persons", key = "#person.emailAddress")
public Person getPerson(long country, Person person)

3.1.1.2 Conditional Caching

We can also do conditional caching using condition and unless parameter to the @Cacheable.

We can use following identifiers.

Identifier Description
#result Business entity returned by the method.
#root.method.name The method being invoked
#root.target The target object being invoked
#root.args Argument array. e.g. #root.args[0]
#root.caches Collection of caches against which the current method is executed. e.g. #root.caches[0].name
Argument names Name of any of the method arguments. e.g. #personId #country, etc

Lets see some examples of conditional caching

In below example the Person object will only be cached if returned value is present (support for java.util.Optional), and the boolean attribute isTerminated is true.

@Cacheable(value = "persons", key = "#person.id", unless = "#result?.isTerminated")
public Optional createPerson(Person person)

In below example Person object will be cached only if the second argument to method is true and the returned value is present. This way caller of the method can sometime decide to by-pass the cache and get the value directly from database.

@Cacheable(value = "persons", condition="#fetchFromCache", unless = "#result?.isTerminated")
public Optional getPerson(long personId, boolean fetchFromCache)

In below example the Product will be cached only if the price is less than 500 and returned result is not out of stock.

@Cacheable(value="products", key="#product.name", 
 condition="#product.price<500", unless="#result.outofstock")
public Product findProduct(Product product)

@Cacheable allows you to specify the sync attribute to ensure only a single thread is building the cache value.

In a multi-threaded environment, certain operations might be concurrently invoked for the same argument. By default, the cache abstraction does not lock anything and the same value may be computed several times, defeating the purpose of caching.

For those particular cases, the sync attribute can be used to instruct the underlying cache provider to lock the cache entry while the value is being computed. As a result, only one thread will be busy computing the value while the others are blocked until the entry is updated in the cache.

@Cacheable(cacheNames="configCache", sync=true)
public void loadAllConfiguration()

3.1.2 @CacheEvict

This is used to remove entries from the cache.

e.g Suppose we have an API that is used for importing person records. And we want that before invocation of this method, entire person cache should be cleared. We can do this in following way

@CacheEvict(value = "persons", allEntries = true, beforeInvocation = true)
public void importPersons()

By default @CacheEvict, runs after method invocation.

Consider another example, where we want to remove a person from cache after he is terminated.

@CacheEvict(value = "persons", key = "#person.emailAddress")
public void terminatePerson(Person person)

3.1.3 @CachePut

This is used to update a cache record. This does not interferes with method execution, but updates the cache with method result.

It support same options as @Cacheable

e.g Update the cache after a person record is updated.

For cases where the cache needs to be updated without interfering with the method execution, one can use the @CachePutannotation. That is, the method will always be executed and its result placed into the cache (according to the @CachePutoptions). It supports the same options as @Cacheable

3.1.4 @Caching

@Caching is used to group multiple caching annotations together.

Let’s look at some example of why it is required.

Suppose a person record is stored in two caches
– persons – a cache where key is the person’s Id and value is the Person record.
– personsByDept – a cache which contains all the persons for a particular department. Key is department Id and value is List of all persons.

Now when a person is terminated we need to evict the entries from both the caches.

If we do something like this, it will result in error

@CacheEvict(value = "persons", key = "#person.id")
@CacheEvict(value = "personsByDept", key = "#person.department.id")
public void terminatePerson(Person person)

So the right way to implement above requirement is by using @Caching annotation

@Caching( evict = {
    @CacheEvict(value = "persons", key = "#person.id")
    @CacheEvict(value = "personsByDept", key = "#person.department.id")
})    
public void terminatePerson(Person person)

3.1.5 @CacheConfig

@CacheConfig is a class-level annotation that allows to share the cache names, the custom KeyGenerator, the custom CacheManager and finally the custom CacheResolver. Placing this annotation on the class does not turn on any caching operation.
@Service
@CacheConfig(cacheNames = "persons")
public class PersonService {

    @Cacheable
    public Person getPerson(long personId) { 
      ....
      ....
    }

}

By default, caches are created as needed, but you can restrict the list of available caches by setting the cache-names property. For instance, if you want only cache1 and cache2 caches, set the cache-names property as follows, in your application.properties file:

spring.cache.cache-names=cache1,cache2

4.0 Implementing and Testing Caching in our sample Spring application

In this section we will add basic caching support to our existing Spring application.

4.1 Observing the behavior before caching

First of all we add a logger in the getPersonById() method of com.dev.springdemo.person.PersonService, so that we can observe what happens without caching.
Note : We have used System.out.println in our code examples for simplicity purpose. Ideally we should use some proper loggers in actual code instead of System.out.println.

public Optional getPersonById(Long personId) {
    System.out.println("Getting person record from the database.");
    return personRepository.findById(personId);
}

Now build and run the project, and call the get person Rest API, multiple times.

curl -i http://localhost:8080/person/37

After every call to the GET API we can see following line being printed in the application console – “Getting person record from the database”. This means the GET API service call is executed each time and person data is being fetched from the database.

4.2 Configuring Cache Support

Since we haven’t added any cache library, so Spring will be using a ConcurrentHashMap as the cache store. This is the default if no caching library is present in your application.

Create a cache configuration class, in your base package and add annotations – @Configuration as @EnableCaching as

@Configuration
@EnableCaching
public class CacheConfig {
}

@Configuration tells Spring that this is a configuration file.

@EnableCaching enables the Spring caching support.

4.3 Adding caching to Get and Update APIs

Add following annotation on PersonService class. This will ensure all cacheable APIs of this class will use a cache named “person” (unless overriden at methodO

@CacheConfig(cacheNames = "person")

Also add following annotation to the getPersonById() method. It will cache the results only if a result is present with a emailAddress that is not null.

@Cacheable(unless = "#result?.emailAddress == null")

Also add a cache update annotation to the update() and create() method, to update the cached on create and update

@CachePut(key = "#person.id")

PersonService class will now look something like this

@Service
@CacheConfig(cacheNames = "person")
public class PersonService {

    //Autowiring the repository
    @Autowired
    private PersonRepository personRepository;

    //Get person by Id
    @Cacheable(unless = "#result?.emailAddress == null")
    public Optional getPersonById(Long personId) {
        System.out.println("Getting person record from the database.");
        return personRepository.findById(personId);
    }

    //Create the person
    @CachePut(key = "#person.id")
    public Person create(Person person) {
        return personRepository.save(person);
    }

    //Update the person
    @CachePut(key = "#person.id")
    public Person update(Person person) {
        return personRepository.save(person);
    }

   ...
   ... 
   ...
}

Now execute the same GET API multiple times again. You will notice that the log –  “Getting person record from the database”, is printed in the application console only for the first time and not after that.

curl -i http://localhost:8080/person/37

Now let’s call the update API and update the same person record.
(We have changed the email of the person and removed 1 out of 2 addresses for the person)

curl -i -X PUT -H "Content-Type: application/json" http://localhost:8080/person/37 -d '{"id":37,ame":"Chatterjee","emailAddress":"sonal@gmail.com","addresses":[{"id":38,"streetAddress":"2400 Bridge Parkway","city":"Redwood Shores","stateCode":"CA","country":"USA","zipCode":"94065","personId":37}]}'

Once the update call is done, the cache will automatically be update due to @CachePut annotation on the method.

After the update call succeeds, call the Get API again.

You will notice that the log Getting person record from the database”, is NOT printed on the application console, but still the API returns the correct data automatically. (since cache got automatically updated).

4.4 Adding cache eviction support

Before we add cache eviction, lets add a new API first to our PersonService API – to delete a person record.

//Deletes a person record.
public void delete(long personId) {
    personRepository.deleteById(personId);
}

Add Delete Rest API to the PersonController class

@DeleteMapping(path = "/{id}")
public ResponseEntity deletePerson(@PathVariable("id") String personId) {
    try {
        personService.delete(Long.valueOf(personId));
        return getSuccessJsonMessage();
    }catch(Exception ex) {
        return handleException(ex);
    }
}


private ResponseEntity getSuccessJsonMessage() {
    return ResponseEntity.ok("{ \"success\" : true}");
}

First lets observe the default behavior without @CacheEvict annotation.

Call the GET API for person with id 40, multiple times, and confirm that the data is indeed coming from the cache.

curl -i http://localhost:8080/person/40

Now call the DELETE API

curl -i -X DELETE http://localhost:8080/person/40

Verify that the data is actually deleted from the database. And then call the GET API back again.

curl -i http://localhost:8080/person/40

You will notice that the API is still returning the person record, although it is not present in the database. This happens because the person record was in the cache and it is returned from cache.

This type of situation can be really scary in production. Hence we have to be very careful when we implement caching in our application.

All updates/deletes need to make sure that they either update the cache or evict the data from the cache

To correct the above situation, just add a @CacheEvict annotation to the delete API

//Deletes a person record.
@CacheEvict
public void delete(long personId) {
    personRepository.deleteById(personId);
}

Now try the same scenario again. You will notice that now you will get below error message if you call the GET API after DELETE API.

{ 
  "statusCode":400,
  "errorMessage":"Unable to fetch person record with id = 42"
}

5.0 Redis Cache Integration

Now that we have seen how to add caching support to our application using Spring integration, lets integrate Redis cache into our application.

If you want to know more about Redis Cache or Jedis/Lettuce (its Java clients), then please go through these Redis tutorials

Since we have used Spring Cache Abstraction, so we won’t have to change anything in our APIs to integrate Redis Cache. We just have to add the configurations related to Redis.

Spring Redis integrates with Jedis and Lettuce, two popular open source Java libraries for Redis.

5.1 Build Dependency

Modify build.gradle to add required dependencies.

Spring Data Redis : Uncomment out following dependency

compile('org.springframework.boot:spring-boot-starter-data-redis')

5.2 Configurations

Modify application.properties to add following properties

#Redis Configuration
#Refer to https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

#1 hour TTL
spring.cache.redis.time-to-live=60000ms
# Redis server host.
spring.redis.host=localhost
# Redis server port.
spring.redis.port=6379
# Whether to enable SSL support.
spring.redis.ssl=false
# Connection timeout - 2 minutes
spring.redis.timeout=120000

# Maximum number of connections that can be allocated by the pool at a given time. Use a negative value for no limit.
spring.redis.jedis.pool.max-active=1
# Maximum number of "idle" connections in the pool. Use a negative value to indicate an unlimited number of idle connections.
spring.redis.jedis.pool.max-idle=25
# Maximum amount of time a connection allocation should block before throwing an exception when the pool is exhausted. Use a negative value to block indefinitely.
spring.redis.jedis.pool.max-wait=30000ms
# Target for the minimum number of idle connections to maintain in the pool. This setting only has an effect if it is positive.
spring.redis.jedis.pool.min-idle=5

Modify CacheConfig class and add @EnableAutoConfiguration, annotation to configure the Redis auto configuration from application.properties file

@Configuration
@EnableCaching
@EnableAutoConfiguration
public class CacheConfig {

}

5.3 Changes to Entity Classes

Make both Person and Address class Serializable

public class Address implements Serializable {
 ....
}
public class Person implements Serializable {
 ....
}

5.4 Test Caching with Redis

This is all that is required to change the caching implementation to work with Redis cache.

Execute the GET and Update and Delete APIs, and you can see that the data is now going into Redis cache.

6.0 Configuring Jedis for Redis Cache

6.1 Add Dependencies

Apache Common Pools :  Add following to build.gradle

compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.5.0'

If you do not add the above Apache Common Pools dependency, then you will get following error at application startup

Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.redis.connection.jedis.JedisConnectionFactory]: Factory method 'redisConnectionFactory' threw exception; nested exception is java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig

Jedis Client : Add following dependency to build.gradle

compile group: 'redis.clients', name: 'jedis', version: '2.9.0'

If you do not add above dependency, you will get following error on application startup

Error creating bean with name 'redisConnectionFactory' defined in class path resource [com/dev/springdemo/CacheConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.redis.connection.jedis.JedisConnectionFactory]: Factory method 'redisConnectionFactory' threw exception; nested exception is java.lang.NoClassDefFoundError: redis/clients/jedis/JedisPoolConfig

6.2 Cache Configuration

Remove @EnableAutoConfiguration annotation from CacheConfig class.

Autowire RedisProperties into CacheConfig class. This will enable loading all the Redis properties from application.properties (properties that start with “spring.redis”).

@Autowired
private RedisProperties redisProps;

Add JedisConnectionFactory bean definition to CacheConfig class

//Create a new Bean definition for JedisConnectionFactory
@Bean
public JedisConnectionFactory redisConnectionFactory() {

    //Create the Builder for JedisClientConfiguration
    JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration
            .builder()
            .connectTimeout(redisProps.getTimeout())
            .readTimeout(redisProps.getJedis().getPool().getMaxWait());

    if(redisProps.isSsl()) builder.useSsl();

    //Final JedisClientConfiguration
    JedisClientConfiguration clientConfig = builder.usePooling().build();

    //TODO: Later: Add configurations for connection pool sizing.

    //Create RedisStandAloneConfiguration
    RedisStandaloneConfiguration redisConfig =
            new RedisStandaloneConfiguration(redisProps.getHost(), redisProps.getPort());

    //Create JedisConnectionFactory
    JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisConfig, clientConfig);
    return jedisConnectionFactory;
}

Add RedisTemplate bean definition to CacheConfig class

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate template = new RedisTemplate();
    template.setConnectionFactory(connectionFactory);
    return template;
}

The conversion between the user (custom) types and raw data (and vice-versa) is handled in Spring Data Redis in the org.springframework.data.redis.serializer package.

Multiple implementations are available out of the box, two of which have been already mentioned before in this documentation:

  • JdkSerializationRedisSerializer which is used by default for RedisCache and RedisTemplate.
  • the StringRedisSerializer.

The serialization mechanism can be easily changed on the template, and the Redis module offers several implementations available in the org.springframework.data.redis.serializer package.

We can override the serialization by setting it in the RedisTemplate.

In this example we are going with default serialization.

6.3 Build and Test

Jedis manual configuration is now complete and you can build and test the above code.

All caching is currently happening as Strings in Redis.

6.4 Caching as Redis Hash instead of String

Coming soon….

 

 


In this article we look at Spring caching via annotations, and also looked at how to add caching to a Spring application, and how to integrate Redis cache.


References

https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html

https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/integration.html#cache

http://caseyscarborough.com/blog/2014/12/18/caching-data-in-spring-using-redis/