Welcome!

Java Authors: Walter H. Pinson, III, Maureen O'Gara, Yakov Werde, Tony Bishop, Kevin Jackson

Related Topics: Java

Java: Article

Proxy Cache

A practical implementation

In order to implement the rest of the framework we will need to start with a very simple concrete service, which we will call EchoService. The EchoService implements the service interface and simply returns the request object as the response object; or, EchoService echoes the input as its output. This simple construct will help illustrate caching functionality when we actually implement it.

package service;
public class EchoService
implements Service {
   public Object service(
   Object toService) {
     return toService;
}
}

Listing 1 shows the implementation of a ServiceFactory that only knows how to create an EchoService. In the method newEchoService(), notice that even though a new EchoService instance is created, it is wrapped in a new CacheProxy instance and returned as a dynamic proxy. While the reflection taking place here might appear to be a heavy process, it is essentially a one-time hit because the Proxy class internally caches Proxy class definitions given the particular set of arguments to Proxy.newProxyInstance(). We could also create the Proxy class ahead of time since we expect to only return objects of type service, but the code is left this way for the sake of brevity; therefore, it is up to you to refactor this code.

Listing 2 shows our Proxy Cache implementation, aptly named CacheProxy. Even though CacheProxy is an implementation of an InvocationHandler, the suffix -Proxy is left in to illustrate that CacheProxy contains our actual proxy code.

Notice that our CacheProxy is somewhat light in terms of desired functionality. In fact, it's quite useless. If we were to throw this code into production, EchoService would return the request object each and every time. From a black-box perspective, we actually desire that the same output be provided for a given input. However, if we can imagine that EchoService is actually a fairly complex service that performs several time-consuming operations on its input, we quickly realize that it would be better to just return a cached response ahead of time without enduring the performance hit (see Figure 2).

Refactoring the Proxy Cache
There are many ways to incorporate a caching mechanism into our CacheProxy class. The most basic approach is to use a java.util.Map, more specifically, a HashMap. This solution sounds simple, but the problem with this approach is made visible in this way: if we initialize the Map instance directly in the CacheProxy, the Map will only survive for as long as the CacheProxy instance does. Because the ServiceFactory creates new CacheProxy instances every time a service is requested, we will, in effect, never cache anything.

We don't want to inject the Map into CacheProxy because only the CacheProxy should care about the Map. Therefore, a suitable solution to this problem is to have the ServiceFactory manage the instances of proxies that it creates. There are, however, several side-effects to this approach. For starters, the ServiceFactory is now acting more like a resource manager than a factory. Although we would normally rename ServiceFactory to something like ServiceManager, at this point, we opt to leave the name as is for the rest of this article.

Second, each CacheProxy instance is a kind of pseudo-singleton. For example, there will be only one EchoService proxy instance per instance of the ServiceFactory. These proxies are only pseudo-singletons because the ServiceFactory is not itself a singleton; meaning, each instance of ServiceFactory will create its own CacheProxy instances. Or better yet, assuming we will only create one ServiceFactory instance per virtual machine, we have what Uncle Bob (Bob Martin) would call "Just Create One," making ServiceFactory a glorified Singulizer.

To ensure one instance of a service per virtual machine, we privately define a class instance variable named echoService and lazily initialize it at runtime. The following code snippet can be applied to ServiceFactory to enable lazy-initialization of an EchoService instance:

...
private Service echoService;
public synchronized Service newEchoService() {
   if (echoService == null)
     echoService = //create EchoService proxy
   return echoService;
}
...

We had to synchronize newEchoService() to ensure that only one instance is ever created lazily for the specific ServiceFactory instance. If newEchoService() is not synchronized in any way, a race condition could exist whereby multiple, simultaneous calls to newEchoService() would produce multiple EchoService instances, thereby reducing the probability of generating a unique cache. This race condition only exists when EchoService is being lazily instantiated.

In addition, if ServiceFactory were to manage more service initializations, we would need a class instance variable per factory method.

Is the Map problem solved? No; how will CacheProxy use the Map? If we want CacheProxy to return the same output for a given input, we should use the argument array provided to invoke() as a key and the response from the call to the real service as the value. Thus, whenever the same input is provided, assuming immutable types or the exact same object instances are being passed, we can retrieve the corresponding output from the Map.

