EhCache3 as JCache (JSR-107) Implementation with Cache Statistics

ehcache3 jcache performance

If you are reading this you already know that caching is mechanism to store static data In-Memory for faster access and avoid expensive database calls or any kind of service calls to get the data.

Read my other post to learn InMemory Caching using EhCache3.

There are many ways to implement cache in java based applications, starting from very simple Map based caching to Apache Common JCS, EhCache, Apache Ignite, Hazelcast, Oracle Coherence and many more. With so many implementations available, question is which one we should go for?

If you have to choose one from multiple options best bet is to look for standard. Standard because many people have put their mind together in it, better support, examples and guide available for reference plus expect that further upgrades/changes will not breaking changes.

So for InMemory cache Standard Specification is JSR-107.  You can read about it in more detail at JSR 107: JCACHE – Java Temporary Caching API. JCache specifies API and semantics for temporary, in memory caching of Java objects, including object creation, shared access, spooling, invalidation, and consistency across JVM’s.

About JCache

JCache is just specification, set of interfaces / APIs and there are multiple implementation available from different vendors.  You have have to choose one implementation that you like or prefer.
Some of the implementations are –

  1. Ehcache
  2. Coherence
  3. Hazelcast
  4. Apache Ignite 1.0

For comprehensive list of JCache implementations click here

Subtle Benefits

  1. Irrespective of what implementation you choose you will always use classes and interfaces provided by jcache api. Which means you can change underlying implementation anytime
  2. If you upgrade underlying implementation it should not be breaking your existing code
  3. Access live cache usage statistics at runtime using JMX
  4. You are using Standard API Specification 🙂

JCache with EhCache as Implementation

Remember to import and use all caching classes from javax.cache package. There are classes with same names in ehcache package also, make sure you don’t use those otherwise you will face issues.

I will showcase example of how to use JCache with EhCache as underlying Implementation. For this example you need to have following jars in the class path.

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.0</version>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency> <!-- We need this because ehcache uses slf4j for logging -->
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>

I will showcase following basic and very important aspects of InMemory caching –

Item Expiry – If entry in the cache meets the expiry condition, it will be evicted (removed) from the cache. To standard type of expiry conditions are Time-To-Live and Time-To-Idle. I will use Time-To-Live

Cache Max Size – Maximum size up to which the cache can grow. The max size constraint can be set by byte size (KB, MB etc) or no on entries in the cache. I will use no of entries.

Cache Usage Statistics –  When you implement caching you are certainly interested in knowing cache usage statistics like, cache hits, misses, no of time cache was accessed etc. With JCache you can get all cache usage statistics live at runtime using JMX mechanism.

There are 2 ways to configure cache. XML and programmatic. I will use xml way because it’s more readable and intuitive.

Below is xml configuration for a cache of Person object by personId With max size of 5 entries and Time-To-Live 30 seconds.

ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<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.xsd
        http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">

    <cache alias="PersonCache">
        <key-type>java.lang.Integer</key-type>
        <value-type>com.rakesh.caching.Person</value-type>
        <expiry>
            <ttl unit="seconds">30</ttl>
        </expiry>
        <resources>
            <heap unit="entries">5</heap>
        </resources>
    </cache>
</config>

The object that we will put in the cache

Person.java
package com.rakesh.caching;

public class Person {
    private int personId;
    private String firstName;
    private String lastName;
    
    public Person(){}
    public Person(int personId, String firstName, String lastName) {
        super();
        this.personId = personId;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    public int getPersonId() {
        return personId;
    }
    public void setPersonId(int personId) {
        this.personId = personId;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    @Override
    public String toString() {
        return "Person [personId=" + personId + ", firstName=" + firstName + ", lastName=" + lastName + "]";
    }
}

Cache manager helper class which  takes care of initializing cache and implements some utility method to easily access cache and cache usage statistics

AppCacheManager.java
package com.rakesh.caching;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.URI;
import java.util.Iterator;

import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

public class AppCacheManager {
    private static AppCacheManager instance = null;
    private CacheManager cacheManager = null;

    private AppCacheManager() {
        URI ehcacheCacheXmlUri = new File("./resources/ehcache.xml").toURI();
        CachingProvider cachingProvider = Caching.getCachingProvider();
        cacheManager = cachingProvider.getCacheManager(ehcacheCacheXmlUri, getClass().getClassLoader());
        System.out.println("Cache Initialized");
    }

    public static AppCacheManager getInstance() {
        if (instance == null) {
            instance = new AppCacheManager();
        }
        return instance;
    }
    
    public Cache<Integer, Person> getPersonCache(){
        return this.cacheManager.getCache("PersonCache", Integer.class, Person.class);
    }
    
    public String getStatistics(Cache<? extends Object, ? extends Object> cache) {
        try {
            StringBuffer b = new StringBuffer();
            ObjectName objectName = getJMXObjectName(cache);
            MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
            
            // printing retrieved cache statistics to console.
            for (CacheStatistics cacheStatistic : CacheStatistics.values()) {
                b.append(cacheStatistic + "=" + mBeanServer.getAttribute(objectName, cacheStatistic.name()) + "\n");
            }
            return b.toString();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    private ObjectName getJMXObjectName(Cache<? extends Object, ? extends Object> cache){
        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        
        // Refer to org.ehcache.jsr107.Eh107CacheStatisticsMXBean.Eh107CacheStatisticsMXBean(String, URI, StatisticsService)
        // and org.ehcache.jsr107.Eh107MXBean.Eh107MXBean(String, URI, String)
        final String beanName = "CacheStatistics";
        String cacheManagerName = sanitize(cache.getCacheManager().getURI().toString());
        String cacheName = sanitize(cache.getName());
        ObjectName objectName = null;
        try {
            objectName = new ObjectName(
                    "javax.cache:type=" + beanName + ",CacheManager=" + cacheManagerName + ",Cache=" + cacheName);
        }
        catch (MalformedObjectNameException e) {
            throw new CacheException(e);
        }
        
        if(!mBeanServer.isRegistered(objectName)){
           throw new CacheException("No MBean found with ObjectName => " + objectName.getCanonicalName());
        }
        
        return objectName;
    }
    
    private String sanitize(String string) {
        return ((string == null) ? "" : string.replaceAll(",|:|=|\n", "."));
    }
    
    public <K extends Object, V extends Object> long getSize(Cache<K, V> cache) {
        Iterator<Cache.Entry<K, V>> itr = cache.iterator();
        long count = 0;
        while(itr.hasNext()){
            itr.next();
            count++;
        }
        return count;
    }
    
    public <K extends Object, V extends Object> String dump(Cache<K, V> cache) {
        Iterator<Cache.Entry<K, V>> itr = cache.iterator();
        StringBuffer b = new StringBuffer();
        while(itr.hasNext()){
            Cache.Entry<K, V> entry = itr.next();
            b.append(entry.getKey() + "=" + entry.getValue() + "\n");
        }
        return b.toString();
    }

    /**
     * Defining cache statistics parameters as constants.
     */
    private enum CacheStatistics {
        CacheHits, CacheHitPercentage,
        CacheMisses, CacheMissPercentage,
        CacheGets, CachePuts, CacheRemovals, CacheEvictions,
        AverageGetTime, AveragePutTime, AverageRemoveTime
    }   
    
}

The main program to test caching

MainApp.java
package com.rakesh.caching;

import java.util.Date;

import javax.cache.Cache;

public class MyMain {

    public static void main(String[] args) throws Exception {
        new MyMain().doMain();
    }
    
    private void doMain() throws Exception{
        Cache<Integer, Person> personCache = AppCacheManager.getInstance().getPersonCache();
        Person p;
        for(int i=1; i<=7; i++){
            p = new Person(i, "firstName-"+1, "lastName-"+i);
            personCache.put(p.getPersonId(), p);
        }
        
        System.out.println("Entries in the Cache at: " + new Date());
        System.out.println("--------------------");
        for(int i=1; i<=7; i++){
            p = new Person(i, "firstName-"+1, "lastName-"+i);
            System.out.println("Key: " + i + ", Value: " +personCache.get(i));
        }
        
        System.out.println("\nWaiting for 40 seconds\n");
        //Wait for 40 seconds so that we reach cache expiry threshold which is 30 seconds
        Thread.sleep(1000*40);
        System.out.println("\nAfter 40 seconds wait\n");
        
        System.out.println("Entries in the Cache at: " + new Date());
        System.out.println("--------------------");
        for(int i=1; i<=7; i++){
            p = new Person(i, "firstName-"+1, "lastName-"+i);
            System.out.println("Key: " + i + ", Value: " +personCache.get(i));
        }
        
        System.out.println("Cache Usage Statitics");
        System.out.println("--------------------");
        System.out.println(AppCacheManager.getInstance().getStatistics(personCache));
        
    }

}

Here is output of main program

Output
Cache Initialized
Entries in the Cache at: Tue Jan 09 22:10:01 EST 2018
--------------------
Key: 1, Value: null
Key: 2, Value: null
Key: 3, Value: Person [personId=3, firstName=firstName-1, lastName=lastName-3]
Key: 4, Value: Person [personId=4, firstName=firstName-1, lastName=lastName-4]
Key: 5, Value: Person [personId=5, firstName=firstName-1, lastName=lastName-5]
Key: 6, Value: Person [personId=6, firstName=firstName-1, lastName=lastName-6]
Key: 7, Value: Person [personId=7, firstName=firstName-1, lastName=lastName-7]

Waiting for 40 seconds


After 40 seconds wait

Entries in the Cache at: Tue Jan 09 22:10:41 EST 2018
--------------------
Key: 1, Value: null
Key: 2, Value: null
Key: 3, Value: null
Key: 4, Value: null
Key: 5, Value: null
Key: 6, Value: null
Key: 7, Value: null

Cache Usage Statitics
--------------------
CacheHits=5
CacheHitPercentage=35.714287
CacheMisses=9
CacheMissPercentage=64.28571
CacheGets=14
CachePuts=7
CacheRemovals=0
CacheEvictions=2
AverageGetTime=91.180786
AveragePutTime=731.2153
AverageRemoveTime=0.0

Explanation

As you can see we added 7 items in the cache. Since cache max entry is set to 5, we found only 5 entries in the cache, 2 are null which means those are not present in the cache. After waiting for 40 seconds we checked cache again. Based on Time-To-Live of 30 seconds all entries are expired and nothing is found in the cache. Note the self explanatory cache usage statistics in the end.

You can download all code from github https://github.com/rakeshprajapati1982/ehcache-jcache

Please share it and help others if you found this blog helpful. Feedback, questions and comments are always welcome.

Further Reading

2 Comments

Comments

%d bloggers like this: