Cache Aspect

In this section we will see how to add a cache to the fibonacci class. This aspect is very useful for a wide spectrum of applications, We will see two approaches: use a metaobject to hold the cached values and use the Fibonacci class itself to hold them. We will see the consequences of taking one or the other of these approaches.

Cached Values in the Metaobject

First, we will see how to use a metaobject to hold the cached values. Taking this option, we are restricted to use one metaobject for each Fibonacci instance because different instances can have different values for the same parameters (we are forgetting the fact that the cached values are the same among all Fibonacci instances. We are doing this because that is the most common scenario).

As usual, we need our hookset matching the get method:

Hookset theHookset = new PrimitiveHookset(
        MsgReceive.class,
        new NameCS("reflex.examples.fibonacci.app.Fibonacci"),
        new NameOS("get")
);

And the metaobject definition:

MODefinition theMO = new MODefinition.MOClass(CacheHandler.class.getName());

In the code above we used another way to create the metaobject definition: MODefinition.MOClass. This kind of metaobject definition takes the metaobject's class name. We have to define it this way because we will use one metaobject per instance, therefore, we cannot pre-instantiate the metaobject: if we do so, the same metaobject will be shared by all Fibonacci instances. To avoid this, we delegate to Reflex the instantiation of the metaobject.

At the same time, we used the CacheHandler class, which definition is:

public static class CacheHandler{
 
    Map<Integer, Long> itsCache = new HashMap<Integer, Long>();
 
    public Object lookCache(int anArg, IExecutionPointClosure aClosure){
        if (!itsCache.containsKey(anArg)){
            Long theValue = (Long) aClosure.proceed();
            itsCache.put(anArg, theValue);
        }
 
        return itsCache.get(anArg);
    }
}

This class keeps the cache of the pre-calculated values in its itsCache field. Its unique method lookCache is invoked instead of the original operation get, and checks if the value for the argument is in the cache: if it does not, the original invocation (get) is invoked and the result is stored in the cache. Finally the cached value is returned (at this stage we are sure the value is in the cache).

The code of the link's definition and configuration is:

BLink theLink = Links.get(theHookset, theMO);
theLink.setControl(Control.AROUND);
theLink.setCall(Control.AROUND, CacheHandler.class.getName(), "lookCache",
                new Parameter[]{
                    new StdParameters.IndexedArgument(0),
                    Parameter.CLOSURE
                });
theLink.setScope(Scope.OBJECT);

In the previous code we set the control attribute as AROUND, because we do not want to execute the original operation if we have already cached the result. Then, we set the call descriptor: the lookCache method should be called with two arguments, the fibonacci number and the closure object.

The source code of the link provider that registers this links is avalilable here. And the command to run this example is:

Windows
    %java -classpath
            "lib\javassist.jar;build\reflex-core.jar;build\reflex-examples.jar"
            reflex.examples.tutorial.CacheMOAspect

Linux
    %java -classpath
            "lib/javassist.jar:build/reflex-core.jar:build/reflex-examples.jar"
            reflex.examples.tutorial.CacheMOAspect

The output should be the same as executing the Fibonacci class without the aspect. To test the proper working of the cache, modify the CacheHandler class to print some information:

public static class CacheHandler{
 
    Map<Integer, Long> itsCache = new HashMap<Integer, Long>();
 
    public Object lookCache(int anArg, IExecutionPointClosure aClosure){
        System.out.print("  lookCache(" + anArg + ")");
 
        if (!itsCache.containsKey(anArg)){
            System.out.println("  cache miss");
            Long theValue = (Long) aClosure.proceed();
            itsCache.put(anArg, theValue);
        }
        else{
            System.out.println("  cache hit");
        }
 
        itsCache.get(anArg);
    }
}

You should see something like:

lookCache(5)  cache miss
  lookCache(4)  cache miss
  lookCache(3)  cache miss
  lookCache(2)  cache miss
  lookCache(1)  cache miss
  lookCache(0)  cache miss
  lookCache(1)  cache hit
  lookCache(2)  cache hit
  lookCache(3)  cache hit
