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:
From Commons Logging, add
commons-logging.jar to your
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
musicCachethat 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
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
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
IsLateral: This option defines whether or not an element can be sent to a lateral cache. It defaults to
IsRemote: This option defines whether or not an element can be sent to a remote cache. Defaults to
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
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
static getInstance() method, passing it the name of the region to retrieve, as in this example: