Implementing Object Caching with AOP
Object caching has a number of characteristics that make it a prime candidate for implementation as an Aspect. Learn what some of these characteristics are.
Introduction
Object caching provides a mechanism to store frequently accessed data in memory, minimizing the calls to back-end database, and resulting in significant improvement in the application performance. It also gives us the ability to refresh different types of data at different time intervals (based on pre-defined eviction and cache refresh policies). Object caching offers several advantages with fast access to data but it also suffers from some disadvantages like memory overhead and synchronization complexity. By making caching an Aspect we get the flexibility of dynamically adding caching ability in a J2EE application for cached objects. We can also remove caching out of the application whenever it becomes a bottleneck in terms of memory usage. In this article, I will provide an example of object caching as an Aspect in J2EE applications and discuss the steps involved in injecting the caching functionality into a sample web application. I will also explain the flexibility of switching between two different caching frameworks without modifying any application code. The article uses AspectJ, with the caching logic implemented in both JBossCache and OSCache.
Caching As An Aspect
Object caching has a number of characteristics that make it a prime candidate for implementation as an Aspect. Some of these characteristics are:
- caching code is often duplicated throughout a web application
- it's not easy to turn caching on or off dynamically when it's part of your business logic
- caching logic does not provide any business functionality, it's not related to the domain of a business application
This article assumes that reader has a basic understanding of Aspects and AOP concepts. Those who are new to Aspects should review the Appendix section at the end of the article for a brief overview of AOP and definitions of AOP components.
In the traditional approach to object caching, objects are stored in memory (cache) after first use, and then reused for subsequent data access requests instead of making costly data source calls. The cached data is released from memory (purged), by implementing a pre-defined eviction policy, when the data is no longer needed.
For an Aspect-based approach, we need to first write a pointcut to intercept the actual data load method called by a client program. Then we write an "around" advice to query the cache for a matching data based on a cache key. If no data match is found, the advice uses proceed()
(which is similar to calling the load method in cache loader class) to execute the data access operation and inserts the data returned from the datastore into cache. Finally, the advice returns the object obtained from the cache if the data already exists in cache or newly created object if cache didn't have it in the first place.
Compared to traditional caching implementation, a cache loader class is not really necessary in aspect oriented caching solution. This is because with a caching aspect we have access to the calling class and its methods via the pointcut, so we know which method to call to retrieve the data from back-end data store.
Following table summarizes the steps involved in data access calls with and without caching implementation and also using caching as an aspect:
Data Access | Scenario 1 | Scenario 2 | Scenario 3 |
---|---|---|---|
Type of caching | No caching implemented. | Traditional caching implementation. | Aspect oriented caching implementation. |
First request | Retrieve data from database and return result to client. | Check if data is in cache. No match is found so retrieve data from database, store the result in cache, and return data to client. |
Intercept the data access call using a pointcut. Using an around advice, check if data is in cache. No match is found so call proceed() method which in turn executes the data load method.Store the result in cache and return data to client. |
Subsequent requests | Retrieve data from database again and return result to client. | Check if data is in cache. A match is found so return the data found in cache to the client. If data in the cache is expired, retrieve data from database again, store it in cache, and return to client. |
Intercept the data access call using the same pointcut. Check if data is in cache using the around advice. A match is found so return data to client. |
Caching Logic | None. | Embedded in application code. | Encapsulated in Aspects. |
Invasive? | N/A | Yes (Changes in application code are necessary to enable or disable caching) | No (Completely separated from the application code. Caching logic is dynamically weaved into the application) |
Caching using JBossCache - AOP Style
I used JBossCache as the main framework to demonstrate aspect oriented object caching implementation in the sample web application. JBossCache provides two flavors of object caching implementation, TreeCache and TreeCacheAop. TreeCache is basically a structured object tree with nodes. The data stored in the cache is accessed by specifying a fully qualified name (FQN) which is the concatenation of all node names from root node to the current node. FQN is same as the region name used in other caching frameworks such as Java Caching System (JCS) and OSCache. A cache region is defined as an organizational name space for holding a collection of cache objects with similar characteristics (such as business usage and time to live). TreeCache can be used as a stand-alone cache (local) or a replicated cache (in a multi-server cluster environment). Also, the cache replication can be done asynchronously or synchronously (synchronous means the client request is not returned until all cache changes are replicated in the cluster nodes). JBossCache uses the JGroups framework for the group communication purposes when replicating the modifications in one cache to other caches in the cluster.
TreeCacheAop is an AOP-enabled extension of TreeCache. It allows for plain java objects to be stored in the cache and replicated transactionally between the nodes in a cluster. TreeCache is highly configurable in terms of replication mode (async, sync, or none), transaction isolation levels, eviction policies, and transactional management.
Object Caching Framework
The object caching framework used in this article is based on the object caching framework described in articles Object Caching in a Web Portal Application Using JCS and J2EE object-caching frameworks. Based on the requirements of caching implementation as an aspect, I have made a list of objectives that needed to be accomplished by the proposed Cache AOP framework. Following is the list of these objectives:
- Avoid duplicating the caching logic at every execution point in the application where there is a business case for data caching.
- Write cache related code in a non-intrusive way so it doesn't affect the core application code. This is because the caching logic has nothing to do with the core functionality of the application. It is used mainly for performance reasons.
- Ability to switch between the scenarios of running the application with and without caching, in order to assess the effectiveness of caching in data access. With the caching aspect implementation, if caching proves to be not really effective, we don't have to modify the application code by commenting or removing the caching logic. This approach is very useful during unit testing phase where we want to test the caching feature with different configuration parameters to come up with the best scenario.
- Flexibility to easily switch between or replace the caching implementation with a different (and better) framework without affecting the application code. There are many different algorithms and configuration settings so it takes few iterations to get optimum caching configuration and best results out of a caching implementation.
- Ability to trace and monitor caching statistics for better management of objects stored in the cache. We need to constantly monitor cache usage to determine how effectively caching is being used (based on cache hit to miss ratio) and make any necessary adjustments in the cache configuration or the expiration policies to keep object caching more effective and efficient. Also, as the business requirements and application functionality change over time we need to make sure we are only storing the expensive-to-access (and frequently requested data) in the cache. By encapsulating tracing and logging functions into aspects as well, we will have a non-intrusive cache monitoring capabilities where we don't have to write the tracing method calls all over in the application code to measure the effectiveness of caching.
Sample Web Application Setup
The web application used to demonstrate the caching implementation using AOP techniques is a bank application used to process home loans. In a typical loan processing application, the customer is given a list of interest rates for a specified product type before the user can actually lock a home loan. These interest rates are a perfect candidate for caching since they only change few times a day but the rate data is frequently accessed by the loan application. So, by storing interest rate details in the cache, we minimize the need to hit the data source every time a loan application is processed. There are two types of loan products used in the web application, Mortgages and Home Equity (HELOC) loans.
The interaction between different tiers in the loan application (client, web, object cache, and back-end HSQL database) is represented in the topology diagram in Figure 1.
Figure 1. Topology Diagram with Tomcat Cluster and JBossCache (Click on the screen shot to open a full-size view.)
The flow of loan processor web application is shown in the sequence diagram in Figure 2.
Figure 2. LoanApp Web Application Sequence Diagram (Click on the screen shot to open a full-size view.)
The entry point into the web application is a servlet called LoanAppServlet
that gets the interest rates based on product type sent by the client (in HTTP request). The helper class LoanAppHelper
has the data access methods to retrieve (using JDBC calls) the interest rates stored in a HSQL database. Following is a code snippet of getInterestRates
method to get the rates from INTERESTRATES
table in LoanDB
database using JDBC calls.
public List getInterestRates(String prodGroup) { List interestRates = new ArrayList(); Connection conn = null; Statement stmt = null; ResultSet rs = null; try { conn = DriverManager.getConnection(jdbcUrl, userName, password); sLog.debug("conn: " + conn); stmt = conn.createStatement(); String sql = "SELECT * FROM InterestRates WHERE "; sql += "productGroup = '" + prodGroup + "'"; rs = stmt.executeQuery(sql); List rates = new ArrayList(); while (rs.next()) { String productType = rs.getString("productType"); String productGroup = rs.getString("productGroup"); double rate = rs.getDouble("rate"); double mtgPoints = rs.getDouble("points"); double apr = rs.getDouble("apr"); InterestRate interestRate = new InterestRate(); interestRate.setProductType(productType); interestRate.setRate(rate); interestRate.setPoints(mtgPoints); interestRate.setApr(apr); interestRates.add(interestRate); } } catch (SQLException e) { e.printStackTrace(); } finally { try { rs.close(); stmt.close(); conn.close(); } catch (Exception e) { // } } return interestRates; }
Note: Using pure JDBC calls in a real world application is not recommended. Instead, object relational modeling (ORM) tools such as Hibernate or JDO and database connection pooling (Commons DBCP) frameworks should be considered for data access requirements.
The LoanApp web application uses TreeCache
(org.jboss.cache.TreeCache
) class to implement caching. It also uses asynchronous replication to propagate cache changes to all nodes in the cluster. Following section describes how caching is introduced into the application using aspects.
I wrote an abstract aspect called ObjectCache
to encapsulate all the cache methods (like putCacheObject
, peek
etc). The concrete implementations of this abstract aspect are defined in JBossCache
and OSCache
(for JBossCache and OSCache caching frameworks respectively). I also added two more aspects to take care of logging and tracing requirements in the web application (logging and tracing are two other popular cross-cutting concerns that are weaved into the application code via aspects).
Figure 3 shows the class diagram with the associations between java classes and aspects created in LoanApp application.
Figure 3. Cache AOP Class Diagram (Click on the screen shot to open a full-size view.)
I initialized the cache objects when the web application context (/CachingAOP
) was created by Tomcat server and keep them in memory until the application context is destroyed. This is done by implementing a custom servlet context listener (LoanAppContextListener
). I added pointcuts contextInitialized
and contextDestroyed
to take care of the instantiation and removal of cache objects when the servlet context is initialized and destroyed respectively. Following code shows how TreeCache
is initialized when servlet context is loaded.
abstract pointcut contextInitialized(ServletContextEvent event); void around(ServletContextEvent event) : contextInitialized(event) { initObjectCache(); }
And here's the implementation of initObjectCache
method in JBossCache
aspect:
private TreeCache cache; public void initObjectCache() { try { cache = new TreeCache(); PropertyConfigurator config = new PropertyConfigurator(); config.configure(cache, "jbosscache-config.xml"); cache.setClusterName(cacheGroupName); cache.createService(); cache.startService(); sLog.debug("cache : " + cache); } catch (Exception e) { // Handle cache region initialization failure sLog.debug("Error in creating JBossCache."); sLog.error(e); } }
We write a pointcut to intercept getInterestRates()
method and then write an "around" advice to implement the caching logic. In the advice, we query the cache for a matching data based on productGroup
cache key. Let's look at how this logic is implemented in ObjectCache
aspect.
abstract pointcut getInterestRates(String productGroup); List around(String productGroup) : getInterestRates(productGroup) { long start = System.currentTimeMillis(); List rateList = null; try { if (peek(productGroup) != null) { sLog.debug("Data found in cache."); rateList = getInterestRates(productGroup); return rateList; } sLog.debug("Data not found in cache."); rateList = proceed(productGroup); putCacheObject(productGroup, rateList); long stop = System.currentTimeMillis(); sLog.debug("Time taken to get interest rates=" + (stop- start) + " ms."); printCacheStatistics(); } catch (Exception e) { e.printStackTrace(); } return rateList; }
If there is a cache miss or stale hit, the advice uses proceed()
to execute the data access operation (LoanAppHelper.getInterestRates()
method) and inserts the data returned from the database into the cache (using putCacheObject
method). Finally, the advice returns the object obtained from the cache if it already exists in the cache or newly created object if cache didn't have it in the first place.
The following table shows the details of the caching aspects and pointcuts.
Aspect | PointCut | Advice |
---|---|---|
ObjectCache.aj JBossCache.aj OSCache.aj |
contextInitialized contextDestroyed getInterestRates |
around around around |
The AspectJ project created to run the web application code using aspects is called CacheAOP
. I created separate directories under CacheAOP/WEB-INF/src
folder (java, web, and aspects) to store java files and aspect files in their own directories. A link to the sample application code is included at the end of this article. To run the application, make sure you have the following JAR files specified in the classpath:
commons-logging-1.0.4.jar, junit.jar, log4j-1.2.8.jar, oscache-2.1.jar, commons-httpclient.jar, aspectjrt.jar, jgroups-2.2.7.jar, concurrent.jar, dom4j-full.jar, hsqldb.jar, jboss-aop.jar, jboss-cache.jar, jboss-common.jar, jboss-j2ee.jar, jboss-remoting.jar, jboss-system.jar, jboss-jmx.jar, and servlet-api.jar.
The config
directory (located under WEB-INF
) also needs to be in the classpath since all the configuration files (JBossCache
, OSCache
, and Log4J
) are stored in this directory.
You can switch between different caching scenarios using AspectJ build properties file (build.ajproperties
) created in the Eclipse project. Just open the properties file in the editor, include or exclude the aspects you would like to weave into the application code, and rebuild the project. This involves no additions or modifications in the web application code. Figure 4 shows the screenshot of build.ajproperties
(AJDT) tab in Eclipse IDE with ObjectCache
and JBossCache
aspects selected for code weaving.
Figure 4. AspectJ Build Properties Window in Eclipse IDE (Click on the screen shot to open a full-size view.)
- Run the web application with no caching implemented.
- Caching is implemented using JBossCache.
- Caching is implemented using OSCache framework.
Cluster Details
LoanApp web application was deployed to run in a Tomcat cluster with two server instances running on a single machine. The configuration parameters used in the cluster are listed in the table below.
Parameter | Node 1 | Node 2 |
---|---|---|
Server Name | TC-01 | TC-02 |
Home Directory | c:/dev/tomcat51 | c:/dev/tomcat52 |
Web App Directory | C:/dev/projects/CachingAspect/node1/CachingAOP | C:/dev/projects/CachingAspect/node2/CachingAOP |
Server Port | 9005 | 10005 |
Connector/Port | 9080 | 10080 |
Coyote/JK2 AJP Connector | 9009 | 10009 |
Cluster mcastAddr | 228.0.0.4 | 228.0.0.4 |
Cluster mcastPort | 45564 | 45564 |
tcpListenAddress | 192.168.0.10 | 192.168.0.20 |
tcpListenPort | 4001 | 4002 |
Cache properties file | jbosscache-config.xml | jbosscache-config.xml |
properties file location | node1/CachingAOP/WEB-INF/config | node2/CachingAOP/WEB-INF/config |
Cache mcast_addr | 228.1.2.3 | 228.1.2.3 |
Cache mcast_port | 48866 | 48866 |
Cache bind_addr | 192.168.0.10 | 192.168.0.20 |
Cache bind_port | 9080 | 10080 |
Database | c:/dev/db/loandb | c:/dev/db/loandb |
Instrumentation Layer
Two aspects called Trace
and CacheLog
were created to introduce logging and tracing capabilities into the web application to measure response times for accessing cached objects and to log various events in the caching process.
Tracing aspect was used to monitor the effectiveness of caching by calculating the total time taken to get the data (from the database or from cache). It takes longer when the data is accessed for the first time since the data is not in cache and we need to retrieve it from the back-end database. But subsequent calls are faster since data is already present in cache and no need to access the database. This aspect also measures the available JVM memory before and after each call to getInterestRates
method.
Logging aspect was used to verify that the data is retrieved from a data source the first time it's requested by the client and from cache for all the subsequent requests. It was also used to keep track of cache hits and misses. All the log messages were saved in a single log file (cachingapp.log
) to monitor the cache statistics and cluster details to see how many cluster members are alive and how cache changes (if any) are propagated between the cluster nodes.
Testing setup
To test the implementation of caching aspect and replication of cache changes, I wrote a test client called LoanAppClient
. It uses the Commons HttpClient API to simulate the web request calls to LoanAppServlet
to process loan applications. Based on a random number generated, the test client issues a different loan request (such as getting interest rates for either mortgage loans or HELOC loans). Depending on the request attributes sent to the server, LoanAppServlet
gets the interest rate data for the specified loan type and product.
The hardware/software specifications of the PC used to test the sample web application are listed below.
- CPU: HP Pavilion Pentium III 800 MHz
- Memory: 512 MB RAM
- Hard disk: 40 GB
- Operating system: Windows 2000 server Service Pack 4
- JDK version: 1.4.2_05
- Tomcat version: 5.0.28
- Tools Used: Tomcat, JBossCache, OSCache, HSQL DB, Eclipse, AspectJ, AJDT, Commons HttpClient
Listed below are the run-time parameters for LoanAppClient
:
- Number of client threads: 2
- Number of repetitions: 1,000
- Delay between requests: 1,000 milliseconds
- Number of test samples: 1,000
The command used to run the test client with the required arguments is as follows:
java -Dlog4j.configuration=log4j.xml com.loanapp.test.LoanAppClient 1 192.168.0.10 9080 192.168.0.20 10080 2 1000
Test Results
The results from the web application test runs are listed in the following table:
Data Access Calls | Without Caching (ms) |
With Caching (ms) |
---|---|---|
First request | 140 | 180 |
Subsequent requests (avg) | 50 | 5 |
Conclusion
We looked at how fast the data access can be when we use a cache to store frequently accessed objects. Even though it took little longer to get the interest rates data the first time from the database, it took almost no time to get data from cache compared to the time it took to get it from the database (5 milliseconds with caching compared to 50 milliseconds without caching). We also looked at how easy and flexible it is to make caching part of the web application by encapsulating caching logic in aspects.
One advantage of using aspects compared to traditional object oriented programming is that if something doesn't turn out to be as effective as originally expected, the aspect can be seamlessly un-weaved from the application code without any impact on the actual code. Aspects provide plug-n-play capabilities in the application development.
We have achieved the following design goals by implementing object caching as an aspect:
- loosely coupled architecture.
- separation of logic.
- dynamic adaptation of caching and tracing cross-cutting concerns.
- modularized design.
Currently, all the cache related code is written in the aspect files. This can be further refactored to separate cache logic into separate java classes and keep just the AOP related code (pointcuts, advices etc) in the aspect files. A good AOP system should involve collaboration between aspects that provide the key cross-cutting concerns and java classes that implement all the helper methods.
A JMX based Cache monitoring tool using aspect oriented design will be a good complementary tool to the CacheAOP framework described in this article. We can use this tool to monitor the effectiveness of caching by collecting the details such as hit/miss ratio, # of accesses, last access time, object size, time to load data from data source etc.
|
Author Bio
Srini Penchikala presently works as Information Systems Subject Matter Expert at Flagstar Bank. His IT career spans over 9 years with systems architecture, design, and development experience in client/server and Internet applications. Srini holds a master's degree from Southern Illinois University, Edwardsville and a bachelor's degree (Sri Venkateswara University, India) in Engineering. His main interests include researching new J2EE technologies and frameworks related to Web Portals. He has also contributed to ONJava, DevX Java and JavaWorld online journals. Srini's spare time is filled with spending time with his wife Kavitha and 3-month old daughter Srihasa and watching Detroit sports teams. Go Pistons.