The fibonacci[5] is 5

  lookCache(6)  cache miss
  lookCache(5)  cache miss
  lookCache(4)  cache miss
  lookCache(3)  cache miss
  lookCache(2)  cache miss
  lookCache(1)  cache miss
  lookCache(0)  cache miss
  lookCache(1)  cache hit
  lookCache(2)  cache hit
  lookCache(3)  cache hit
  lookCache(4)  cache hit
The fibonacci[6] is 8

As you can see, two different CacheHandler instances were created (because there are two cache misses for the same values: 5, 4, 3, 2, 1 and 0).

In the following section we will see how to change this relationship cardinality (in this example we have 1:1 between Fibonacci and CacheHandler instances) to n:1 as it is a more scalable solution.

Cached Values in the Fibonacci Instances

In the previous section we created a configuration where there is one instance of a CacheHandler per Fibonacci instance. This relation must be 1:1 because we used the metaobject to hold the cached values.

In this section, we will use the Fibonacci instances themselves to hold their cached values and just one GlobaCacheHandler instance to manage these values. This avoids the duplication of the object's number in the system, resulting in a more scalable solution.

When we have just one instance of a metaobject in the system, we say that the metaobject has GLOBAL scope. . If we want to add the cache map to the Fibonacci class, we have to use a structural metaobject, which is defined here:

public class CacheElementsSMO implements SMetaobject{
 
    public void handleClass(RClass aClass) throws MOPException{
        try{
            MemberFactory theMemberFactory = API.getMemberFactory();
 
            theMemberFactory.addField(
                    "java.util.Map itsCache = new java.util.HashMap();", aClass
            );
 
            theMemberFactory.addMethod(
                    "public java.util.Map getCache(){ return itsCache; }", aClass
            );
 
            aClass.addInterface(CacheHolder.class.getName());
            }
        catch (Exception e){
            //This should not happen
            throw new RuntimeException(e);
        }
    }
}

Here we add three cache-related elements to the Fibonacci class:

  • A itsCache field, where we will store the cached values (we do not use generics this time because they are not supported by Javassist).
  • A getCache method, used to obtain a reference to the itsCache field from outside of the class.
  • A CacheHolder interface, used to get a type-safe reference to an instance which class declares the getCache method.
Notice that when we add a method or a field to a class, the only way to access it at runtime is making use of reflection:
ClasstheFibClass = fibInstance.getClass();
FieldtheField = theFibClass.getField("itsCache");
Map theCache = (Map) theField.get(fibInstance);

But, the code above is error-prone because we are using the string itsCache and it will fail at runtime if we did not write it well. Then, in this example we added two more elements besides the field: an access method and an interface, so we use the following code to access the field:

Map theCache = ((CacheHolder) fibInstance).getCache();

It is worth to notice that making type-safe the access to introduced fields and methods is a good practice (we have discovered it ourselves) as it avoids the use of reflection and the problems that comes with it: security issues, name mistakes, performance penalties, etc.

The code of the CacheHolder interface is:

public interface CacheHolder{
 
    public Map getCache();
}

With this structural metaobject, we need to define a structural link that will apply to the Fibonacci class:

SLink theSLink = Links.get(
        new NameCS("reflex.examples.fibonacci.app.Fibonacci"),
        theSMO
);

At the moment, we have transformed our Fibonacci class to hold the already-calculated values. Now we have to add the behavior to use these values. As in previous examples, we will define a hookset, a metaobject definition and a behavioral link.

The hookset code is:

Hookset theHookset = new PrimitiveHookset(
        MsgReceive.class,
        new NameCS("reflex.examples.fibonacci.app.Fibonacci"),
        new NameOS("get")
);

The code above is the same as in the first implementation of the cache because the results of get are still the values we want to cache.

Then, the code of the metaobject definifion is:

MODefinition theMO = new MODefinition.SharedMO(new GlobalCacheHandler());

Here we came back to MODefinition.SharedMO (because our metaobject is global) with a GlobalCacheHandler, which definition is:

public static class GlobalCacheHandler{
 
    public Object lookCache(Object aContext, int anArg,
                            IExecutionPointClosure aClosure){
 
        CacheHolder theHolder = ((CacheHolder) aContext);
        Map<Integer, Long> theCache = theHolder.getCache();
 
        if (!theCache.containsKey(anArg)){
            Long theValue = (Long) aClosure.proceed();
            theCache.put(anArg, theValue);
        }
 
        return theCache.get(anArg);
    }
}

Here we have a slightly modified version of the lookCache method: it does not use its own itsCache field, it asks the context object (the Fibonacci instance) for it through the getCache method. Here you can see in practice what we were refering to when we said “type-safe way to access the getCache method”: as the Fibonacci class implements CacheHolder, we can safely cast the context object to this interface, obtaining this way a type-safe way to access the getCache method.

Finally, the definition and configuration of the behavioral link is:

BLink theLink = Links.get(theHookset, theMO);
theLink.setControl(Control.AROUND);
theLink.setCall(Control.AROUND, GlobalCacheHandler.class.getName(), "lookCache",
                new Parameter[]{
                    Parameter.CONTEXT_OBJECT,
                    new StdParameters.IndexedArgument(0),
                    Parameter.CLOSURE
                }));

There are only two changes in the link configuration between this approach and the previous one (the one that used the metaobject to hold the cached values):

  • We pass a reference to the context object in order to, at the end, make the cache map accessible: Parameter.CONTEXT_OBJECT
  • There is not a theLink.setScope(Scope.OBJECT) because our metaobject is global (there is not a theLink.setScope(Scope.GLOBAL) either because GLOBAL is the default scope.

The complete code of the link provider that add these two links (the structural one and the behavioral one) is here. The command to run this example is:

Windows
    %java -classpath
            "lib\javassist.jar;build\reflex-core.jar;build\reflex-examples.jar"
            reflex.examples.tutorial.GlobalCacheMOAspect

Linux
    %java -classpath 
            "lib/javassist.jar:build/reflex-core.jar:build/reflex-examples.jar"
             reflex.examples.tutorial.GlobalCacheMOAspect

You should see the same output as run the Fibonacci application without Reflex. If you want to test the cache, modify the GlobalCacheHandler class this way:

public static class GlobalCacheHandler{
 
    public Object lookCache(Object aContext, int anArg,
                            IExecutionPointClosure aClosure){
 
        System.out.print("  lookCache(" + anArg + ")");
 
        CacheHolder theHolder = ((CacheHolder) aContext);
        Map<Integer, Long> theCache = theHolder.getCache();
        if (!theCache.containsKey(anArg)){
            System.out.println("  cache miss");
            Long theValue = (Long) aClosure.proceed();
            theCache.put(anArg, theValue);
        }
        else{
            System.out.println("  cache hit");
        }
    }
}

When executed, you should see something like the following output:

lookCache(5)  cache miss
  lookCache(4)  cache miss
  lookCache(3)  cache miss
  lookCache(2)  cache miss
  lookCache(1)  cache miss
  lookCache(0)  cache miss
  lookCache(1)  cache hit
  lookCache(2)  cache hit
  lookCache(3)  cache hit
The fibonacci[5] is 5

  lookCache(6)  cache miss
  lookCache(5)  cache miss
  lookCache(4)  cache miss
  lookCache(3)  cache miss
  lookCache(2)  cache miss
  lookCache(1)  cache miss
  lookCache(0)  cache miss
  lookCache(1)  cache hit
  lookCache(2)  cache hit
  lookCache(3)  cache hit
  lookCache(4)  cache hit
The fibonacci[6] is 8

As you can see, we obtained the same results, but making use of just one metaobject to coordinate all caches.