Ehcache Cache Provider in Spring Boot

Photo by Cris Ovalle on Unsplash

Ehcache Cache Provider in Spring Boot

Brief overview of using Ehcache 3 in Spring Boot 3 with Java 17 (XML-based configuration)

This is a continuation of my blog post about caching in Spring Boot. If caching in Spring Boot is a new topic for you, it's best to start there.

1. Introduction

In this blog post, we will look at how we can use the caching provider Ehcache in Spring Boot. Ehcache is an open-source library implemented in Java for implementing caches in Java programs, especially local and distributed caches in the main memory or on the hard disk. Thanks to the implementation of JSR-107, Ehcache is fully compatible with jakarta.persistence API. Due to this compatibility, integration into Spring or Hibernate is very easy.

2. Why choose Ehcache?

Besides the possibility to configure multi-tier caching tiers, Ehcache supports distributed caching environments, which means it can replicate and synchronize data across multiple microservices to increase scalability via a clustered cache.

In addition, Ehcache provides a variety of configuration options and settings that allow developers to customize the cache to meet the specific needs of their applications.

3. Storage tiers

Ehcache can be configured in such a way that the caching layer can consist of more than one memory area. When using more than one memory area, the areas are arranged as hierarchical tiers. The lowest tier is called the Authority tier and the other tiers are called the Near Cache.

The most frequently used data is stored in the fastest caching tier (top layer). The authority tier contains all cache entries.

The memory areas supported by Ehcache include:

  • On-Heap Store: Uses the Java heap memory to store cache entries and shares the memory with the application. The cache is also scanned by the garbage collection. This memory is very fast, but also very limited.

  • Off-Heap Store: Uses the RAM to store cache entries. This memory is not subject to garbage collection. Still has quite fast memory, but slower than the on-heap memory, because the cache entries have to be moved to the on-heap memory before they can be used.

  • Disk Store: Uses the hard disk to store cache entries. Much slower than RAM. It is recommended to use a dedicated SSD that is only used for caching.

  • Clustered: Not covered in this post.

For a multi-tier setup, the following must be considered:

  • Due to the current Ehcache implementation, there must always be a heap-tier

  • The size of the heap tier must be smaller than the size of the off-heap tier and the size of the off-heap tier must be smaller than the size of the disk tier (see figure). The tiers are related to each other. The fastest memory is at the top and the next slowest one is below it.

Multiple-tier setup

Nevertheless, a single-tier setup is also possible, for example, caches that store exclusively on-heap or off-heap.

4. Demo

I brought along a demo project with some code, which you can find on my GitHub Account. The demo project is built on the following foundation:

  • Java 17

  • Spring Boot 3

  • Gradle 7.6

  • Ehcache 3.10.8

4.1 Used Dependencies

For the Ehcache demo project, we need the following dependencies in our Spring Boot application:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation('org.ehcache:ehcache:3.10.8') {
        capabilities {
            requireCapability('org.ehcache:ehcache-jakarta')
        }
    }
}

The dependency spring-boot-starter-web is a starter for building web applications. We will use the same examples here as from my other caching post. This means we will build a simple service that performs a calculation for us and this calculation can be triggered by using a REST endpoint.

Furthermore, for caching we need the dependency spring-boot-starter-cache as well as the dependency ehcache as a cache provider. In this post, I will demonstrate the configuration of Ehcache in XML. JAXB is used for parsing the configuration file. Ehcache 3.10 provides support for the Jakarta EE Namespaced versions of the JAXB APIs (see the capabilities block in build.gradle). Since Spring Boot 3 has already moved to the new Jakarta EE APIs, we can conveniently use a provided Ehcache variant here.

For older versions of Java and/or Spring Boot, appropriate JAXB dependencies may need to be added, for example, the JAXB reference implementation from glassfish (org.glassfish.jaxb:jaxb-runtime:[2.2,3)).

4.2 Enable Caching

As we already know, to enable caching support in Spring Boot, we need a simple configuration class that must be annotated with @EnableCaching:

@Configuration
@EnableCaching
public class EhcacheConfig {
}

4.3 Cacheable operation

For our first example, we will take a rather simple mathematical calculation. We assume that this calculation is a very expensive operation whose result we want to cache. For this, we annotate the method with the @Cachable annotation:

@Service
@Slf4j
public class CalculationService {

  @Cacheable(value = "areaOfCircleCache", key = "#radius")
  public double areaOfCircle(int radius) {
    log.info("calculate the area of a circle with a radius of {}", radius);
    return Math.PI * Math.pow(radius, 2);
  }
}

4.4 Ehcache cache configuration

Now the configuration of the Ehcache cache has to be done. The configuration is XML-based. We will use a three-tier cache with a disk store as an authority tier.

4.4.1 Cache template

First, we will define a cache template in the ehcache.xml file. You can create this file, for example, in the resources folder of the application. A Cache template is especially advantageous if the application is to have more than one cache, but the configuration of the caches is largely the same. For our demo application, it is conceivable, for example, that we want to cache the results of the circle area calculation and in another cache the results of a power calculation. For the cache template, we use the following XML code:

<config
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.ehcache.org/v3"
    xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.10.xsd">

    <!-- Persistent cache directory -->
    <persistence directory="spring-boot-ehcache/cache" />

    <!-- Default cache template -->
    <cache-template name="default">
        <expiry>
            <ttl unit="hours">1</ttl>
        </expiry>
        <resources>
            <heap>500</heap>
            <offheap unit="MB">10</offheap>
            <disk persistent="true" unit="MB">20</disk>
        </resources>
    </cache-template>
