Chapter 4. Architecture

Following explains the concepts and top-level design of PojoCache.

4.1. Dynamic AOP interception

JBossAop provides an API (appendInterceptor) to add an interceptor at runtime. PojoCache 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), PojoCache will examine to see if there is already a org.jboss.cache.aop.CacheInterceptor attached. (Note that a CacheInterceptor is the entrance of PojoCache to dynamically manage cache contents.) If it has not, one will be added to InstanceAdvisor object. Afterward, any POJO field modification will invoke the corresponding CacheInterceptor instance. Below is a schematic illustration of this process.

JBossAop has the capability to intercept both method level call and field level read write. From the perspective of PojoCache, field level interception is the appropriate mechanism to synchronize with the backend cache store. Please note that,

  • the filed level interception applies to all access qualifiers. That is, regardless whether it is public, protected, or private
  • we skip interception for field with final, static, and transient qualifiers. As a result, any field with these 3 qualifiers will not be replicated or persisted.

The figures shown below illustrate 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.

Dynamic AOP interception for field write

Figure 4.1. Dynamic AOP interception for field write

Dynamic AOP Interception for field read

Figure 4.2. Dynamic AOP Interception for field read

4.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, PojoCache 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.

    • 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 class definition from the Appendix section where we have a Person object that has composite non-primitive types (e.g., List and Address). After we execute the putObject call, the resulting tree node will schematically look like the cache node in the following figures:

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

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

The PojoCache APIs will be explained in fuller details later. But notice the illustration of object mapping by reachability in the following figure. The fqn /aop/joe is associated with the POJO joe. Then under that fqn, there are three children nodes: addr, skills, and languages. If you look at the Person class declaration, you will find that addr is an Address class, skills is a Set , and languages 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 inside the respective node's HashMap (e.g., addr will have Zip , Street , etc. stored there).

Object mapping by reachability

Figure 4.3. Object mapping by reachability

Here is a code snippet to demonstrate the object mapping by reachability feature that we just explained. 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.PojoCache;
import org.jboss.test.cache.test.standAloneAop.Person;
import org.jboss.test.cache.test.standAloneAop.Address;

PojoCache tree = new PojoCache();
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 PojoCache usage involves instantiating the PojoCache , 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, PojoCache also supports get/set with parameter type of some Collection classes (i.e., List , Map , and Set ) automatically. For example, the following code snippet in addition to the above example will trigger PojoCache to manage the states for the Languages list as well. The following figure illustrates the node structure from the replicated GUI point of view. Details of Collection class support will be given later.

ArrayList lang = new ArrayList();
lang.add("Ensligh");
lang.add("Mandarin");
joe.setLanguages(lang);
Schematic illustration of List class mapping

Figure 4.4. Schematic illustration of List class mapping

4.3. Object relationship management

Like we have mentioned, traditional cache system does not support object relationship management during serialization (be it to the persistent data store or replicated to the other in-memory nodes.) Examples of object relationship are like an address object is shared by members of the household, and a child-parent relationship. All these relationship will be lost once the objects are replicated or persisted. As a result, explicit mapping will be needed outside of the cache system to express the object relationship. PojoCache, in contrast, can manage object relationship transparently for users.

During the mapping process, we will check whether any of its associated object is multiple or circular referenced. A reference counting mechanism has been implemented associating with the CacheInterceptor. If a new object created in the cache referenced to another POJO, a referenced fqn will be stored there to redirect any query and update to the original node.

To look at one example, let's say that multiple Persons ("joe" and "mary") objects can own the same Address (e.g., a household). Graphically, here is what it will look like in the tree nodes. Like we have covered in the previous section on the mapping by reachability, the POJO will map recursively into the cache. However, when we detect a multiple reference (in this case, the Address), we will keep track of the reference counting for the sub-object addr.

Schematic illustration of object relationship mapping

Figure 4.5. Schematic illustration of object relationship mapping

