TreeCacheAOP

Bela Ban

November, 2004


1. Introduction
2. Feature
3. Concepts
3.1. Dynamic AOP interception
3.2. Object mapping by reachability
3.3. Object relationship management
3.4. Object inheritance hierarchy
3.5. Collection class proxy
4. Requirement
5. API
6. Configuration
6.1. TreeCacheAop MBean service
6.2. TreeCacheAop eviction policy
6.3. JBossAop-specific configuration
7. Example POJO
8. Limitations and Problems

1. Introduction

The JBossCache component, TreeCacheAop, is a replicated, transactional, and fine-grained "object-oriented" cache. By "object-oriented", we mean that TreeCacheAop provides tight integration with the object-oriented Java language paradigm, specifically it automatically : 1) manages object mapping and relationship, 2) provides support for inheritance relationship between "aspectized" POJOs (plain old Java object), and 3) preserves object identity, for a client under both local and replicated cache modes.

By leveraging the dynamic AOP capability in JBossAop, it is able to map a complex object into the cache store while preserving the object relationship behind the scene. During replication mode, it also performs fine-granularity (i.e., on a per-field basis) updates, and thus has the potential to boost cache performance and minimize network traffic.

TreeCacheAop extends the functionality of TreeCache to POJOs with dynamic AOP (Aspect-Oriented Programming)-enabled capability. That is, in addition to the TreeCache basic features such as transaction, replication, eviction policy, and cache loader, TreeCacheAop also provides transparent object cache retrieval and update for user-specified POJOs (configured in a jboss-aop.xml file) through plain get/set methods associated with the POJOs.

TreeCacheAop employs the JBoss standalone AOP framework in the JBossAop release to perform dynamic AOP interception. The framework provides declarative semantics (e.g., a jboss-aop.xml configuration) to "aspectize" POJOs. Once it is declared, a user will only need to initiate the transparent cache mechanism on that POJO by issuing a putObject(String fqn, Object pojo) method call first. After that, plain get/set methods from that POJO [e.g., getName(), setName(), etc], or direct field read and write operations, will be intercepted by the AOP framework, and the framework will, in turn, invoke the org.jboss.cache.aop.CacheInterceptor that will retrieve or update the object contents from the cache, respectively.

TreeCacheAop can also be used as a plain TreeCache. For example, if a POJO is not declared aop-enabled, a user will need to use the TreeCache API [e.g., get(String fqn) and set(String fqn, String key, String value)] to manage the cache states. Of course, users will need to consider the extra cost in doing this (instead of just doing plain TreeCache).

2. Feature

