Friday, April 3, 2015

Caching of web content with Spring's cache manager


I this post, I would like to show basics how to cache and manage the caching of web content with Spring's CacheManager, @Cacheable and JMX annotations. Imagine a web shop which fetches some content, such as header, footer, teasers, main navigation, from a remote WCMS (Web Content Management System). The fetching may e.g. happen via a REST service. Some content is rarely updated, so that it makes sense to cache it in the web application due to performance reasons.

Getting Started

First, we need a cache provider. A good cache provider would be EhCache. You need to add the EhCache as dependency to your project. You also need to configure ehcache.xml which describes, among other things, the cache name(s), where and how long the cached content is stored. Please refer to the documentation to learn how the ehcache.xml looks like. The central class of the EhCache is the net.sf.ehcache.CacheManager. With help of this class you can add or remove any objects to / from the cache and much more programmatically. Objects can be cached in memory, on the disk or somewhere else.

The Spring framework provides a CacheManager backed by the EhCache - org.springframework.cache.CacheManager. It also provides the @Cacheable annotation. From the documentation: "As the name implies, @Cacheable is used to demarcate methods that are cacheable - that is, methods for whom the result is stored into the cache so on subsequent invocations (with the same arguments), the value in the cache is returned without having to actually execute the method. In its simplest form, the annotation declaration requires the name of the cache associated with the annotated method". We will use the JMX annotations as well. These are Spring's annotations @ManagedResource and @ManagedOperation. Why do we need those? We need them to be able to clear cache(s) via an JMX console. Why? Well, e.g. the underlying data have been changed, but the cache is not expired yet. The outdated data will be still read from the cache and not from the native source. The beans annotated with @ManagedResource will be exposed as JMX beans and methods annotated by @ManagedOperation can be executed via an JMX console. I recommend to use JMiniX as a simple JMX entry point. Embedding JMiniX in a webapp is done simply by declaring a servlet. Parametrized methods are supported as well, so that you can even input some real values for method's parameters and trigger the execution with these values.

How to do it...

Now we are ready to develop the first code. We need a service which communicates with a remote backend in order to fetch various contents from the WCMS. Let's show exemplary a basic code with one method fetchMainNavigation(). This method fetches the structure of the main navigation menu and converts the structure to a DTO object NavigationContainerDTO (model class for the menu). The whole business and technical logic is resided in the bean MainNavigationHandler. This logic is not important for this blog post. The method fetchMainNavigation() expects two parameters: locale (e.g. English or German) and variant (e.g. B2C or B2B shop).
@Component
public class WCMSServiceImpl extends BaseService implements WCMSService {
 
    // injection of Spring's CacheManager is needed for @Cacheable
    @Autowired
    private CacheManager cacheManager;
 
    @Autowired
    private MainNavigationHandler mainNavigationHandler;
 
    ...
 
    @Override
    @Cacheable(value = "wcms-mainnavigation",
                        key = "T(somepackage.wcms.WCMSBaseHandler).cacheKey(#root.methodName, #root.args[0], #root.args[1])")
    public NavigationContainerDTO fetchMainNavigation(Locale lang, String variant) {
        Object[] params = new Object[0];
        if (lang != null) {
            params = ArrayUtils.add(params, lang);
        }
        if (variant != null) {
            params = ArrayUtils.add(params, variant);
        }
 
        return mainNavigationHandler.get("fetchMainNavigation", params);
    }
}
The method is annotated with the Spring's annotation @Cacheable. That means, the returned object NavigationContainerDTO will be cached if it was not yet available in the cache. The next fetching will return the object from the cache until the cache gets expired. The caching occurs according to the settings in the ehcache.xml. Spring's CacheManager finds the EhCache provider automatically in the classpath. The value attribute in @Cacheable points to the cache name. The key attribute points to the key in the cache the object can be accessed by. Since caches are essentially key-value stores, each invocation of a cached method needs to be translated into a suitable key for the cache access. In a simple case, the key can be any static string. In the example, we need a dynamic key because the method has two parameters: locale and variant. Fortunately, Spring supports dynamic keys with SpEL expression (Spring EL expression). See the table "Cache SpEL available metadata" for more details. You can invoke any static method generating the key. Our expression T(somepackage.wcms.WCMSBaseHandler).cacheKey(#root.methodName, #root.args[0], #root.args[1]) means we call the static method cacheKey in the class WCMSBaseHandler with three parameters: the method name, first and second arguments (locale and variant respectively). This is our key generator.
public static String cacheKey(String method, Object... params) {
    StringBuilder sb = new StringBuilder();
    sb.append(method);

    if (params != null && params.length > 0) {
        for (Object param : params) {
            if (param != null) {
                sb.append("-");
                sb.append(param.toString());
            }
        }
    }

    return sb.toString();
}
Let's show how the handler class MainNavigationHandler looks like. This is just a simplified example from a real project.
@Component
@ManagedResource(objectName = "bean:name=WCMS-MainNavigation",
                                description = "Manages WCMS-Cache for the Main-Navigation")
public class MainNavigationHandler extends WCMSBaseHandler<NavigationContainerDTO, Navigation> {

    @Override
    NavigationContainerDTO retrieve(Objects... params) {
        // the logic for content retrieving and DTOs mapping is placed here
        ...
    }
 
    @ManagedOperation(description = "Delete WCMS-Cache")
    public void clearCache() {
        Cache cache = cacheManager.getCache("wcms-mainnavigation");
        if (cache != null) {
            cache.clear();
        }
    } 
}
The CacheManager is also available here thanks to the following injection in the WCMSBaseHandler.
@Autowired
private CacheManager cacheManager;
@ManagedResource is the Spring's JMX annotation, so that the beans are exported as JMX MBean and become visible in the JMX console. The method to be exported should be annotated with @ManagedOperation. This is the methode clearCache() which removes all content for the main navigation from the cache. "All content" means an object of type NavigationContainerDTO. The developed WCMS service can be now injected into a bean on the front-end side. I already blogged about how to build a multi-level menu with plain HTML and shown the code. This is exactly the main navigation from this service.

There is more...

The scanning of JMX annotations should be configured in a Spring's XML configuration file.
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
    <property name="server" ref="mbeanServer"/>
    <property name="assembler" ref="assembler"/>
    <property name="namingStrategy" ref="namingStrategy"/>
    <property name="autodetect" value="true"/>
</bean>
The JMX console of the JMiniX is reachable under the http(s)://:/mct/webshop/admin/jmx/ A click on the execute button of the clearCache() method triggers the cache clearing.

3 comments:

  1. Just a thought. As of Spring Framework 4.1, you can specify a KeyGenerator implementation per cache operation which is less verbose than the spel expression (and provide access to the same data).

    I wouldn't use the method name as part of the key. What you cache is supposed to be identified by the method arguments, not the method itself.

    ReplyDelete
    Replies
    1. The method name as part of the key is not necessary here, but it is necessary if you have just one cache with several methodes and the methods have the same parameters.

      Delete
    2. Key as SpEL is not bad because you can define and use it as constant, e.g.

      @Cacheable(value = WCMSConstants.MAIN_NAVIGATION, key = WCMSConstants.KEY_TWO_PARAMS)

      This is exactly how I use value and key in @Cacheable.

      Delete

Note: Only a member of this blog may post a comment.