How to build and clear a reference data cache with singleton EJBs and MBeans
In one of my projects I had a requirement to load reference data from several sources in a Java EE 6 WebLogic environment, with EclipseLink as ORM framework. Since I couldn’t find an annotation in the Java EE world comparable to the sweet @Cacheable from Spring YET, I had to write my “own” caching solution. Although reference data barely changes over time, one extra requirement was to be able to clear the cache from exterior. So here it goes…
1. Cache
This was supposed to be a read-only cache with the possibility to flush it from exterior. I wanted to have the cache as a sort of a wrapper on the service providing the actual reference data for the application – AOP style with code!
1.1. Interface
Simple cache interface for reference data
@Local public interface ReferenceDataCache { /** * Returns all reference data required in the application */ ReferenceData getReferenceData(); /** * evict/flush all data from cache */ void evictAll(); }
The caching functionality defines two simple methods:
getReferenceData()
– which caches the reference data gathered behind the scenes from all the different sourcesevictAll()
– method called to completely clear the cache
1.2. Implementation
Simple reference data cache implementation with @Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) @Singleton public class ReferenceDataCacheBean implements ReferenceDataCache { private static final String ALL_REFERENCE_DATA_KEY = "ALL_REFERENCE_DATA"; private ConcurrentHashMap<String, Object> refDataCache = null; @EJB ReferenceDataService referenceDataService; @PostConstruct public void initialize(){ this.refDataCache = new ConcurrentHashMap<>(); } @Override @Lock(LockType.READ) public ReferenceData getReferenceData() { if(refDataCache.containsKey(ALL_REFERENCE_DATA_KEY)){ return refDataCache.get(ALL_REFERENCE_DATA_KEY); } else { ReferenceData referenceData = referenceDataService.getReferenceData(); refDataCache.put(ALL_REFERENCE_DATA_KEY, referenceData); return referenceData; } } @Override public void evictAll() { refDataCache.clear(); } .......... }
Note:
@Singleton
– probably the most important line of code in this class. This annotation specifies that there will be exactly one singleton of this type of bean in the application. This bean can be invoked concurrently by multiple threads. It comes also with a@PostConstruct
annotation. This annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization – in our case is to initialize the “cache”(hash map)- the
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
declares a singleton session bean’s concurrency management type. By default it is set toContainer
. I use it here only to highlight its existence. The other optionConcurrencyManagementType.BEAN
specifies that the bean developer is responsible for managing concurrent access to the bean instance. - the actual “cache” is a
ConcurrentHashMap
which hasString
based keys and storesObject
s. This is being held in memory due to the singleton nature of the bean - the injected
ReferenceDataService
is a@Stateless
@EJB
that, behind the scenes, gathers the reference data from the different sources - the getReferenceData() method implementation is very simple – it checks whether the
ConcurrentHashMap
has an entry with the String key specified as constant “ALL_REFERENCE_DATA
“. If so this will be retrieved from memory, otherwise will be loaded by the service bean - the
@Lock(LockType.READ)
specifies the concurrency lock type for singleton beans with container-managed concurrency. When set toLockType.READ
, it enforces the method to permit full concurrent access to it (assuming no write locks are held). This is exactly what I wanted, as I only need to do read operations. The other more conservative option@Lock(LockType.WRITE)
, which is the DEFAULT by the way, enforces exclusive access to the bean instance. This should make the method slower in a highly concurrent environment… - the
evictAll()
method, just removes all the elements from the hash map.
2. Flushing the cache
The second part of this post will deal with the possibilities of clearing the cache. Since the cache implementation is an enterprise java bean, we can call it either from an MBean or, why not, from a web service.
2.1. MBean
If you are new to Java Management Extensions (JMX) , which is a Java technology that supplies tools for managing and monitoring applications, system objects, devices (e.g. printers) and service oriented networks. Those resources are represented by objects called MBeans (for Managed Bean), I highly recommend you start with this tutorial Trail: Java Management Extensions (JMX)
2.1.1. Interface
The method exposed will only allow the reset of the cache via JMX:
CacheRest MBean
@MXBean public interface CacheResetMXBean { void resetReferenceDataCache(); }
“An MXBean is a type of MBean that references only a predefined set of data types. In this way, you can be sure that your MBean will be usable by any client, including remote clients, without any requirement that the client have access to model-specific classes representing the types of your MBeans. MXBeans provide a convenient way to bundle related values together, without requiring clients to be specially configured to handle the bundles.” [4]
2.1.2. Implementation
MBean implementation of CacheReset
@Singleton @Startup public class CacheReset implements CacheResetMXBean { private MBeanServer platformMBeanServer; private ObjectName objectName = null; @EJB ReferenceDataCache referenceDataCache; @PostConstruct public void registerInJMX() { try { objectName = new ObjectName("org.codingpedia.simplecacheexample:type=CacheReset"); platformMBeanServer = ManagementFactory.getPlatformMBeanServer(); //unregister the mbean before registerting again Set<ObjectName> existing = platformMBeanServer.queryNames(objectName, null); if(existing.size() > 0){ platformMBeanServer.unregisterMBean(objectName); } platformMBeanServer.registerMBean(this, objectName); } catch (Exception e) { throw new IllegalStateException("Problem during registration of Monitoring into JMX:" + e); } } @Override public void resetReferenceDataCache() { referenceDataCache.evictAll(); } }
Note:
- as mentioned the implementation only calls the
evictAll()
method of the injected singleton bean described in the previous section - the bean is also defined as
@Singleton
- the
@Startup
annotation causes the bean to be instantiated by the container when the application starts – eager initialization - I use again the
@PostConstruct
functionality. Here this bean is registered in JMX, checking before if theObjectName
is used to remove it if so…
2.2. Rest service call
I’ve also built in the possibility to clear the cache by calling a REST resource. This happends when you execute a HTTP POST on the (rest-context)/reference-data/flush-cache:
Rest calls on Reference data cache
@Path("/reference-data") public class ReferenceDataResource { @EJB ReferenceDataCache referenceDataCache; @POST @Path("flush-cache") public Response flushReferenceDataCache() { referenceDataCache.evictAll(); return Response.status(Status.OK).entity("Cache successfully flushed").build(); } @GET @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) public Response getReferenceData(@QueryParam("version") String version) { ReferenceData referenceData = referenceDataCache.getReferenceData(); if(version!=null && version.equals(referenceData.getVersion())){ return Response.status(Status.NOT_MODIFIED).entity("Reference data was not modified").build(); } else { return Response.status(Status.OK) .entity(referenceData).build(); } } }
Notice the existence of the version query parameter in the @GET
getReferenceData(...)
method. This represents a hash on the reference data and if it hasn’t modified the client will receive a 304 Not Modified HTTP Status. This is a nice way to spare some bandwidth, especially if you have mobile clients. See my post Tutorial – REST API design and implementation in Java with Jersey and Spring, for a detailed discussion around REST services design and implementation.
Note:
In a clustered environment, you need to call resetCache(…) on each JVM where the application is deployed, when the reference data changes.
Well, that’s it. In this post we’ve learned how to build a simple cache with Java EE annotations. Of course you can easily extend the cache functionality to offer more granular access/clearing to cached objects. Don’t forget to use LockType.WRITE
for the clear methods in this case…
Reference: | How to build and clear a reference data cache with singleton EJBs and MBeans from our JCG partner Adrian Matei at the Codingpedia.org blog. |
ReferenceDataCacheBean – is awful. In highly concurrent environment it will invoke referenceDataService.getReferenceData() many times.
Hi Nietzschean,
Do you mean, that initially, there could be many users getting the data from cache at the same time? This is highly improbable in an enterprise environment.
Adrian
Disclaimer: “This sort of cache implementation is not suitable for facebook like requirements”
Hi Adrian. Thanks for reply. If it’s highly improbable, so we can simplify code to
private Object refDataCache = null;
@Override
@Lock(LockType.READ)
public ReferenceData getReferenceData() {
if(refDataCache == null) {
refDataCache = referenceDataService.getReferenceData();
}
return refDataCache”
}
I don’t understand, why do you use ConcHashMap to store only single entry?
Hi Nietzschean,
You are completely right. For one single value there’s no reason to use a ConcHashMap. I used it though, to have the possibility to store other values in the reference data rather than the single object presented in the example, which I eventually did. I will add that to my original post.
Thanks for noticing/mentioning it
Adrian
Hi Andrian, thank you for clarification.
Very nice. We had to do something similar. Using spring DI we have several different types of caches and allow the use of a cache of basic objects, a clustered cache, a riak cache, or database cache using JPA. We have all the caches be persistent in a back ground process, because we are trying to maintain the latest state and want to reload it after a possible app restart.
Your article has given me some more ideas to add to our stuff.
This is why I LOVE Java Code Geeks.
Hi Stephen,
I am glad I could inspire you :)
Adrian