Configuring EhCache 3 and Event Listeners in Spring Boot
TLDR: You can jump straight to an example project azdanov/ehcache-config-demo.
Spring offers amazing caching utilities. Throw in some @EnableCaching
, @Cacheable
, @CacheEvict
on and the results will be cached. You can read more about Spring Caching on https://www.baeldung.com/spring-cache-tutorial.
The problem that I encountered was figuring out how to use EhCache 3 (org.ehcache
) and inside Spring Boot with Java Based Configuration. A lot of how-to guides were either showing ehcache.xml
or an older EhCache 2 (net.sf.ehcache
) configuration convention.
Let's see how to do configure EhCache 3 programmatically.
Setup
First step is adding required dependencies to the project (Maven/Gradle):
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.0</version>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
or
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.ehcache:ehcache:3.9.0'
implementation 'javax.cache:cache-api:1.1.1'
}
Configuration
Next step would be creating a configuration class:
package org.js.azdanov.ehcacheconfigdemo.configuration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ExpiryPolicyBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.jsr107.Eh107Configuration;
import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.configuration.CacheEntryListenerConfiguration;
import javax.cache.configuration.Factory;
import javax.cache.configuration.FactoryBuilder;
import javax.cache.configuration.MutableCacheEntryListenerConfiguration;
import javax.cache.event.CacheEntryEventFilter;
import java.time.Duration;
@EnableCaching
@Configuration
public class CacheConfiguration {
private static final Factory<? extends CacheEntryEventFilter<Integer, Integer>> NO_FILTER = null;
private static final boolean IS_OLD_VALUE_REQUIRED = false;
private static final boolean IS_SYNCHRONOUS = true;
// (1)
private final javax.cache.configuration.Configuration<Integer, Integer> jcacheConfiguration =
Eh107Configuration.fromEhcacheCacheConfiguration(
CacheConfigurationBuilder
.newCacheConfigurationBuilder(Integer.class, Integer.class, ResourcePoolsBuilder.heap(100))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(5)))
.build()
);
CacheEntryListenerConfiguration<Integer, Integer> listenerConfiguration =
new MutableCacheEntryListenerConfiguration<>(
FactoryBuilder.factoryOf(CacheListener.class),
NO_FILTER,
IS_OLD_VALUE_REQUIRED,
IS_SYNCHRONOUS);
// (2)
@Bean
public JCacheManagerCustomizer cacheManagerCustomizer() {
return cm -> {
createCache(cm, "worker1");
createCache(cm, "worker2");
};
}
private void createCache(CacheManager cm, String cacheName) {
Cache<Integer, Integer> cache = cm.getCache(cacheName);
if (cache == null) {
cm.createCache(cacheName, jcacheConfiguration)
.registerCacheEntryListener(listenerConfiguration);
}
}
}
- Here's the configuration itself, which can be modified to read the values from an
application.properties
file or any other resource. - Inside
JCacheManagerCustomizer
bean the individual cache creation happens.
I've added some extras where a cache event listener will be registered for each created cache.
Here's how a listener class might look like:
package org.js.azdanov.ehcacheconfigdemo.configuration;
import lombok.extern.slf4j.Slf4j;
import javax.cache.event.CacheEntryCreatedListener;
import javax.cache.event.CacheEntryEvent;
@Slf4j
public class CacheListener implements CacheEntryCreatedListener<Integer, Integer> {
@Override
public void onCreated(final Iterable<CacheEntryEvent<? extends Integer, ? extends Integer>> cacheEntryEvents) {
for (CacheEntryEvent<? extends Integer, ? extends Integer> entryEvent : cacheEntryEvents) {
log.info("Cached key: {}, with value: {}", entryEvent.getKey(), entryEvent.getValue());
}
}
}
Now for the last step let's mark methods inside a service with previously defined cache names:
package org.js.azdanov.ehcacheconfigdemo.service;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.Random;
@RequiredArgsConstructor
@Service
public class WorkerService {
private final Random random;
@Cacheable(value = "worker1", key = "#bound")
public int getFirstWork(int bound) {
return random.nextInt(bound);
}
@Cacheable(value = "worker2", key = "#bound")
public int getSecondWork(int bound) {
return random.nextInt(bound);
}
}
Last step
It's time to see if this works or not with a simple sanity-check 'test':
package org.js.azdanov.ehcacheconfigdemo;
import lombok.extern.slf4j.Slf4j;
import org.js.azdanov.ehcacheconfigdemo.service.WorkerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import java.util.Random;
@Slf4j
@EnableCaching
@Configuration
@SpringBootApplication
public class EhcacheConfigDemoApplication {
@Autowired
WorkerService workerService;
public static void main(String[] args) {
SpringApplication.run(EhcacheConfigDemoApplication.class, args);
}
@Bean
Random random() {
return new Random();
}
@EventListener(ApplicationReadyEvent.class)
public void afterStartup() {
log.info("Running!");
log.info("getFirstWork(100): {}", workerService.getFirstWork(100));
log.info("getFirstWork(50): {}", workerService.getFirstWork(50));
log.info("getFirstWork(100): {}", workerService.getFirstWork(100));
log.info("getFirstWork(50): {}", workerService.getFirstWork(50));
log.info("getSecondWork(100): {}", workerService.getSecondWork(100));
log.info("getSecondWork(50): {}", workerService.getSecondWork(50));
log.info("getSecondWork(100): {}", workerService.getSecondWork(100));
log.info("getSecondWork(50): {}", workerService.getSecondWork(50));
}
}
This is the result I got in the terminal:
Running!
Cached key: 100, with value: 8
getFirstWork(100): 8
Cached key: 50, with value: 29
getFirstWork(50): 29
getFirstWork(100): 8
getFirstWork(50): 29
Cached key: 100, with value: 6
getSecondWork(100): 6
Cached key: 50, with value: 33
getSecondWork(50): 33
getSecondWork(100): 6
getSecondWork(50): 33
Seems to be working 🙂