Published on dev2dev (http://dev2dev.bea.com/)
 http://dev2dev.bea.com/pub/a/2006/05/declarative-caching.html
 See this if you're having trouble printing code examples

Declarative Caching Services for Spring

by Alex Ruiz
05/19/2006

Abstract

Scalability, reliability, and high performance are must-have requirements in modern J2EE applications. Regardless of the type of client, request processing usually involves actions that have a negative effect on performance, such as information gathering from disparate data sources and execution of complex calculations, to say the least. Caching is one of the most essential practices that improve the performance of enterprise applications. Every application has its own caching requirements that must be constantly adjusted to ensure that no performance degradation occurs. Enterprise applications need an easy way to add and tune caching functionality without touching the application's code. This article introduces a code-free caching framework for Spring-based applications, Declarative Caching Services for Spring.

Caching Overview

In simple terms, caching involves the temporary storage of data that is expensive to retrieve, resulting in quick responses to clients by avoiding additional data retrieval from the original source. For example, caching can produce enormous performance gains in enterprise applications by avoiding calls from the application server to the database, which may involve network roundtrips if they are not co-located. With caching, application response time will be notably better because of reduced database workload and freed network bandwidth.

Distributed systems are excellent targets for caching services. Remote calls are particularly slow and consume precious network bandwidth, introducing performance issues. A call to a remote service starts with the creation of the request by marshalling its parameters into a format that can be transported over the wire, like XML or a byte stream. Once the request is received, it must be unmarshalled on the server side in order to start its processing. A similar procedure applies to the response generated by the service, which adds more overhead. Caching can dramatically increase performance of distributed applications by eliminating, or at least reducing, remote calls.

Regardless of its benefits, caching can add considerable complexity to the design, development, and deployment of enterprise applications. You should not apply caching without clear knowledge of the business requirements, and your decision should be backed up by test results and other solid evidence.

The following are basic guidelines for caching:

Implementing our own cache is not a simple task. You may have to deal with complexity issues like thread management, removal of stale data, and clustering support. Currently there are many high quality, third-party cache implementations, both open source and commercial, that can save you from the burden of creating, and more important, maintaining your own cache.

Declarative Caching Services for Spring

The Spring Framework is an open source application framework that provides, among other things, enterprise services to plain Java objects. Most of the applications created with Spring are multitier, data-centric enterprise applications with multiple concurrent users. Such applications may also integrate with external systems using some kind of remoting technology like remote EJBs, Web services, RMI, or CORBA, just to name a few. Enterprise applications are important to the core business and are expected to deliver high performance.

Declarative Caching Services for Spring provides declarative caching for Spring-powered applications. Declarative caching does not involve any programming and therefore it is a much easier and more rapid way of applying and tuning caching services. Configuration of caching services can be completely done in the Spring IoC container. Declarative caching:

Benefits of Declarative Caching Services

To better understand declarative caching and its benefits, let's look at the following code snippet. It is a simple implementation of a customer manager that retrieves customer information from a data source given the customer's ID. To improve performance, the Customer object retrieved from the data access layer is programmatically stored in a Coherence cache. In addition, to prevent the storage of stale data, the cache containing customer information is flushed when the information of a single customer is updated:

public class CustomerManager {
  private CustomerDao customerDao;
  private CacheKeyGenerator keyGenerator;    
  
  public Customer load(long customerId) {
    validateCustomerId(customerId);
    Customer customer = null;
    
    Serializable key = keyGenerator.generateKey(customerId);
    NamedCache cache = getCache();
    customer = (Customer) cache.get(key);
    if (customer == null) {
      customer = customerDao.load(customerId);
      cache.put(key, customer);
    }
    return customer;
  }

  public void update(Customer customer) {
    customerDao.update(customer);  
    Serializable key = keyGenerator.generateKey(customer.getId());
    NamedCache cache = getCache();
    cache.remove(key);
  }       
  
  private NamedCache getCache() {
    return CacheFactory.getCache("customerCache");
  }
  // rest of class implementation
}

Although the use of caching has improved the performance of the application, it also has added extra (and unnecessary) complexity. Programmatic use of caching raises the following issues:

Declarative caching encapsulates how caching is performed and eliminates any dependencies on the cache implementation from our Java code. After introducing declarative caching, the method from the previous example implements only its core requirement:

public class CustomerManager {
  private CustomerDao customerDao;
  
  public Customer load(long customerId) {
    validateCustomerId(customerId);
    return customerDao.load(customerId);
  }
  
  public void update(Customer customer) {
    customerDao.update(customer);    
  }  
  // rest of class implementation
}

The following XML fragment illustrates how caching services can be configured for the CustomerManager instance in the Spring IoC container, rather than in our Java code:

<bean id="customerDaoTarget"
  class="org.springmodules.cache.samples.dao.CustomerDao" />
  <!-- Properties -->
</bean>

<coherence:proxy id="customerDao" refId="customerDaoTarget">
  <coherence:caching methodName="load" cacheName="customerCache" />
  <coherence:flushing methodName="update" cacheNames="customerCache" />
</coherence:proxy>

<bean id="customerManager"
  class="org.springmodules.cache.samples.business.CustomerManager" />
  <property name="customerDao" ref="customerDao" />
</bean>

Declarative caching configuration effectively separates the caching functionality from the core requirements of our application, solving the problems mentioned above and providing the following benefits:

How Declarative Caching Services Work

Declarative Caching Services uses run-time method interception to deliver transparent, code-free caching and cache flushing to Spring-managed beans. Configuration simply involves selecting which methods should be intercepted and how caching services should be applied to those methods. The following is a quick overview of how declarative caching services work.

The activity diagram in Figure 1 shows how the caching method interceptor works:

Caching in action
Figure 1. Caching in action

The caching method interceptor first generates a unique key using the signature of the intercepted method and the provided parameter values. Then, the interceptor searches in the cache for an object stored under the generated key. If an object is found, it is immediately returned to the client, bypassing the execution of the intercepted method. If the interceptor does not find any value in the cache, it executes the intercepted method, stores the return value in the cache (using the generated key), and returns the cached object to the client. Keys for cache entries are automatically generated through pluggable key generators. Currently, only one strategy is provided, HashCodeCacheKeyGenerator, which creates unique keys based on the hash codes of the intercepted method and its parameters. Key generators can be created easily by implementing the interface CacheKeyGenerator.

The activity diagram in Figure 2 shows how the cache-flushing method interceptor works:

Cache flushing in action
Figure 2. Cache flushing in action

The cache-flushing method interceptor can be configured to flush a cache before or after the intercepted method is executed. The interceptor can flush one or more regions of a cache, or the whole cache, depending on how it is configured and which cache provider is being used.

Configuring Declarative Caching Services

Declarative Caching Services provides a short, simple, and straightforward configuration based on Spring 2.0's support for XML namespaces. Each cache provider has its own namespace that needs to be declared in the Spring IoC container configuration. The following example illustrates how to declare the coherence namespace and how to specify its location:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:coherence="http://www.springmodules.org/schema/coherence"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springmodules.org/schema/coherence http://www.springmodules.org/schema/cache/springmodules-tangosol.xsd">

Before setting up the Spring beans that will be the target of caching services, we need to include a couple system-wide configuration settings that influence how caching services behave:

<coherence:config failQuietly="false" 
                  serializableFactory="XSTREAM" />

Caching is an orthogonal concern with the primary goal of improving the performance of an application transparently. Caching should not affect the core business logic or change the behavior of an application. If caching fails, the application should not stop its execution, especially in production. On the other hand, in certain cases you would want your application to stop running for any major or minor malfunction, including caching failures—for example, when performing integration tests during development and you must find any configuration error before placing your application in production.

The property failQuietly gives developers the power to decide whether or not a caching failure should stop the execution of an application. When this property is set to true, any caching failure will not affect the application at all. Its default value is false.

Once caching is up and running, you are going to find cases when you need to store cached objects in the file system or replicate changes in the cache across nodes in a cluster. Cache providers are capable of performing those functions with the help of Java serialization, which implies that the objects to be stored in the cache should implement the interface java.io.Serializable.

Such a requirement imposes a problem when you need to store in the cache objects that are not serializable and that you do not have control of (for example, objects generated by JAXB.) The property serializableFactory provides a solution to this problem by forcing serialization on non-serializable objects. Only one strategy is provided, XStreamSerializableFactory which uses XStream to serialize objects to XML before storing them in the cache. A new strategy using JBoss Serialization will be ready by the time you read this article.

An alternative to force object serialization is to use AspectJ's inter-type declarations to make classes implement java.io.Serializable. The following example assumes the class Customer is not serializable and needs to be stored in a clustered cache:

public aspect SerializationAspect {
  declare parents : Customer implements java.io.Serializable;
}

For more information about inter-type declarations or AspectJ in general, refer to the AspectJ documentation.

Declarative Caching Services provides different configuration strategies giving developers the opportunity to choose the strategy that best suites the needs of their applications. The following sections describe each of the provided configuration strategies.

Per-bean configuration

This configuration strategy is simple and very easy to set up. Configuration of caching services is performed inside a proxy element which has a reference to a single Spring-managed bean. You must supply the id of the bean to be referred in the attribute refId. You also need to choose the methods to be intercepted to apply caching services. The elements caching and flushing specify which methods should trigger data caching and which methods should trigger cache flushing respectively. These two elements provide the attribute methodName which specifies the name of the method to intercept. These elements contain additional attributes, unique for each cache provider, that specify how caching and cache flushing should be performed by that particular cache. The following example illustrates per-bean configuration using Coherence:

<bean id="customerDaoTarget"
  class="org.springmodules.cache.samples.dao.CustomerDao" />
  <!-- Properties -->
</bean>

<coherence:proxy id="customerDao" refId="customerDaoTarget">
  <coherence:caching methodName="load" 
                 cacheName="customerCache" />
  <coherence:flushing methodName="update" 
                 cacheNames="customerCache" when="after" />
</coherence:proxy>

Note that you should qualify every cache-related XML element with the namespace of the chosen cache provider. In our example, we set up caching services to store the return value of the method CustomerDao.load in the Coherence cache with name "customerCache" and flush the cache with the same name after the execution of the method CustomerDao.update. It is important to mention that to configure beans that have dependencies on a CustomerDao, you should use the id of the proxy element, not the id of the original bean:

<bean id="customerManager"
 class="org.springmodules.cache.samples.business.CustomerManager"/>
 <property name="customerDao" ref="customerDao" />
</bean>

As an alternative, you can declare the bean definition inside the proxy element, which results in a more compact configuration:

<coherence:proxy id="customerDao">
  <bean
    class="org.springmodules.cache.samples.dao.CustomerDao" />
    <!-- Properties -->
  </bean>

  <coherence:caching methodName="load" 
           cacheName="customerCache" />
  <coherence:flushing methodName="update" 
           cacheNames="customerCache" when="after" />
</coherence:proxy>

Per-bean configuration offers an easy way to set up caching services. Since you need to define a beanRef element per bean, it is better to use this approach when you have an application with a small number of objects to apply caching services to.

Source-level metadata attributes

Although per-bean configuration provides a straightforward way to configure caching services, you may end up having a great amount of configuration-related XML if your application contains a fairly large number of Spring-managed beans. To simplify configuration you can "decorate" your application with source-level metadata attributes indicating which methods should be intercepted to apply caching services. Declarative caching supports Commons Attributes and J2SE 5.0 Annotations.

Commons Attributes allows you to add metadata attributes to applications using J2SE 1.4 or earlier. Because the Java compiler does not recognize this type of metadata attribute, an extra compilation process is required. Compilation of Commons Attributes metadata generates source code that must be compiled with the application's source files. There is no IDE support for Commons Attributes metadata, which means common software development practices like refactoring are not supported either. The following example illustrates the use of Commons Attributes. Note the following configuration is equivalent to the one in the previous section:

public class CustomerDao {
  
  /**
   * @@Cached(modelId="cachingModel")
   */
  public Customer load(long customerId) {
    // method implementation
  }
  
  /**
   * @@FlushCache(modelId="flushingModel")
   */
  public void update(Customer customer) {
    // method implementation
  }  
  // rest of class implementation
}

Source-level metadata attributes are restricted to describe whether caching services should be applied, instead of describing how caching should occur. The how is described in the Spring IoC container. In our example, metadata only indicates that that the return value of the method CustomerDao.load should be cached, following the settings specified in the model with id cachingModel. The model, which is declared in the Spring IoC container, contains the cache-specific settings that describe how values are stored in the cache. The same rule applies to cache flushing. Here is the declaration of the models referenced by the metadata attributes:

<coherence:commons-attributes>
  <coherence:caching id="cachingModel" 
            cacheName="customerCache" />
  <coherence:flushing id="flushingModel" 
            cacheNames="customerCache" when="after" />
</coherence:commons-attributes>

<bean id="customerDao"
  class="org.springmodules.cache.samples.dao.CustomerDao" />
  <!-- Properties -->
</bean>

The model with id cachingModel indicates that objects should be stored in the Coherence cache with name "customerCache." In a similar way, the model with id flushingModel indicates the same cache should be flushed after the intercepted method is executed. Any changes related to the cache provider will be performed only in the Spring IoC container without affecting cache-related metadata attributes. Such changes do not require recompilation of your Java source code. Also note that to process the cache-related metadata, you also need to declare in the Spring container an instance of the class decorated with source-level metadata (the bean customerDao in our example.)

J2SE 5.0 Annotations are a more structured format for adding metadata attributes to your applications. Because Annotations are a built-in language feature, the Java compiler identifies them as another language construct, overcoming the problems related to Commons Attributes. Configuration of caching services using J2SE 5.0 Annotations is almost identical to the configuration using Commons Attributes. In fact, it is a matter of replacing a couple of lines in the Java source code:

public class CustomerDao {
  
  @Cacheable(modelId = "cachingModel")
  public Customer load(long customerId) {
    // method implementation
  }

  @CacheFlush(modelId = "flushingModel")  
  public void update(Customer customer) {
    // method implementation
  }  
  // rest of class implementation
}

For more information about J2SE 5.0 Annotations, refer to the JDK 1.5 documentation. Switching from Commons Attributes to J2SE 5.0 Annotations in the Spring IoC container only requires replacing the element commons-attributes with the element annotations:

<coherence:annotations>
  <coherence:caching id="cachingModel" 
          cacheName="customerCache" />
  <coherence:flushing id="flushingModel" 
          cacheNames="customerCache" when="after" />
</coherence:annotations>

<bean id="customerDao"
  class="org.springmodules.cache.samples.dao.CustomerDao" />
  <!-- Properties -->
</bean>

Caching services using source-level metadata attributes are set up only once in the Spring IoC container. You can add as many beans as you need without requiring any extra configuration. This makes configuration with source-level metadata attributes an ideal solution when you need to apply caching services to a large number of objects.

BeanNameAutoProxyCreator

The BeanNameAutoProxyCreator, like the metadata attributes approach, allows you to configure declarative services to a large number of beans in the Spring IoC container without needing any extra configuration. Unlike the metadata attributes approach, BeanNameAutoProxyCreator does not require any changes to the Java source code.

BeanNameAutoProxyCreator allows you to apply one or more method interceptors to a large number of beans. You only need to configure your caching settings once and then provide the name of the bean to the proxy creator. The following example shows how to specify the caching-related method interceptors in a BeanNameAutoProxyCreator:

<coherence:methodMapInterceptors 
  cachingInterceptorId="cachingInterceptor"
  flushingInterceptorId="flushingInterceptor">
  <coherence:caching
    methodFQN="org.springmodules.cache.samples.dao.CustomerDao.load"
    cacheName="customerCache" />
  <coherence:flushing
    methodFQN="org.springmodules.cache.samples.dao.CustomerDao.update"
    cacheNames="customerCache" />
</coherence:methodMapInterceptors>

<bean
  class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
  <property name="beanNames">
    <list>
      <idref local="customerDao" />
    </list>
  </property>
  <property name="interceptorNames">
    <list>
      <value>cachingInterceptor</value>
      <value>flushingInterceptor</value>
    </list>
  </property>
</bean>

<bean id="customerDao"
  class="org.springmodules.cache.samples.dao.CustomerDao" />
  <!-- Properties -->
</bean>

As you may have guessed, the configuration presented above does exactly the same thing as the configuration shown in the sections explaining per-bean configuration and source-level metadata attributes. BeanNameAutoProxyCreator is provided by the Spring Framework. To learn more, refer to the Spring online documentation.

Download

Download the sample code used in this article: caching.zip

Conclusion

Although caching can dramatically improve the performance of enterprise applications, it also can introduce considerable complexity to the application life cycle. This article introduces Declarative Caching Services for Spring, a framework that encapsulates such complexity and provides code-free, transparent caching and cache flushing to Spring-powered applications.

In the spirit of Spring, declarative caching provides out-of-the-box support for various open source and commercial cache providers. It also offers different configuration strategies. It is up to us, the developers, to choose the combination that best suits the needs of our projects.

With declarative caching, developers can concentrate on the core requirements of their applications which results in code that is easier to understand, maintain, test, and reuse. Because caching services are configured in the Spring IoC container, they can be added only when truly needed, without making system-wide changes.

References

Alex Ruiz is a Software Engineer in the development tools organization at Oracle.


Return to dev2dev.