Open source Java projects: Java Caching System

A distributed caching system for enterprise applications

Enterprise Java specialist Steve Haines joins the Open source Java projects series this month with an introduction to Java Caching System (JCS), a robust enterprise-level caching solution. Steve starts with a quick introduction to caching, discussing the criteria for determining if objects should be cached and whether your application would benefit from a cache. He then shows you how to configure JCS and use it to build a caching application.

The Java Caching System (JCS) is a robust open source caching product released through the Apache Jakarta subproject. It provides the standard features that you would expect of a cache system, such as in-memory caching and algorithms for selectively removing objects from the cache. It also offers more-advanced features, such as indexed disk caching and support for distributed caches.

A JCS cache has a map-like structure in which data is stored in the cache as a name-and-value pair. JCS partitions the cache into regions. Each region has its own configuration as well as its own set of name-value pairs. Each region can:

  • Be sized differently
  • Be implemented differently
  • Contain different data

The keys (the names in the name-and-value pairs) in one region can be the same as keys in other regions. This is important because it enables you to maintain separate caches for different objects all within the same JVM -- and all defined in a single properties file.

Open source licenses

Each of the open source Java projects covered in this series is subject to a license, which you should understand before integrating the project with your own projects. JCS is subject to the Apache License; see Resources to learn more.

This article explores JCS by first showing you how to obtain and install the current release. I'll then explain what a cache is, why you might use one, and whether or not it is the right solution for a specific application. Next, you'll delve into the JCS properties file, which is the best route to understanding JCS. Finally, you'll build a sample caching application that uses JCS.

Get started with JCS

You can download JCS from the downloads page of the JCS project site. As of this writing, the latest version is 1.3. Download the binary distribution (either as a TAR file on Unix systems or a ZIP file on Windows) and decompress it to a local directory on your computer.

The root of the installation directory contains jcs-1.3.jar, which you must add to your CLASSPATH before compiling and running a JCS applications.

Class documentation goldmine

Throughout this article, as well as in your own independent studies, you'll find that the JCS docs directory is an invaluable resource for information about JCS, including the API documentation. The robust Javadoc document is your authority for understanding how to use JCS classes.

You will need two dependencies:

  • Commons Logging
  • Concurrent

From Commons Logging, add commons-logging.jar to your CLASSPATH.

A quick caching primer

A cache is designed to hold objects, typically in memory, for immediate access by an application. An application interacts differently with a cache from the way it interacts with external storage solutions. Typically, an application obtains a connection to a database, executes a query across a network, and parses the results as they are returned. A cache maintains a collection of readily available objects in a robust map-like structure that does not require a network call. Enterprise Java application performance improves exponentially when it accesses reusable objects in a cache after loading them from a database, rather than making remote database calls.

If your application has a manageable number of objects that are frequently accessed, then a cache can probably improve its performance. Java applications are constrained by available resources in the JVM, the most precious of which is memory. It makes no sense to take memory away from a JVM to hold objects that are rarely accessed. It's probably better to load an object that's accessed once every few hours as it's needed and leave enough free memory for other resources. On the other hand, it's better to load objects that are accessed several times a minute -- or even several times an hour -- into a cache and serve them from memory, rather than make a remote call every time the object is needed. If the number of objects your application accesses frequently is manageable within the available memory, then it is a good candidate for caching. But if it accesses millions of objects frequently, then it still might be in an application's best interest to load objects as needed rather than use 75 percent of a JVM's heap to host the cache.

Caching versus pooling

Confusion about the distinction between a cache and a pool often emerges in discussions about caching. Which objects should be cached and what objects should be pooled? The answer lies in the nature of the objects themselves. If an object maintains state, it should be cached. Stateless objects should be pooled. As an analogy, consider two activities: buying food at a supermarket and picking a child up from school. Any cashier can check out any customer at the supermarket; it doesn't matter which cashier you get, so cashiers should be pooled. When you pick up your child from school, you want your child, not someone else's, so children should be cached.

Extrapolating this idea out to enterprise Java, resources such as database connections and business processing beans should be pooled, whereas objects such as employees, documents, and widgets should be cached. It doesn't matter which database connection your application obtains from a connection pool -- they all do the same thing -- but if you want to give yourself a pay raise, it is important that you obtain your employee object.

