The JBossCache component, TreeCacheAop, is a replicated and transactional "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 "advised" 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, and eviction policy, 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 JBoss4.0 release to perform dynamic AOP interception. The framework provides declarative semantics (e.g., a jboss-aop.xml configuration) to "advise" 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.
Here are the current features and benefits of TreeCacheAop:
Replication, transaction, and eviction policy. All three aspects can be configured through a TreeCache/Aop configuration xml file. Please see the documentation of JBossCache for details.
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.
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.
Replication on modified attribute nodes only (as in per-field update basis), e.g., when an object is put into TreeCacheAop , a replication request will be sent out only when any attribute is modified and only the node corresponds to that modified attribute. 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 map!
Support 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 advisable.
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. 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 not need to manage any object relationship and cache contents synchronization.
Following explains the concepts and top-level design consideration of TreeCacheAop.
JBossAop provides a mechanism to add an interceptor at runtime. TreeCacheAop uses this feature extensively to provide user transparency. Every "advised" 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, as you can see the difference between these figures, while field write operation will go to cache first before, eventually, invoke the in-memory update, the field read invocation does not involve in-memory reference at all, since the value in cache and memory should be synchronized. Instead, the field value from the cache is returned.
A complex object by definition is an object that may consist of composite object references. Once a complex object is declared "prepare" (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.
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 advisable, 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, configuration, and start the cache instance. Then, a user creates the advisable 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 Lanugages list as well:
ArrayList lang = new ArrayList(); lang.add("Ensligh"); lang.add("Mandarin"); joe.setLanguages(lang);
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 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, we 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:
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.
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.
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
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 per-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.
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.
This call returns the existing object under fqn. 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.
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.
In addition to the TreeCache 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). JBoss AOP 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 "advisable" so that AOP framework knows to start intercepting either method, field, or constructor invocations. The standalone TreeCacheAop 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 will be "advised".
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; } ... }
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. Eviction in TreeCacheAop is quite different in that in aop world, first of all the concept of a unit is object (which can have multiple nodes and children nodes).
Second of all, 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, everything will be channeled to the in-memory reference. Then a user has no way of knowing this fact!
We could have thrown a runtime exception when a user is accessing a "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.
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.