EhCache3 as JCache (JSR-107) Implementation with Cache Statistics
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
About JCache
Some of the implementations are -
For comprehensive list of JCache implementations click here
Subtle Benefits
- 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
- If you upgrade underlying implementation it should not be breaking your existing code
- Access live cache usage statistics at runtime using JMX
- You are using Standard API Specification :-)
JCache with EhCache as Implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
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
Comments
Post a Comment