Utilizing cache abstraction with Spring boot and Ehcache

Caching is a very common operation when developing applications. Spring made a neat abstraction layer on top of the different caching providers (Ehcache, Caffeine, Guava, GemFire, …). In this article I will demonstrate how the cache abstraction works using Ehcache as the actual cache implementation.

spring-boot-ehcache

Setting up the project

In this example I will be creating a simple REST service. So let’s start by opening the Spring Initialzr. In this example I’ll create a simple REST API, so let’s add Web and Cache as dependencies.

spring-initializr-cache

Now that we’ve done that, it’s time to create a simple REST API.

Creating a dummy REST API

The first step is to create a DTO. I’m going to create a simple task REST API, so my DTO will look like this:

public class TaskDTO {
    private Long id;
    private String task;
    private boolean completed;

    public TaskDTO(Long id, String task, boolean completed) {
        this.id = id;
        this.task = task;
        this.completed = completed;
    }

    public TaskDTO(String task, boolean completed) {
        this(null, task, completed);
    }

    public TaskDTO() {
    }

    public Long getId() {
        return id;
    }

    public String getTask() {
        return task;
    }
    public void setTask(String task) {
        this.task = task;
    }

    public boolean isCompleted() {
        return completed;
    }
    public void setCompleted(boolean completed) {
        this.completed = completed;
    }
}

Now the next step is to create a service, so this is what I did in my dummy service:

@Service
public class TaskServiceImpl {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    public List<TaskDTO> findAll() {
        logger.info("Retrieving tasks");
        return Arrays.asList(new TaskDTO(1L, "My first task", true), new TaskDTO(2L, "My second task", false));
    }
}

The logging is added just for demonstration, because it will allow us to see if this method is actually called, or a cached version of the result is retrieved.

Finally, we have to add a controller:

@RestController
@RequestMapping("/api/tasks")
public class TaskController {
    @Autowired
    private TaskServiceImpl service;

    @RequestMapping(method = RequestMethod.GET)
    public List<TaskDTO> findAll() {
        return service.findAll();
    }
}

Nothing too fancy here. Run the application and test it out by going to http://localhost:8080/api/tasks, which should show you the dummy tasks.

postman-api-result

Configuring the cache

Now, before we start configuring anything, we have to add a dependency. Spring by itself only provides a caching abstraction. This means you still have to actually add a caching implementation to your classpath. Like I mentioned at the start of the article, I will be using Ehcache.

So, add the following dependency to your Maven descriptor (pom.xml):

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.5</version>
</dependency>

Ehcache has a lot of possibilities. You can configure the cache size, time to live, time to idle, if it should be a persistent cache, if it should overflow to disk, … . In this example I will just create a simple in memory cache. Create a file called ehcache.xml in the src/main/resources folder:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
         updateCheck="true" monitoring="autodetect" dynamicConfig="true">

    <cache name="tasks"
           maxElementsInMemory="100" eternal="false"
           overflowToDisk="false"
           timeToLiveSeconds="300" timeToIdleSeconds="0"
           memoryStoreEvictionPolicy="LFU" transactionalMode="off">
    </cache>
</ehcache>

As you can see, I create a <cache> with the name “tasks”, with 100 items that can be stored in memory, with a time to live of 5 minutes.

The next step is to configure Spring boot to use this configuration file by adding the following property to application.properties (or application.yml):

# application.properties
spring.cache.ehcache.config=classpath:ehcache.xml
# application.yml
spring:
  cache:
    ehcache:
      config: classpath:ehcache.xml

The final step is to enable caching itself in Spring boot with the @EnableCaching annotation. Open the main class and add the annotation like this:

@SpringBootApplication
@EnableCaching
public class SpringBootEhcacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootEhcacheApplication.class, args);
    }
}

Setting up caching for your service

Setting up caching for a class is quite easy. Open the TaskServiceImpl and add the @Cacheable annotation to the methods you want to cache, for example:

@Cacheable("tasks")
public List<TaskDTO> findAll() {
    logger.info("Retrieving tasks");
    return Arrays.asList(new TaskDTO(1L, "My first task", true), new TaskDTO(2L, "My second task", false));
}

Now, if you run the application again and refresh the tasks endpoint a few times, you’ll see that the log entry within TaskServiceImpl only appears once. So indeed, caching is working fine now.

There are a few other annotations you can use though, so let’s see if we can test those as well!

Be aware, the Spring cache abstraction works by proxying your target class. This means that calls within the same service will not be cached. So if we had another method called findStuff() which was calling findAll(), the call will not be cached.

Conditional caching

Have you ever encountered a REST API that has a noCache parameter that allows you to retrieve the actual value rather than the cached version? Well, with Spring you can implement such a behaviour as well.

If we take a look at the documentation about cache abstraction, we see there is the possibility to implement conditional caching.

So, if we modify our REST API a bit to include an optional @RequestParam called noCache, we could use this parameter to implement the conditional caching:

@RequestMapping(method = RequestMethod.GET)
public List<TaskDTO> findAll(@RequestParam(required = false) boolean noCache) {
    return service.findAll(noCache);
}

In our service we now have to change the @Cacheable annotation a bit:

@Cacheable(value = "tasks", condition = "!#noCache")
public List<TaskDTO> findAll(boolean noCache) {
    logger.info("Retrieving tasks");
    return Arrays.asList(new TaskDTO(1L, "My first task", true), new TaskDTO(2L, "My second task", false));
}

So, if we rerun the service now, we’ll see that the caching now works fine like previously, but if you add ?noCache=true to the end of the URL (like this: http://localhost:8080/api/tasks?noCache=true), and you check the logs, you’ll see it prints the log statement from the service each time. That means the conditional caching is working fine!

But what if we still want to update the cache if the user provides noCache=true, but we just don’t want to use the cache as the response? Well, then you could use the @CachePut annotation and use the opposite condition of what we used before:

@CachePut(value = "tasks", condition = "#noCache")
@Cacheable(value = "tasks", condition = "!#noCache")
public List<TaskDTO> findAll(boolean noCache) {
    logger.info("Retrieving tasks");
    return Arrays.asList(new TaskDTO(1L, "My first task", true), new TaskDTO(2L, "My second task", false));
}

However, this is not enough yet. Spring generates a key, by default using the hashcode of all method arguments. In our case we want to use the same key, ignoring the state of noCache . The easiest workaround to this problem is by setting your own key based on the field and for the other annotation use the exact opposite of that annotation, for example:

@CachePut(value = "tasks", condition = "#noCache", key = "#noCache")
@Cacheable(value = "tasks", condition = "!#noCache", key = "!#noCache")
public List<TaskDTO> findAll(boolean noCache) {
    logger.info("Retrieving tasks");
    return Arrays.asList(new TaskDTO(1L, "My first task", true), new TaskDTO(2L, "My second task", false));
}

The result should be similar, however, even when you specifiy the noCache=true parameter, it will still store the result in the cache, and both will use the same key, namely the hashcode of Boolean.TRUE.

Clearing the cache

The last annotation I will take a look at in this article is the @CacheEvict annotation. With this annotation you can clear the cache “on command”, rather than have it getting expired. This can be useful in situations when someone updated something in the back-end and wants the value immediately to be updated. By evicting the cache the next time someone makes a call, there won’t be a cache to load the value from. For a small cache with a TTL of 5 mins this isn’t probably as important, but if you choose to cache for several hours, it could be quite useful.

So, let’s create a separate operation to evict the cache in our controller:

@RequestMapping(value = "/cache", method = RequestMethod.DELETE)
public void clearCache() {
    service.clearCache();
}

Now we just have to add an empty method to our service with the right annotation:

@CacheEvict(value = "tasks", allEntries = true)
public void clearCache() {
    // Empty method, @CacheEvict annotation does everything
}

And there we have it, make sure you add the allEntries property, without it you can still use @CacheEvict to remove one item from the cache. Now we can clear the cache now by calling http://localhost:8080/api/tasks/cache, using the DELETE method.

You might need a REST client to test this. I’m personally using Postman, but other REST clients such as DHC should also work. I’m often getting questions about what REST client I’m using, so here it is.

Anyhow, test it out by first calling the tasks API once to fill the cache, then evict it and call the tasks API again. Normally you should see the log appear again, while before this you had to wait 5 minutes before the method would be invoked again due to the caching TTL.

postman-delete-task-cache

You probably want to secure this endpoint using Spring Security, but that’s out of scope for this article. Anyways, now we’ve seen most of the caching annotations, so this is where I’ll end the article.

Achievement: Cache master

If you’re seeing this, then it means you successfully managed to make it through this tutorial. If you’re interested in the full code example, you can find it on GitHub.

Tagged , , .

g00glen00b

IT Consultant with a passion for JavaScript. Experienced in the Spring Framework and various JavaScript frameworks.