That sounds great too, but Cache-Proxy's generic behavior elicits yet another problem. What happens when two methods of a service (or any object being proxied) share the same method signature? In this case, a call to one method may actually return a previously cached response of another method. The quick-and-easy solution is to keep a cache for each method, which is very feasible considering that the invoke() method provides us with an instance of the Method object that was being called. But, this enhancement creates another headache for us because we need to store the mapping of methods to their caches.

Assuming we have incorporated the enhancement, we now have a two-level caching scheme using Maps. Are we ready to go yet? In theory, yes, but there is one more item we should address. Because we don't need all of the functionality provided by a Map and we really just want to deal with a cache, we should define a cache type.

package service;
public interface Cache {
   public Object retrieve(
     Object key);
   public void store(
     Object key, Object value);
}

In our cache, the only behavior we are concerned with is the ability to store and retrieve values via their keys. In addition, if we need any other special behavior for our cache(s), we can implement that behavior in cache, the proper place, and not make CacheProxy responsible for it. Once cache is defined, we can implement a SimpleCache that uses HashMap internally (see Listing 3).

From an object-oriented perspective, realizing the cache type is actually very important. Earlier in this article I stated that CacheProxy is responsible for all caching behavior, which is true, but only to a point. The cache type highlights where the statement breaks down. If we leave all caching behavior in CacheProxy, CacheProxy is then responsible for the investigating the cache and possibly calling the real service (proxy behavior) and managing the actual cache. To keep things simple, we only really want CacheProxy to be responsible for one of those two, therefore, cache is responsible for managing the cache and CacheProxy only needs to know what cache provides.

Now are we ready to move on? Quite so! We finally move on to adding cache behavior to CacheProxy (see Listing 4). The new version of CacheProxy is slightly longer, but not much more. There is also some duplication in the invoke() and findMethodCache() methods related to storing values when a retrieved value is null, but, we'll leave this refactoring for another day.

At the heart of CacheProxy is the invoke() method. In the invoke() method a specific protocol is followed. First, the cache for the method being called is identified and retrieved. This functionality is mostly implemented in the findMethodCache() method, wherein, a new cache instance is created if no cache instance has yet been created for the particular method. Thus, the method instance is itself the key to a cache.

Once the method's cache has been identified, another key is created from the array of arguments that is to eventually be passed to the targeted method. This key is generated via the generateArgumentKey() method. After creating this composite key, the key is used to find any previously cached value from the method's cache retrieved earlier. If no value was previously cached, the proxied object's method is called reflectively (identified by the Method object) and the results of that call are cached. Finally, the cached results are returned back to the original caller.

It is important to note what is going on in the generateArgumentKey() method. Superficially, the array of arguments is being morphed into a list of arguments (java.util.List). If the array of arguments is null, a predefined NullKey is used as the new key. But why convert the array to a list? Lists that extend AbstractList have a unique feature in that the hash-code is generated based on the set of elements in the list, such that two distinct lists containing the exact same elements will produce the exact same hash-code.

On the other hand, two arrays of arguments containing the same elements will produce two distinct hash-codes, inhibiting us from being able to retrieve a cached value given the same set of arguments. In addition, if we were to simply use the object array provided to the invoke() method as the key, we would have an ever-growing cache that never finds anything we are looking for.

It could also be argued that because the hash-code issue is cache related, the job of converting the array to a list belongs to cache. It could be argued, but, the reality is that CacheProxy is actually concerned about presenting identical keys, not cache. Therefore, CacheProxy is responsible for the conversion.

More Stories By Justin Knowlden

Justin Knowlden is a solutions architect with United Airlines. Prior to United, he helped develop the MyPoints.com product from its inception. Justin is actively participating in the open source community (see Helium and ESP). When not programming he is a husband, a basketball and football coach for his son, and an environmental and animal rights activist.

Comments (2) View Comments

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.


Most Recent Comments
Justin Knowlden 02/16/06 05:57:47 PM EST

If you read the online version, the source code is available as links to the listings. Here is the URL to the listings (http://res.sys-con.com/story/jan06/171489/source.html).

larry fernandez 02/16/06 02:15:08 PM EST

where is the source code?