Here are the current features and benefits of TreeCacheAop:

  • Fine-grained replication. The replication mode supported is the same as that of the TreeCache: LOCAL, REPL_SYNC, and REPL_ASYNC. The replication in aop is fine-grained once the POJO is mapped into the internal cache store. When a POJO field is updated (that has been replicated) , a replication request will be sent out only to the node corresponding to that modified attribute (instead of the whole object). This can have potential performance boost during replication process, e.g., update a single key in a big HashMap will only replicate one single field instead of the whole map! Please see the documentation of JBossCache for more details on cache mode.

  • Transaction. The POJO operation can be transacted once a TransactionManager is specified properly. Upon user rollback, it will rollback all POJO operations as well. Note that the transaction syntax only applies to the node level though. That is, in a complex object graph where you have multiple sub-nodes, only the nodes (or fields) accessed by a user are under transaction context.

  • Eviction policy. TreeCacheAop supports eviction policy that can evict the whole POJO object (and any field object references, recursively). Currently there is a eviction policy class called org.jboss.cache.eviction.AopLRUPolicy (that is sub-class of org.jboss.cache.eviction.LRUPolicy). The configuration parameters is the same as that of the TreeCache counterpart. Note that, the concept of "Region" in eviction needs to be carefully defined at the top of the object FQN level. Otherwise, eviction policy will not operate correctly.

  • Object cache by reachability, i.e., recursive object mapping into the cache store. For example, if a POJO has a reference to another advised POJO, TreeCacheAop will transparently manage the sub-object states as well. During the initial putObject() call, TreeCacheAop will traverse the object tree and map it accordingly to the internal TreeCache nodes. This feature is explained in full details later.

  • Object reference handling. In TreeCacheAop, multiple and recursive object references are handled automatically. That is, a user does not need to declare any object relationship (e.g., one-to-one, or one-to-many) to use the cache. Therefore, there is no need to specify object relationship via xml file.

  • Automatic support of object identity. In TreeCacheAop, each object is uniquely identified by an internal FQN. Client can determine the object equality through the usual equal method. For example, an object such as Address may be multiple referenced by two Persons (e.g., joe and mary). The objects retrieved from joe.getAddress() and mary.getAddress() should be identical.

  • Inheritance relationship. TreeCacheAop preserves the POJO inheritance hierarchy after the object item is stored in the cache. For example, if a Student class inherits from a Person class, once a Student object is mapped to TreeCacheAop (e.g., putObject call), the attributes in base class Person is "aspectized" as well.

  • Support Collection classes (e.g., List, Set, and Map based objects) automatically without declaring them as aop-enabled. That is, you can use them either as a plain POJO or a sub-object to POJO without declaring them as "aspectized". In addition, it supports runtime swapping of the proxy reference as well.

  • Support pre-compiling of POJOs. The latest JBossAop has a feature to pre-compile (called aopc) and generate the byte code necessary for AOP system. By pre-compiling the user-specified POJOs, there is no need for additional declaration file (e.g., jboss-aop.xml) or specifying a JBossAop system classloader. A user can treat the pre-generated classes as regular ones and use TreeCacheAop in a non-intrusive way.

    This provides easy integration to existing Java runtime programs, eliminating the need for ad-hoc specification of a system class loader, for example. Please see the Ant build file (build.xml) under the standalone package for an example of pre-compiling.

  • Ease of use and transparency. Once a POJO is declared to be managed by cache (i.e., putObject() call), the POJO object is mapped into the cache store behind the scene. Client will have no need to manage any object relationship and cache contents synchronization.

3. Concepts

Following explains the concepts and top-level design consideration of TreeCacheAop.

3.1. Dynamic AOP interception

JBossAop provides an API (appendInterceptor) to add an interceptor at runtime. TreeCacheAop uses this feature extensively to provide user transparency. Every "aspectized" POJO class will have an associated org.jboss.aop.InstanceAdvisor instance. During a putObject(FQN fqn, Object pojo) operation (API explained below), TreeCacheAop will examine to see if there is already a org.jboss.cache.aop.CacheInterceptor attached. (Note that a CacheInterceptor is the entrance of TreeCacheAop to dynamically manage cache contents.) If it has not, one will be added to InstanceAdvisor object. Afterwards, any POJO field modification will invoke the corresponding CacheInterceptor instance. Below is a schematic illustration of this process.

The figures shown are operations to perform field read and write. Once a POJO is managed by cache (i.e., after a putObject method has been called), AOP will invoke CacheInterceptor automatically every time there is a field read or write. However, you should see the difference between these figures. While field write operation will go to cache first and, then, invoke the in-memory update, the field read invocation does not involve in-memory reference at all. This is because the value in cache and memory should have been synchronized during write operation. As a result, the field value from the cache is returned.

Figure 1. Dynamic AOP interception for field write

Dynamic AOP interception for field write

Figure 2. Dynamic AOP Interception for field read

Dynamic AOP Interception for field read

3.2. Object mapping by reachability

A complex object by definition is an object that may consist of composite object references. Once a complex object is declared "prepared" (e.g., a Person object), during the putObject(Fqn fqn, Object pojo) operation, TreeCacheAop will add a CacheInterceptor instance to the InstanceAdvisor associated with that object, as we have discussed above. In addition, the cache will map recursively the primitive object fields into the corresponding cache nodes.

The mapping rule is as follows:

  • Create a tree node using fqn, if not yet existed.

  • Go through all the fields (say, with an association java.lang.reflect.Field type field) in POJO,

    • If it is a primitive type, the field value will be stored under fqn with (key, value) pair of (field.getName(), field.getValue()). The following are primitive types supported now: String, Boolean, Double, Float, Integer, Long, Short, Character, Boolean.

    • If it is a non-primitive type, creates a child FQN and then recursively executes another pubObject until it reaches all primitive types.

Following is a code snippet that illustrates this mapping process

         for (Iterator i = type.getFields().iterator(); i.hasNext();) {
            Field field = (Field) i.next();
            Object value = field.get(obj);
            CachedType fieldType = getCachedType(field.getType());
            if (fieldType.isImmediate()) {
               immediates.put(field.getName(), value);
            } else {
               putObject(new Fqn(fqn, field.getName()), value);
            }
         }

Let's take an example POJO classes definition from Example POJO section below where we have a Person object that has composite non-primitive types (e.g., Set and Address). After we execute the pubObject call, the resulting tree node will schematically look like the cache node in the following figures after we have executed calls:

Person joe = new Person();
joe.setAddress(new Address());

cache.putObject("/aop/joe", joe);

The TreeCacheAop APIs will be explained in fuller details later. But notice the illustration of object mapping by reachability in the following figure.

Figure 3. Object mapping by reachability

Object mapping by reachability

Under the fqn "/aop/joe", there are three children nodes: addr, skill, and language. If you look at the Person class declaration, you will find that addr is an Address class, skill is a Set, and language is a List type. Since they are non-primitive, they are recursively inserted under the parent object (joe) until all primitive types are reached. In this way, we have broken down the object graph into a tree view which fit into our internal structure nicely. Also note that all the primitive types will be stored as a Map inside the respective node (e.g., addr will have Zip, Street, etc. store there).

Here is a code snippet to demonstrate the object mapping by reachability feature. Notice how a Person object (e.g., joe) that has complex object references will be mapped into the underlying cache store as explained above.

import org.jboss.cache.PropertyConfigurator;
import org.jboss.cache.aop.TreeCacheAop;
import org.jboss.test.cache.test.standAloneAop.Person;
import org.jboss.test.cache.test.standAloneAop.Address;

TreeCacheAop tree = new TreeCacheAop();
PropertyConfigurator config = new PropertyConfigurator(); // configure tree cache.
config.configure(tree, "META-INF/replSync-service.xml");

Person joe = new Person(); // instantiate a Person object named joe
joe.setName("Joe Black");
joe.setAge(31);

Address addr = new Address(); // instantiate a Address object named addr
addr.setCity("Sunnyvale");
addr.setStreet("123 Albert Ave");
addr.setZip(94086);
joe.setAddress(addr); // set the address reference

tree.startService(); // kick start tree cache
tree.putObject("/aop/joe", joe); // add aop sanctioned object (and sub-objects) into cache.
// since it is aspectized, use of plain get/set methods will take care of cache contents automatically.
joe.setAge(41);

Note that a typical TreeCacheAop usage involves instantiating the TreeCacheAop, configuring, and starting the cache instance. Then, a user creates the aspectized POJO that will be put into the cache using putObject() API.

In addition, TreeCacheAop also supports get/set with parameter type of some Collection classes (i.e., List, Map, and Set). For example, the following code snippet in addition to the above example will trigger TreeCacheAop to manage the states for the Languages list as well:

ArrayList lang = new ArrayList();
lang.add("Ensligh");
lang.add("Mandarin");
joe.setLanguages(lang);

Figure 4. Schematic illustration of List class mapping

Schematic illustration of List class mapping

3.3. Object relationship management

