Today, we are going to explore a little bit one of the cache options have available when working with Java projects. This option is Ehcache.
Ehcache is an open-source, standards-based cache that boosts performance, offloads your database, and simplifies scalability. It’s the most widely-used Java-based cache because it’s robust, proven, full-featured, and integrates with other popular libraries and frameworks. Ehcache scales from in-process caching, all the way to mixed in-process/out-of-process deployments with terabyte-sized caches.
— Ehcache web page —
In our case, we are going to use Ehcache version 3 as this provides an implementation of a JSR-107 cache manager and Spring Boot to create a simple endpoint that is going to return the MD5 hash of a given text.
Let’s do it.
We are going to be starting a maven project and adding various dependencies:
Dependency | Version | Comment |
---|---|---|
spring-boot-starter-parent | 2.3.4.RELEASE | Parent of our project |
spring-boot-starter-web | — | Managed by the parent |
spring-boot-starter-actuator | — | Managed by the parent |
spring-boot-starter-cache | — | Managed by the parent |
lombok | — | Managed by the parent |
javax.cache:cache-api | 1.1.1 | |
org.ehcache:ehcache | 3.8.1 |
Now, let’s create the endpoint and the service we are going to cache. Assuming you, the reader, have some knowledge of spring, I am not going to go into details on this.
// Controller code
@RestController
@RequestMapping(value = "/api/hashes")
@AllArgsConstructor
public class HashController {
private final HashService hashService;
@GetMapping(value = "/{text}", produces = APPLICATION_JSON_VALUE)
public HttpEntity<String> generate(@PathVariable final String text) {
return ResponseEntity.ok(hashService.generateMd5(text));
}
}
// Service code
@Service
public class HashServiceImpl implements HashService {
@Override
public String generateMd5(final String text) {
try {
final MessageDigest md = MessageDigest.getInstance("MD5");
md.update(text.getBytes());
return DatatypeConverter.printHexBinary(md.digest()).toUpperCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to get MD5 instance");
}
}
}
Simple stuff. Let’s know add the cache capabilities.
First, we will add the cache configuration to our service as a new annotation.
@Cacheable(value = "md5-cache")
This is going to define the name that it is going to be used for this cache ‘md5-cache’. As a key, the content of the method parameter will be used.
The next step is to add the configuration. To activate the cache capabilities on Spring we can use the configuration and enable configuration annotations:
- @Configuration
- @EnableCaching
Even with this, and using the Spring Boot auto-configuration, no caches are created by default and we need to create them. There are two ways this can be done:
- Using and XML file with the configuration.
- Programmatically.
If you are a follower of this blog or you have read some of the existing posts, probably, you have realised I am not a big fan of the XML configuration and I prefer to do things programmatically and, this is what we are going to do. In any case, I will try to add the XML equivalent to the configuration but, it has not been tested.
The full configuration is:
@Bean
CacheManager getCacheManager() {
final CachingProvider provider = Caching.getCachingProvider();
final CacheManager cacheManager = provider.getCacheManager();
final CacheConfigurationBuilder<String, String> configurationBuilder =
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, String.class,
ResourcePoolsBuilder.heap(50)
.offheap(10, MemoryUnit.MB)) .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)));
final CacheEventListenerConfigurationBuilder asynchronousListener = CacheEventListenerConfigurationBuilder
.newEventListenerConfiguration(new CacheEventLogger(), EventType.CREATED, EventType.EXPIRED)
.unordered().asynchronous();
cacheManager.createCache("md5-cache",
Eh107Configuration.fromEhcacheCacheConfiguration(configurationBuilder.withService(asynchronousListener)));
return cacheManager;
}
But, let’s explain it in more details.
final CacheConfigurationBuilder<String, String> configurationBuilder =
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, String.class,
ResourcePoolsBuilder.heap(50)
.offheap(10, MemoryUnit.MB))
.withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)));
Here we can see of the cache characteristics:
- The type of data: String for both, key and value.
- Cache size: Heap = 50 entries and size 10MB (obviously absurd numbers but good enough to exemplify).
- And the expiration policy: ‘Time to Idle’ of 10 seconds. It can be defined as ‘Time to Live’.
The next thing we are creating is a cache listener to log the operations:
final CacheEventListenerConfigurationBuilder asynchronousListener = CacheEventListenerConfigurationBuilder
.newEventListenerConfiguration(new CacheEventLogger(), EventType.CREATED, EventType.EXPIRED)
.unordered().asynchronous();
Basically, we are going log a message when the cache creates or expired and entry. Other events can be added.
And, finally, we create the cache:
cacheManager.createCache("md5-cache",
Eh107Configuration.fromEhcacheCacheConfiguration(configurationBuilder.withService(asynchronousListener)));
With the name matching the one we have used on the service annotation.
The XML configuration should be something like:
<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="md5-cache">
<key-type>java.lang.String</key-type>
<value-type>java.lang.String</value-type>
<expiry>
<tti unit="seconds">10</ttl>
</expiry>
<listeners>
<listener>
<class>dev.binarycoders.ehcache.utils.CacheEventLogger</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">50</heap>
<offheap unit="MB">10</offheap>
</resources>
</cache>
Just remember we need to add a property to our ‘application.properties’ file if we choose the XML approach.
spring.cache.jcache.config=classpath:ehcache.xml
And, with this, everything should be in place to test it. We just need to run our application and invoke our endpoint, for example, using ‘curl’.
curl http://localhost:8080/api/hashes/hola
The result should be something like this:
2020-10-25 11:29:22.364 : Type: CREATED, Key: hola, Old: null, New: D41D8CD98F00B204E9800998ECF8427E
2020-10-25 11:29:42.707 : Type: EXPIRED, Key: hola, Old: D41D8CD98F00B204E9800998ECF8427E, New: null
2020-10-25 11:29:42.707 : Type: CREATED, Key: hola, Old: null, New: D41D8CD98F00B204E9800998ECF8427E
As we can see, invoking multiple times the endpoint only created the first log line and, it is just after waiting for some time (more than 10 seconds) when the cache entry gets expired and re-created.
Obviously, this is a pretty simple example but, this can help us to increase the performance of our system for more complex operations.
As usual, you can find the code here.