</config>

In the persistence tag, we define the directory for a file-based cache on the hard disk (disk store). This is only the definition of the folder. Whether we want to use a disk store or not will be configured later.

In the expiry tag, we define a time to live (ttl) of 1 hour. The ttl specifies how long a cache entry may remain in the cache independently of access. After the specified time has expired, the value is removed from the cache.

It is also possible to define a time to idle (tti). The tti specifies how long the cache entry may exist in the cache without access. For example, if a value is not requested for more than 30 seconds, it is removed from the cache.

In the resources tag, we configure the tiers and capacities of our cache. We use a three-tier cache with a disk store as the authority tier:

  • heap: For the on-heap store we configure a capacity of 500 cache entries. This is the maximum number of entries before eviction starts.

  • offheap: For the off-heap store we configure a capacity of 10 MB.

  • disk: As disk cache, we configure 20 MB. Important: The disk cache must always have a higher memory capacity than the heap cache, otherwise the application throws an exception during application startup when parsing the XML file.

4.4.2 Cache configuration

Using the cache template we just created, we can now configure our cache. Thanks to the template we only have to define a name (alias) as well as the type of the cache key (key-type) and the type of the cached value (value-type):

<config ...>
    <!-- Persistent cache directory -->
    ...
    <!-- Default cache template -->
    ...
    <!-- Cache configuration -->
    <cache alias="areaOfCircleCache" uses-template="default">
        <key-type>java.lang.Integer</key-type>
        <value-type>java.lang.Double</value-type>
    </cache>
</config>

I would like to point out that we could have configured the cache without the cache template. All settings made in the cache-template tag can also be used directly within the cache tag.

Note: If the cache key consists of more than one method parameter, the type java.util.ArrayList must be used as key-type.

4.4.3 Wiring of ehcache.xml with application.yml

Finally, we tell the application.yml file where our configuration file for Ehcache is located:

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

4.4.4 Testing the cache

If, for example, we call the URL http://localhost:8080/api/area-of-circle?radius=9 after starting our application, the area of a circle with a radius of 9 is calculated and the result is displayed in the browser or Postman. For the first call of the URL, the calculation of the circle area is still carried out. For all further calls, we get the result directly from the cache. Our built-in log output shows that the method is entered only once.

You probably noticed that Ehcache created a folder in the root folder of your project after starting the application (see figure).

This is because we have defined a disk store as a persistent caching tier. This tier is located on your hard disk exactly in that folder. This tier also ensures that the cache is preserved even if you shut down the application.

5. Cache Listener

Cache listeners allow you to register callback methods that are executed when a cache event occurs. The listener reacts to the following events:

  • A cache entry is placed in the cache (CREATED).

  • The validity of a cache entry has expired (EXPIRED).

  • A cache entry is evicted from the cache (EVICTED).

  • A cache entry has been updated. Updated means that an entry with a certain key already exists in the cache, while a new entry with the same key is inserted (UPDATED).

  • Similar to EVICTED is REMOVED.

5.1 Implementing the listener

For a listener that reacts to the cache events, only the interface CacheEventListener needs to be implemented. My sample implementation CacheListener only logs the occurred cache event on the console. You can implement whatever you want there.

@Slf4j
public class CacheListener implements CacheEventListener<Object, Object> {

  @Override
  public void onEvent(CacheEvent<?, ?> cacheEvent) {
    log.info("Key: {} | EventType: {} | Old value: {} | New value: {}",
        cacheEvent.getKey(), cacheEvent.getType(), cacheEvent.getOldValue(), cacheEvent.getNewValue());
  }
}

5.2 Register the listener

A listener must be registered for a certain cache and cache events at the level of a cache (or cache template). Therefore, the listener only receives events for caches for which it has been registered.

In the listeners tag in the file ehcache.xml, we register our CacheListener and the events to fire on:

<config ...>
    <!-- Persistent cache directory -->
    ...
    <!-- Default cache template -->
    ...
    <!-- Cache configuration -->
    <cache alias="areaOfCircleCache" uses-template="default">
        <key-type>java.lang.Integer</key-type>
        <value-type>java.lang.Double</value-type>
        <listeners>
            <listener>
                <class>dev.wagnus.ehcache.config.CacheListener</class>
                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
                <event-ordering-mode>UNORDERED</event-ordering-mode>
                <events-to-fire-on>CREATED</events-to-fire-on>
                <events-to-fire-on>EXPIRED</events-to-fire-on>
            </listener>
        </listeners>
    </cache>
</config>

5.3 Testing the listener

For easier testing, it is recommended to reduce the TTL to 5 seconds. Please note that this TTL only applies to all new cache entries. For entries (other radius values) that are already in the cache, the TTL of one hour or whatever was previously configured still applies.

The expiration event is only fired when the TTL has expired AND there is a new access to the cache for the respective cache key (see below log output).

6. Summary

In this post, I have shown how to integrate and use Ehcache in Spring Boot 3. In addition to demonstrating the different storage tiers, we also looked at cache event listeners. Ehcache is a great solution for scalable microservices when no external key/value store such as Redis is available for caching.

The source code for this post is available on GitHub.