In the following code snippet, we show programmatically the object sharing example.

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

PojoCache tree = new PojoCache();
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 instance

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.

To further illustrate this relationship management, let's examine the Java code under a replicated environment. Imagine that we have two separate cache instances in the cluster now (cache1 and cache2). Let's say, on the first cache instance, we put both joe and mary under cache management as above. Then, we failover to cache2. Here is the code snippet:

/**
 * Code snippet on cache2 during fail-over
 */
import org.jboss.cache.PropertyConfigurator;
import org.jboss.cache.aop.PojoCache;
import org.jboss.test.cache.test.standAloneAop.Person;
import org.jboss.test.cache.test.standAloneAop.Address;

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

tree.startService(); // kick start tree
Person joe = tree.getObject("/aop/joe"); // retrieve the POJO reference.
Person mary = tree.getObject("/aop/mary"); // retrieve the POJO reference.

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

maryAddr = mary.getAddress().setZip(95123);
int zip = joeAddr.getAddress().getZip(); // Should be 95123 as well instead of 94086!

4.4. Object inheritance hierarchy

PojoCache preserves the POJO object inheritance hierarchy automatically. For example, if a Student extends Person with an additional field year (see POJO definition in the Appendix section), then once Student is put into the cache, all the class attributes of Person can 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

4.5. Collection class proxy

The POJO classes that inherit from Set , List , and Map are treated as "aspectized" automatically. That is, users need not declare them "prepared" in the xml configuration file or via annotation. Since we are not allowed to instrument the Java system library, we will use a proxy approach instead. That is, when we encounter any Collection instance, we will:

  • Create a Collection proxy instance and place it in the cache (instead of the original reference). The mapping of the Collection elements will still be carried out recursively as expected.
  • If the Collection instance is a sub-object, e.g., inside another POJO, we will swap out the original reference with the new proxy one to promote transparent usage.

To obtain the proxy reference, users can then use another getObject to retrieve this proxy reference and use this reference to perform POJO operations.

Here is a code snippet that illustrates the usage of a Collection proxy reference:

List list = new ArrayList();
list.add("ONE");
list.add("TWO");

tree.putObject("/aop/list", list);
list.add("THREE"); // This won't intercept by the cache!

List proxyList = tree.getObject("/aop/list"; // Note that list is a proxy reference
proxyList.add("FOUR"); // This will be intercepted by the cache

Here is another snippet to illustrate the dynamic swapping of the Collection reference when it is embedded inside another object:

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.

Finally, when you remove a Collection reference from the cache (e.g., via removeObject), you still can use the proxy reference since we will update the in-memory copy of that reference during detachment. Below is a code snippet illustrating this:

List list = new ArrayList();
list.add("ONE");
list.add("TWO");

tree.putObject("/aop/list", list);
List proxyList = tree.getObject("/aop/list"); // Note that list is a proxy reference
proxyList.add("THREE"); // This will be intercepted by the cache

tree.removeObject("/aop/list"); // detach from the cache
proxyList.add("FOUR"); // proxyList has 4 elements still.

4.5.1. Limitation

Use of Collection class in PojoCache helps you to track fine-grained changes in your collection fields automatically. However, current implementation has the follow limitation that we plan to address soon.

Currently, we only support a limited implementation of Collection classes. That is, we support APIs in List, Set, and Map. However, since the APIs do not stipulate of constraints like NULL key or value, it makes mapping of user instance to our proxy tricky. For example, ArrayList would allow NULL value and some other implementation would not. The Set interface maps to java.util.HashSet implementation. The List interface maps to java.util.ArrayList implementation. The Map interface maps to java.util.HashMap implementation.

Another related issue is the expected performance. For example, the current implementation is ordered, so that makes insert/delete from the Collection slow. Performance between Set, Map and List collections also vary. Adding items to a Set is slower than a List or Map, since Set does not allow duplicate entries.