Understanding JCS regions

Using JCS is actually quite simple, but you need some foundational knowledge about how JCS defines cache regions and how they can be configured. The JCS properties file is the logical place to start understanding JCS. Listing 1 shows a sample JCS properties file.

Listing 1. A JCS properties file (cache.ccf)

# DEFAULT CACHE REGION
jcs.default=DC
jcs.default.cacheattributes=
     org.apache.jcs.engine.CompositeCacheAttributes
jcs.default.cacheattributes.MaxObjects=1000
jcs.default.cacheattributes.MemoryCacheName=
     org.apache.jcs.engine.memory.lru.LRUMemoryCache
jcs.default.cacheattributes.UseMemoryShrinker=true
jcs.default.cacheattributes.MaxMemoryIdleTimeSeconds=3600
jcs.default.cacheattributes.ShrinkerIntervalSeconds=60
jcs.default.elementattributes=org.apache.jcs.engine.ElementAttributes
jcs.default.elementattributes.IsEternal=false
jcs.default.elementattributes.MaxLifeSeconds=21600
jcs.default.elementattributes.IdleTime=1800
jcs.default.elementattributes.IsSpool=true
jcs.default.elementattributes.IsRemote=true
jcs.default.elementattributes.IsLateral=true

# PREDEFINED CACHE REGIONS
jcs.region.musicCache=DC
jcs.region.musicCache.cacheattributes=
     org.apache.jcs.engine.CompositeCacheAttributes
jcs.region.musicCache.cacheattributes.MaxObjects=1000
jcs.region.musicCache.cacheattributes.MemoryCacheName=
     org.apache.jcs.engine.memory.lru.LRUMemoryCache
jcs.region.musicCache.cacheattributes.UseMemoryShrinker=true
jcs.region.musicCache.cacheattributes.MaxMemoryIdleTimeSeconds=3600
jcs.region.musicCache.cacheattributes.ShrinkerIntervalSeconds=60
jcs.region.musicCache.cacheattributes.MaxSpoolPerRun=500
jcs.region.musicCache.elementattributes=
     org.apache.jcs.engine.ElementAttributes
jcs.region.musicCache.elementattributes.IsEternal=false

# AVAILABLE AUXILIARY CACHES
jcs.auxiliary.DC=
     org.apache.jcs.auxiliary.disk.indexed.IndexedDiskCacheFactory
jcs.auxiliary.DC.attributes=
     org.apache.jcs.auxiliary.disk.indexed.IndexedDiskCacheAttributes
jcs.auxiliary.DC.attributes.DiskPath=c:/temp
jcs.auxiliary.DC.attributes.MaxPurgatorySize=10000000
jcs.auxiliary.DC.attributes.MaxKeySize=1000000
jcs.auxiliary.DC.attributes.MaxRecycleBinSize=5000
jcs.auxiliary.DC.attributes.OptimizeAtRemoveCount=300000
jcs.auxiliary.DC.attributes.ShutdownSpoolTimeLimit=60

Listing 1 contains three sections:

  • The default region defines the default configuration for all regions unless it is overridden explicitly by one of the other regions.
  • Next is a list of predefined (that is, user-defined) cache regions, which in this case includes the musicCache that I'll use in the upcoming example.
  • Auxiliary caches define auxiliaries that can be plugged into a cache region. Although each cache region must have one (and only one) memory auxiliary, it can have any number of other auxiliaries that can hold cached data. In this example I create an indexed disk cache, but you can also define lateral and remote auxiliaries. A lateral auxiliary can replicate your cached data to other caches via a TCP socket or JGroups protocol stack. A remote auxiliary can replicate data to other caches via Remote Method Invocation (RMI).

Each region can define cache attributes as well as element attributes. A cache attribute defines a configuration option for the cache, whereas an element attribute defines a configuration option for the elements in the cache. Here's a summary of the cache attribute options:

  • MaxObjects: This is the maximum number of objects allowed in memory.
  • MemoryCacheName: This property allows you to define the memory manager to use as your MemoryCache. The default memory manager implements a LRU strategy.
  • UseMemoryShrinker: This option allows JCS to iterate periodically over the cache, looking for objects that can be removed (items that have expired or have exceeded their maximum memory-idle time). The default value is false.
  • MaxMemoryIdleTimeSeconds: If the memory shrinker is enabled, this property tells JCS how long an object can remain idle before the shrinker removes it (and spools it to disk if an indexed disk cache has been created). The default value is -1, which disables this option.
  • ShrinkerIntervalSeconds: If the memory shrinker is enabled, this property tells JCS how often to run the shrinker. The default value is 60 seconds.
  • DiskUsagePattern: If a disk cache is enabled, this property tells JCS how to persist data when the memory cache is full. The default value is SWAP, which spools items to disk only when the memory cache is full. The other option is UPDATE, which persists all data out to disk, but only when data is updated. If a JDBC auxiliary has been defined as a disk cache, all objects remain in memory (until the memory is full) and are also persisted to a database, which provides for good performance as well as reliability.

And here are the element attribute options:

  • IsEternal: If an element is eternal then it cannot be removed from the cache because it exceeds its maximum life. This option defaults to true.
  • MaxLifeSeconds: If elements are not eternal, this option defines the maximum life of each object before it is removed. If the memory shrinker is running, objects are removed by the shrinker; if not, they are removed when they're accessed. This option defaults to -1, which disables the option.
  • IsSpool: This option defines whether or not an element can be spooled out to disk. It defaults to true.
  • IsLateral: This option defines whether or not an element can be sent to a lateral cache. It defaults to true.
  • IsRemote: This option defines whether or not an element can be sent to a remote cache. Defaults to true.

In Listing 1, I created a region named musicCache that holds up to 1,000 items in memory. Its memory manager uses a LRU algorithm: when the cache is full and JCS needs to make room for new items, it will remove items that have not been recently accessed. It has the memory shrinker enabled, and the shrinker will run every 60 seconds. It will evict items that sit idle for more than 60 minutes (3,600 seconds.) Its items are not eternal, and they can be written out to disk, to a lateral cache, or to a remote cache.

Note that the IsSpool, IsLateral, and IsRemote settings are inherited from the default settings. Because the jcs.region.musicCache element is set to DC, it is defined not only to maintain an in-memory cache, but also to use the indexed disk cache as an auxiliary. (The property can be set to a comma-separated list of multiple auxiliaries.) The disk cache is configured to store items in the c:/temp directory. (JCS prefers forward slashes to backslashes.) The remaining attributes configure the disk cache using an IndexedDiskCacheAttribute object; you can read about these attributes in the JCS Javadoc.

Building a sample caching application

Once you understand how to configure JCS, building a caching application is straightforward. The application needs to be able to:

  • Initialize the cache from its configuration file
  • Access a region in the cache
  • Load objects into the cache
  • Retrieve objects from the cache
  • Remove objects from the cache

The cache can be initialized either automatically or manually. If you name your configuration file cache.ccf and put it directly in your CLASSPATH (such as your root build directory), then the first time JCS is invoked it finds the file and initializes appropriately. If you need to store your configuration file elsewhere or name it differently, you can use the org.apache.jcs.utils.props.PropertyLoader's loadProperties() method to load JCS properties from any properties file.

To access a region in the cache, you invoke the JCS class's static getInstance() method, passing it the name of the region to retrieve, as in this example:

JCS cache = JCS.getInstance( "musicCache" );

You can put objects into the cache by invoking the JCS class's put() method, specifying a key and a value:

cache.put(new Integer(1), new Album(1, "Toby Mac", "Diverse City"));

You can then retrieve objects by invoking the JCS class's get() method, passing it the key of the object to retrieve:

Album album = ( Album )cache.get( id );

Finally, you can remove objects from the cache by invoking the JCS class's remove() method, passing it the key of the object to remove:

cache.remove( id );

The only requirement for objects to be stored in the cache is that they must be serializable, meaning that they must implement the java.io.Serializable interface and contain either primitive types, Strings, wrapper classes, or other serializable objects.

Listing 2 shows the code for the Album class, which is the cached object in this example.

Listing 2. The Album class

package com.geekcap.jcs;

public class Album implements java.io.Serializable
{
   private Integer id;
   private String artist;
   private String title;

   public Album() {
   }

