Spring Boot Eh Cache made simple

This tutorial covers all basic aspects of using a Cache implementation named Ehcache in Spring Boot. We will start with some basic cache definitions and then we will create a quickstart example.

Firstly, let’s introduce Eh Cache:

Ehcache is a cache library which improves performance by reducing the load on underlying resources. Ehcache is an open source, standards-based (JSR-107) cache that can improve performance, offload your database, and complex operations. It’s the most widely-used Java-based cache because it’s robust, proven, full-featured, and provides out of the box Spring Caching and Hibernate integration.

That being said, let’s see how to create a Spring Boot application which uses Eh Cache to speed up CPU intensive operations.

Coding a Spring Boot Eh Cache application

Our example application will store in a Cache a Long attribute as key and as value Long attribute that corresponds to the factorial of the key. The larger is the key, the larger is the impact on the CPU to calculate the value. Therefore we will cache it.

Spring Boot Eh Cache tutorial

To enable Caching, the first requirement is to annotate our application with @EnableCaching annotation.

@SpringBootApplication
@EnableCaching
public class SpringBootEhcacheDemoApplication {

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

}

Now the Service class, which contains the Cache annotations:

@Service
public class CacheService {

	@Cacheable("numbers")
	public Long getFactorial(long id) {
		return calculateFactorial(id);
	}

	@CachePut(value = "numbers")
	public Long update(Long id) {
		return calculateFactorial(id);
	}

	@CacheEvict(value = "numbers", allEntries = true)
	public void evictData() {
		System.out.println("Cache evicted");

	}
	
	public long calculateFactorial(long n) {
		System.out.println("calculating...");
		return LongStream.rangeClosed(1, n).reduce(1, (long x, long y) -> x * y);
	}
}
  1. Let’s begin from the method getFactorial. This method contains an annotations @Cachable which references the Cache “numbers”. That means, that the method will access the Cache “numbers”. If the id is available as key, it will be returned immediately. Otherwise, the method calculateFactorial will calculate the value and it will be added to the Cache.
  2. Next, let’s take a look at the update method. It contains the @CachePut annotation. This is used to add an element to the Cache. In this case, the calculateFactorial will always be called to calculate the value and it will be added to the Cache.
  3. Finally, the evictData method is used to evict all entries from the Cache with @CacheEvict. It can be parameterized however, for the sake of simplicity, we will clean all Cache data with “allEntries=true”

We are done with Cache Management. Let’s add a REST Control to access our Service:

@RestController
@RequiredArgsConstructor
@RequestMapping("/numbers")
public class CacheController {
	private final CacheService cacheService;


	@GetMapping("/get/{id}")
	public ResponseEntity<Long> get(@PathVariable long id) {

		Long fact = cacheService.getFactorial(id);

		return new ResponseEntity<>(
				fact,
				HttpStatus.OK
				);
	}

	@GetMapping("/put/{id}")
	public ResponseEntity<Long> add(@PathVariable long id) {

		Long fact = cacheService.update(id);

		return new ResponseEntity<>(
				fact,
				HttpStatus.OK
				);
	}

	@GetMapping("/evict")
	public ResponseEntity<String> evict() {

		cacheService.evictData();

		return new ResponseEntity<>(
				"Cache cleaned",
				HttpStatus.OK
				);
	}
}

The Controller is pretty simple. It is merely a wrapper to the ServiceCache which provides the key, when required to store/update the Cache.

To complete our example, we will add a CacheEventListener which traces Cache events. We will use it to verify cache hits or misses:

public class CacheLogger implements CacheEventListener<Object, Object> {

  private final Logger LOG = LoggerFactory.getLogger(CacheLogger.class);

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

}

Adding the Eh Cache configuration

To define the actual Cache settings, we will provide a minimal Eh Cache configuration in a file named resources/ehcache.xml.

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

    <cache alias="numbers">
        <key-type>java.lang.Long</key-type>
        <value-type>java.lang.Long</value-type>
        <expiry>
            <ttl unit="seconds">30</ttl>
        </expiry>

        <listeners>
            <listener>
                <class>com.sample.config.CacheLogger</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>

        <resources>
            <heap unit="entries">100</heap>
            <offheap unit="MB">10</offheap>
        </resources>
    </cache>

</config>

As you can see, this file contains the configuration for the Cache named “numbers”, including:

  • The key/value type
  • The time-to-live of items in the Cache items. In our configurations, items will expiry in 30 seconds
  • The Cache holds up to 100 entries on heap
    The Cache holds as well up to 10 MB of off-heap memory before it starts evicting data

The configuration also contains a reference to our CacheLogger which will be fired upon any CREATED or EXPIRY events.

Testing our Eh Cache

To test our application, let’s add the required dependencies to the pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.7.1</version>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>

Now we can run the application:

mvn install spring-boot:run

we will first put an item in the cache:

$ curl http://localhost:8080/numbers/put/9
362880

Let’s check on the Console what happened:

calculating...
2021-10-04 15:28:15.824  INFO 19421 --- [e [_default_]-0] com.sample.config.CacheLogger            : Key: 9 | EventType: CREATED | Old value: null | New value: 362880

So, the calculation happened as that key was missing in the Cache.

Now let’s try fetching that key from the Cache:

$ curl http://localhost:8080/numbers/get/9
362880

If you check the logs of Spring Boot Console, you will see that no CacheLogger notifications are there. Nor the “calculating…..” message is there.

We will now evict the data from the Cache:

curl http://localhost:8080/authors/evict

Now if you try accessing again the key ‘9’, it will require again a Calculation.

Much the same way, if you wait for 30 seconds, the item will be evicted from the Cache:

2021-10-04 16:53:54.324  INFO 23188 --- [e [_default_]-1] com.sample.config.CacheLogger            : Key: 9 | EventType: EXPIRED | Old value: 362880 | New value: null
calculating...
2021-10-04 16:53:54.325  INFO 23188 --- [e [_default_]-1] com.sample.config.CacheLogger            : Key: 9 | EventType: CREATED | Old value: null | New value: 362880

Using Conditional Caching

Caching annotations can include conditions as well. Consider, for example, we want to store in Cache only calculation which are really expensive. So, factorial numbers with a basis greater than 10.

Here is how to set it in the @CachePut:

@CachePut(value = "numbers", condition="#id>10")
public Long update(Long id) {
	return calculateFactorial(id);
}

In the above example, keys which are not larger than 10 will not go in the Cache.

Much the same way, we can make our Cache eviction more granular. For example, we can choose to delete just a single item:

@CacheEvict(value = "numbers", key = "#id")
public void delete(long id) {
 System.out.println("Cache evicted");
}

Conclusion

In this tutorial we have covered the basics of Eh Cache and how to integrate it with a Spring Boot application to store and fetch data from a Cache.

The source code for this tutorial is available here: https://github.com/fmarchioni/masterspringboot/tree/master/cache/spring-boot-ehcache

Leave a comment