During the mapping process, we will also need to check whether any of its associated object is multiple or circular referenced. A reference counting mechanism has been implemented associating with the CacheInterceptor. If the cache detects an object that has been referenced more than twice for the first time, it will re-locate the current object node to an internal area. Afterwards, all object node will be referenced indirectly to there.

To look at one example, let's say that multiple Person objects can own the same Address (e.g., a household). Graphically, here is what it will look like in the tree nodes:

Figure 5. Schematic illustration of object graph handling

Schematic illustration of object graph handling

Notice in the addr node for both joe and mary, they are grayed out and have dashed arrows pointing toward an internal addr node instead.

In the following code snippet, we show how a TreeCacheAop can handle multiple object references, namely, an Address can be shared among multiple Person objects (e.g., joe and mary).

import org.jboss.cache.PropertyConfigurator;
import org.jboss.cache.aop.TreeCacheAop;
import org.jboss.test.cache.test.standAloneAop.Person;
import org.jboss.test.cache.test.standAloneAop.Address;

TreeCacheAop tree = new TreeCacheAop();
PropertyConfigurator config = new PropertyConfigurator(); // configure tree cache.
config.configure(tree, "META-INF/replSync-service.xml");

Person joe = new Person(); // instantiate a Person object named joe
joe.setName("Joe Black");
joe.setAge(31);

Person mary = new Person(); // instantiate a Person object named mary
mary.setName("Mary White");
mary.setAge(30);

Address addr = new Address(); // instantiate a Address object named addr
addr.setCity("Sunnyvale");
addr.setStreet("123 Albert Ave");
addr.setZip(94086);

joe.setAddress(addr); // set the address reference
mary.setAddress(addr); // set the address reference

tree.startService(); // kick start tree
tree.putObject("/aop/joe", joe); // add aop sanctioned object (and sub-objects) into cache.
tree.putObject("/aop/mary", mary); // add aop sanctioned object (and sub-objects) into cache.

Address joeAddr = joe.getAddress();
Address maryAddr = mary.getAddress(); // joeAddr and maryAddr should be the same

tree.removeObject("/aop/joe");
maryAddr = mary.getAddress(); // Should still have the address.

Notice that after we remove joe instance from the cache, mary should still have reference the same Address object in the cache store.

3.4. Object inheritance hierarchy

TreeCacheAop preserves the POJO object inheritance hierarchy automatically. For example, if a Student extends Person with an additional field year (see following POJO example), then once Student is put into the cache, all the base class attributes of Person will be managed as well.

Following is a code snippet that illustrates how the inheritance behavior of a POJO is maintained. Again, no special configuration is needed.

import org.jboss.test.cache.test.standAloneAop.Student;

Student joe = new Student();  // Student extends Person class
joe.setName("Joe Black"); // This is base class attributes
joe.setAge(22);  // This is also base class attributes
joe.setYear("Senior"); // This is Student class attribute

tree.putObject("/aop/student/joe", joe);

//...

joe = (Student)tree.putObject("/aop/student/joe");
Person person = (Person)joe; // it will be correct here
joe.setYear("Junior"); // will be intercepted by the cache
joe.setName("Joe Black II"); // also intercepted by the cache

3.5. Collection class proxy

The POJO classes that inherits from Set, List, and Map are treated as "aspectized" automatically. That is, users need not declare them "prepared" in the xml configuration file. Rather, upon a putObject call (note that this call returns the old object value instead) is issued, it will dynamically generate a proxy that is "aspectized". Users can then use another getObject to retrieve this proxy reference and use this reference to perform POJO operations.

In addition, current implementation has the capability to swap out the original Collection class reference with the corresponding proxy reference. For example, here is a code snippet that illustrates this:

Person joe = new Person();
joe.setName("Joe Black"); // This is base class attributes
ArrayList lang = new ArrayList();
lang.add("English");
lang.add("Mandarin");
joe.setLanguages(lang);
// This will map the languages List automatically and swap it out with the proxy reference.
tree.putObject("/aop/student/joe", joe);
ArrayList lang = joe.getLanguages(); // Note that lang is a proxy reference
lang.add("French"); // This will be intercepted by the cache

As you can see, getLanguages simply returns the field reference that has been swapped out for the proxy reference counterpart.

4. Requirement

TreeCacheAop requires the following libraries (in addition to jboss-cache.jar and the required libraries for the plain TreeCache), and specific class loader (if it is not pre-compiled) during start up:

  • Library: jboss-aop.jar, trove.jar, and javassist.jar.

  • Classloader: To run under the JBoss standalone Aop framework without pre-compiling, you will need to use the AOP system class loader, i.e., you will need to specify the class loader during start up as: -Djava.system.class.loader=org.jboss.aop.standalone.SystemClassLoader.

5. API

There are 3 core APIs for TreeCacheAop:

  • Object putObject(String fqn, Object pojo) where fqn is a user-specified fully qualified name (FQN) to store the node in the underlying cache, e.g., "/aop/joe", and pojo is the object instance to be managed by TreeCacheAop.

    If pojo has sub-objects, e.g., it has fields that are non-primitive type, this call will issue putObject recursively until all object tree are traversed. In addition, if you put pojo in multiple times, it will simply returns the original object reference right away. Note that this call is necessary for pojo to be managed by TreeCacheAop.

    The return value after the call is the existing object under fqn (if any). As a result, a successful call will replace that old value with pojo instance, if it exists. Note that a user will only need to issue this call once for each pojo. Once it is executed, TreeCacheAop will assign an interceptor for the pojo instance and its sub-objects.

  • Object getObject(String fqn). This call will return the current object content located under fqn. This method call is useful when you start a replicated node and you want to get the object reference first, for example. In that case, TreeCacheAop will create a new Java object and then add the cache interceptor such that every future access will be in sync with the underlying cache store.

  • Object removeObject(String fqn). This call will remove the contents under fqn and return the POJO instance stored there (null if it doesn't exist). Note this call will also remove everything stored under fqn and, in addition, it will remove the associated cache interceptor. The resulting pojo will not be managed by the cache anymore.

6. Configuration

Since TreeCacheAop inherits from TreeCache, the xml configuration file attributes are almost identical to that of the later. Attributes such as replication mode, transaction manager, eviction policy, cache loader, and JGroups stack, for example, are still the same. There are two differences, however, when using the xml file--- configuring as a MBean service and eviction policy.

6.1. TreeCacheAop MBean service

TreeCacheAop can also be deployed as a MBean service under JBoss Application Server. However, you will need to use the correct class to instantiate. For example, this is the code snippet for the MBean attribute in the xml file:

<mbean code="org.jboss.cache.aop.TreeCacheAop" name="jboss.cache:service=TreeCacheAop">

You can modify the object service name to your liking, of course.

6.2. TreeCacheAop eviction policy

TreeCacheAop also provides an eviction policy, org.jboss.cache.eviction.AopLRUPolicy, that is a subclass of org.jboss.cache.eviction.LRUPolicy (with the same configuration parameters). The reason we need a distinctive implementation is because eviction in TreeCacheAop is quite different from the regular TreeCache. In the plain cache world, a unit is a FQN node, while in the aop world, the concept of a unit is an object (which can have multiple nodes and children nodes!).

In addition, once a user obtains a POJO reference, everything is supposed to be transparent, e.g., cache retrieval and update operations. But if an object is evicted, that means there is no CacheInterceptor for the POJO, and the contents are not intercepted by the cache. Instead, every operation access will be channeled to the in-memory reference. So all operations will succeed but then a user has no way of knowing that it is merely updating the in-memory reference!

To remedy this problem, we could have thrown a runtime exception when a user is accessing an "evicted" node. But this is intrusive and not ideal. What we should do then is to evict an object (by removing all the nodes and children nodes). But we leave the CacheInterceptor for that POJO (and any sub-POJOs) alone. This way, when a user is using the POJO methods, it will still get intercepted by the CacheInterceptor. And if it finds that the node is empty, then it will also check to see if the eventual invocation from the in-memory reference is null or not. If not null, we know this is an evicted node and we will need to populate this object in TreeCacheAop based on the in-memory value.

6.3. JBossAop-specific configuration

In addition to the TreeCache-specific configuration xml file, you will also need a META-INF/jboss-aop.xml file located under the class path, unless you use aopc to pre-compile the byte code. JBossAOP framework will read this file during startup to make necessary byte code manipulation for advice and introduction. You will need to declare any of your POJO to be "prepared" so that AOP framework knows to start intercepting either method, field, or constructor invocations.

The standalone JBossCache distribution package provides an example declaration for the tutorial classes, namely, Person and Address. Detailed class declaration for Person and Address are provided in the next section. But here is the snippet for META-INF/jboss-aop.xml:

<aop>
  <prepare expr="field(* $instanceof{org.jboss.test.cache.test.standAloneAop.Address}->*)" />
  <prepare expr="field(* $instanceof{org.jboss.test.cache.test.standAloneAop.Person}->*)" />
</aop>

Detailed semantics of jboss-aop.xml can be found in JBossAop. But above statements basically declare all field read and write operations will be "prepared" (or "aspectized").

Note that in the next release of JBossAop, it will support the "prepare" expression through annotation. Therefore, a user can specify the expression inside the POJO class without the need of specifying the xml file again.

As already mentioned, a user can use the aop precompiler (aopc) to precompile the POJO classes such that during runtime, there is no additional system class loader needed. The precompiler will read in jboss-aop.xml and weave the POJO byte code at compile time. This is a convenient feature to make the aop less intrusive. In the JBossCache distribution, there is a sample "aopc" Ant target in the build.xml that illustrates how to invoke aopc from command line.

7. Example POJO

The example POJO classes used for Tutorial are: Person and Address. Here is the snippet of the class definition for Person and Address (note that neither class implements Serializable).

public class Person {
   String name=null; 
   int age=0; 
   Map hobbies=null; 
   Address address=null; 
   Set skills; 
   List languages; 
   
   public String getName() { return name; } 
   public void setName(String name) { this.name=name; } 

   public int getAge() { return age; }
   public void setAge(int age) { this.age = age; }

   public Map getHobbies() { return hobbies; }
   public void setHobbies(Map hobbies) { this.hobbies = hobbies; }

   public Address getAddress() { return address; }
   public void setAddress(Address address) { this.address = address; }

   public Set getSkills() { return skills; }
   public void setSkills(Set skills) { this.skills = skills; }

   public List getLanguages() { return languages; }
   public void setLanguages(List languages) { this.languages = languages; }
}
public class Student extends Person {
   String year=null;

   public String getYear() { return year; }
   public void setYear(String year) { this.year=year; }
}
public class Address {
   String street=null; 
   String city=null; 
   int zip=0; 

   public String getStreet() { return street; } 
   public void setStreet(String street) { this.street=street; } 
   ...
}

8. Limitations and Problems

Here are some of the current limitation in TreeCacheAop.

  • Currently, plain TreeCache and TreeCacheAop can share the same fqn name, i.e., the same node. However, if you do remove the node, you will remove any contents associated with that nodes, say, both remove(String fqn) and removeObject(String fqn, Object pojo) method calls. Both method calls currently do not differentiate the content type. This limit will be removed in the future release.

  • Currently, there is an outstanding bug that requires that if you have a HashMap object that you would like to be "aspectized" (i.e., use putObject operation), then your key object can be non-primitive only when you don't over-ride the equals method. If you over-ride it in your key non-primitive object, you will encounter an infinite loop. Fix is planned in the near future.