   public Album( Integer id, String artist, String title ) {
     this.id = id;
     this.artist = artist;
     this.title = title;
   }

   public Integer getId() {
     return id;
   }

   public void setId( Integer id ) {
     this.id = id;
   }

   public String getArtist() {
     return artist;
   }

   public void setArtist( String artist ) {
     this.artist = artist;
   }

   public String getTitle() {
     return title;
   }

   public void setTitle( String title ) {
     this.title = title;
   }

   public String toString() {
     return artist + ": " + title;
   }
}

The Album class is just a thin, serializable, wrapper around three attributes: id, artist, and title. You can expand your objects to include additional fields and subobjects; you just need to ensure that they are serializable.

Listing 3 shows the contents of the MusicStore class, which demonstrates how to cache Albums.

Listing 3. The MusicStore class

package com.geekcap.jcs;

import org.apache.jcs.JCS;
import org.apache.jcs.access.exception.CacheException;

public class MusicStore
{
    private JCS cache;

    public MusicStore()
    {
        try
        {
            // Load the cache
            cache = JCS.getInstance( "musicCache" );

            // Initialize the cache
            cache.put( new Integer( 1 ),
                 new Album( 1, "Toby Mac", "Diverse City" ) );
            cache.put( new Integer( 2 ),
                 new Album( 2, "Pillar", "Fireproof" ) );
            cache.put( new Integer( 3 ),
                 new Album( 3, "Audio Adrenaline", "Underdog" ) );
        }
        catch( CacheException e )
        {
            e.printStackTrace();
        }
    }

    public void addAlbum( Album album )
    {
        try
        {
            cache.put( album.getId(), album );
        }
        catch( CacheException e )
        {
            e.printStackTrace();
        }
    }

    public Album getAlbum( Integer id )
    {
        return ( Album )cache.get( id );
    }

    public void removeAlbum( Integer id )
    {
        try
        {
            cache.remove( id );
        }
        catch( CacheException e )
        {
            e.printStackTrace();
        }
    }

    public static void main( String[] args )
    {
        MusicStore musicStore = new MusicStore();
        musicStore.addAlbum( new Album( 4, "The O.C. Supertones",
                                       "Supertones Strike Back" ) );
        Album album = musicStore.getAlbum( 1 );
        System.out.println( "Album 1: " + album );
        album = musicStore.getAlbum( 4 );
        System.out.println( "Album 4: " + album );
        musicStore.removeAlbum( 4 );
        album = musicStore.getAlbum( 4 );
        System.out.println( "Album 4: " + album );
    }
}

The MusicStore class retrieves the musicCache region from JCS in its constructor. Because the cache configuration file is named cache.ccf and is in the CLASSPATH, the getInstance() method causes JCS to load the cache properties and create the region. Subsequent getInstance() invocations would simply return the region that has already been created. The constructor then loads three albums into the cache by invoking the put() method.

The main() method demonstrates how to add an album, retrieve an album, and remove an album from the MusicStore, which simply delegates to one of the JCS cache methods.

Although this example runs as a standard Java class, a more realistic example would integrate these classes into an enterprise Java application. For example, the music store could be expanded to check for an object in the cache. If it doesn't find the object there, it would then query a database for the requested object, putting the result back into the cache. Then a servlet, a business object, or a Spring bean could use the music store as a service object.

In conclusion

The primary benefit of a cache is that objects can be served from local memory faster than they can be loaded from an external data source, such as a database located across a network. The drawback is that caches can consume a considerable amount of memory, and if objects and their usage patterns are not analyzed thoroughly, cache management can introduce another bottleneck into your application. If a cache appears to be a good fit once you analyze your objects, JCS is a great option.

Steven Haines is the founder and CEO of GeekCap, Inc., which provides technical e-learning solutions for software developers. Previously he was the Java EE Domain Expert at Quest Software, defining software used to monitor the performance of various Java EE application servers. He is the author of Pro Java EE 5 Performance Management and Optimization, Java 2 Primer Plus, and Java 2 From Scratch. He is the Java host on InformIT.com and a Java Community Editor on InfoQ.com. Steven has taught Java at the University of California, Irvine and Learning Tree University.

Learn more about this topic

More from JavaWorld

Join the discussion
Be the first to comment on this article. Our Commenting Policies