Chapter 3. Architecture
Prev |  | Next

## Chapter 3. Architecture

POJO Cache internally uses the JBoss Aop framework to both intercept object field access, and to provide an internal interceptor stack for centralizing common behavior (e.g. locking, transactions).

The following figure is a simple overview of the POJO Cache architecture. From the top, it can be can seen that when a call comes in (e.g., `attach` or `detach`), it will go through the POJO Cache interceptor stack first. After that, it will store the object's fields into the underlying Core Cache, which will be replicated (if enabled) using JGroups.

Figure 3.1. POJO Cache architecture overview

## 3.1. POJO Cache interceptor stack

As mentioned, the JBoss Aop framework is used to provide a configurable interceptor stack. In the current implementation, the main POJO Cache methods have their own independant stack. These are specified in `META-INF/pojocache-aop.xml` In most cases, this file should be left alone, although advanced users may wish to add their own interceptors. The Following is the default configuration:

       <!-- Check id range validity -->
       <interceptor name="CheckId" class="org.jboss.cache.pojo.interceptors.CheckIdInterceptor"
             scope="PER_INSTANCE"/>

       <!-- Track Tx undo operation -->
       <interceptor name="Undo" class="org.jboss.cache.pojo.interceptors.PojoTxUndoInterceptor"
             scope="PER_INSTANCE"/>

       <!-- Begining of interceptor chain -->
       <interceptor name="Start" class="org.jboss.cache.pojo.interceptors.PojoBeginInterceptor"
             scope="PER_INSTANCE"/>

       <!-- Check if we need a local tx for batch processing -->
       <interceptor name="Tx" class="org.jboss.cache.pojo.interceptors.PojoTxInterceptor"
             scope="PER_INSTANCE"/>

       <!--
          Mockup failed tx for testing. You will need to set PojoFailedTxMockupInterceptor.setRollback(true)
          to activate it.
       -->
       <interceptor name="MockupTx" class="org.jboss.cache.pojo.interceptors.PojoFailedTxMockupInterceptor"
             scope="PER_INSTANCE"/>

       <!-- Perform parent level node locking -->
       <interceptor name="TxLock" class="org.jboss.cache.pojo.interceptors.PojoTxLockInterceptor"
             scope="PER_INSTANCE"/>

       <!-- Interceptor to perform Pojo level rollback -->
       <interceptor name="TxUndo" class="org.jboss.cache.pojo.interceptors.PojoTxUndoSynchronizationInterceptor"
                    scope="PER_INSTANCE"/>

       <!-- Interceptor to used to check recursive field interception. -->
       <interceptor name="Reentrant" class="org.jboss.cache.pojo.interceptors.MethodReentrancyStopperInterceptor"
                    scope="PER_INSTANCE"/>

       <!-- Whether to allow non-serializable pojo. Default is false. -->
       <interceptor name="MarshallNonSerializable" class="org.jboss.cache.pojo.interceptors.CheckNonSerializableInterceptor"
                    scope="PER_INSTANCE">
             <attribute name="marshallNonSerializable">false</attribute>
                    </interceptor>

       <stack name="Attach">
          <interceptor-ref name="Start"/>
          <interceptor-ref name="CheckId"/>
          <interceptor-ref name="Tx"/>
          <interceptor-ref name="TxLock"/>
          <interceptor-ref name="TxUndo"/>
       </stack>

       <stack name="Detach">
          <interceptor-ref name="Start"/>
          <interceptor-ref name="CheckId"/>
          <interceptor-ref name="Tx"/>
          <interceptor-ref name="TxLock"/>
          <interceptor-ref name="TxUndo"/>
       </stack>

       <stack name="Find">
          <interceptor-ref name="Start"/>
          <interceptor-ref name="CheckId"/>
       </stack>

The stack should be self-explanatory. For example, for the `Attach` stack, we currently have `Start, CheckId, Tx, TxLock`, and `TxUndo` interceptors. The stack always starts with a `Start` interceptor such that initialization can be done properly. `CheckId` is to ensure the validity of the Id (e.g., it didn't use any internal Id string). Finally, `Tx, TxLock`, and `TxUndo` are handling the the proper transaction locking and rollback behavior (if needed).

## 3.2. Field interception

POJO Cache currently uses JBoss AOP to intercept field operations. If a class has been properly instrumented (by either using the `@Replicable` annotation, or if the object has already been advised by JBoss AOP), then a cache interceptor is added during an `attach()` call. Afterward, any field modification will invoke the corresponding `CacheFieldInterceptor` instance. Below is a schematic illustration of this process.

Only fields, and not methods are intercepted, since this is the most efficient and accurate way to gaurantee the same data is visible on all nodes in the cluster. Further, this allows for objects that do not conform to the JavaBean specficiation to be replicable. There are two important aspects of field interception:

- All access qualifiers are intercepted. In other words, all `private`, all `protected`, all default, and all `public` fields will be intercepted.
- Any field with `final`, `static`, and/or `transient` qualifiers, will be skipped. Therefore, they will not be replicated, passivated, or manipulated in any way by POJO Cache.

The figure below illustrates both field read and write operations. Once an POJO is managed by POJO Cache (i.e., after an `attach()` method has been called), JBoss Aop will invoke the `CacheFieldInterceptor` every time a class operates on a field. The cache is always consulted, since it is in control of the mapped data (i.e. it gaurantess the state changes made by other nodes in the cluster are visible). Afterwords, the in-memmory copy is updated. This is mainly to allow transaction rollbacks to restore the previous state of the object.

Figure 3.2. POJO Cache field interception

## 3.3. Object relationship management

As previously mentioned, unlike a traditional cache system, POJO Cache preserves object identity. This allows for any type of object relationship available in the Java language to be transparently handled.

During the mapping process, all object references are checked to see if they are already stored in the cache. If already stored, instead of duplicating the data, a reference to the original object is written in the cache. All referenced objects are reference counted, so they will be removed once they are no longer referenced.

To look at one example, let's say that multiple `Person`s ("joe" and "mary") objects can own the same `Address` (e.g., a household). The following diagram is a graphical representation of the pysical cache data. As can be seen, the "San Jose" address is only stored once.

Figure 3.3. Schematic illustration of object relationship mapping

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

    import org.jboss.cache.pojo.PojoCache;
    import org.jboss.cache.pojo.PojoCacheFactory;
    import org.jboss.test.cache.test.standAloneAop.Person;
    import org.jboss.test.cache.test.standAloneAop.Address;

    String configFile = "META-INF/replSync-service.xml";
    PojoCache cache = PojoCacheFactory.createCache(configFile); // This will start PojoCache automatically

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

    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

    cache.attach("pojo/joe", joe); // add aop sanctioned object (and sub-objects) into cache.
    cache.attach("pojo/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

    cache.detach("pojo/joe");
    maryAddr = mary.getAddress(); // Should still have the address.

If `joe` is removed 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 two separate cache instances in the cluster now (`cache1` and `cache2`). On the first cache instance, both `joe` and `mary` are attached as above. Then, the application fails over to `cache2.` Here is the code snippet for `cache2` (assume the objects were already attached):

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

    String configFile = "META-INF/replSync-service.xml";
    PojoCache cache2 = PojoCacheFactory.createCache(configFile); // This will start PojoCache automatically

    Person joe = cache2.find("pojo/joe"); // retrieve the POJO reference.
    Person mary = cache2.find("pojo/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!

## 3.4. Object Inheritance

POJO Cache preserves the inheritance hierarchy of all attached objects. For example, if a `Student` extends `Person` with an additional field `year`, then once `Student` is put into the cache, all the class attributes of `Person` are mapped to the cache 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

    cache.attach("pojo/student/joe", joe);

    //...

    joe = (Student)cache.attach("pojo/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. Physical object cache mapping model

The previous sections describe the logical object mapping model. In this section, we will explain the physical mapping model, that is, how do we map the POJO into Core Cache for transactional state replication. However, it should be noted that the physical structure of the cache is purely an internal implementation detail, it should not be treated as an API as it may change in future releases. This information is provided solely to aid in better understanding the mapping process in POJO Cache.

When an object is first attached in POJO Cache, the Core Cache node representation is created in a special internal area. The `Id` fqn that is passed to `attach()` is used to create an empty node that references the internal node. Future references to the same object will point to the same internal node location, and that node will remain until all such references have been removed (detached).

The example below demonstrates the mapping of the `Person` object under id "pojo/joe" and "pojo/mary" as metioned in previous sections. It is created from a two node replication group where one node is a Beanshell window and the other node is a Swing Gui window (shown here). For clarity, multiple snapshots were taken to highlight the mapping process.

The first figure illustrates the first step of the mapping approach. From the bottom of the figure, it can be seen that the `PojoReference` field under `pojo/joe` is pointing to an internal location, `/__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49n5o-2`. That is, under the user-specified Id string, we store only an indirect reference to the internal area. Please note that `Mary` has a similar reference.

Figure 3.4. Object cache mapping for `Joe`

Figure 3.5. Object cache mapping for `Mary`

Then by clicking on the referenced internal node (from the following figure), it can seen that the primitive fields for `Joe` are stored there. E.g., `Age` is `41` and `Name` is `Joe Black`. And similarly for `Mary` as well.

Figure 3.6. Object cache mapping for internal node `Joe`

Figure 3.7. Object cache mapping for internal node `Mary`

Under the `/__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49n5o-2`, it can be seen that there is an `Address` node. Clicking on the `Address` node shows that it references another internal location: `/__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49ngs-3` as shown in the following figure. Then by the same token, the `Address` node under `/__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49na0-4` points to the same address reference. That is, both `Joe` and `Mary` share the same `Address` reference.

Figure 3.8. Object cache mapping: Joe's internal address

Figure 3.9. Object cache mapping: Mary's internal address

Finally, the `/__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49ngs-3` node contains the various various primitive fields of `Address`, e.g., `Street`, `Zip`, and `City`. This is illustrated in the following figure.

Figure 3.10. Object cache mapping: Address fields

## 3.6. Collection Mapping

Due to current Java limitations, Collection classes that implement `Set`, `List`, and `Map` are substituted with a Java proxy. That is, whenever POJO Cache encounters any Collection instance, it 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 referenced from another object, POJO Cache will swap out the original reference with the new proxy, so that operations performed by the refering object will be picked up by the cache.

The drawback to this approach is that the calling application must re-get any collection references that were attached. Otherwise, the cache will not be aware of future changes. If the collection is referenced from another object, then the calling app can obtain the proxy by using the publishing mechanism provided by the object (e.g. Person.getHobbies()). If, however, the collection is directly attached to the cache, then a subsequent `find()` call will need to be made to retrieve the proxy.

The following code snippet illustrates obtaining a direct Collection proxy reference:

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

    cache.attach("pojo/list", list);
    list.add("THREE"); // This won't be intercepted by the cache!

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

This snippet illustrates obtaining the proxy reference from a refering object:

    Person joe = new Person();
    joe.setName("Joe Black"); // This is base class attributes
    List 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.
    cache.attach("pojo/student/joe", joe);
    lang = joe.getLanguages(); // Note that lang is now a proxy reference
    lang.add("French"); // This will be intercepted by the cache

Finally, when a Collection is removed from the cache (e.g., via `detach`), you still can use the proxy reference. POJO Cache will just redirect the call back to the in-memory copy. See below:

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

    cache.attach("pojo/list", list);
    List proxyList = cache.find("pojo/list"); // Note that list is a proxy reference
    proxyList.add("THREE"); // This will be intercepted by the cache

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

### 3.6.1. Limitations

The current implementation has the following limitations with collections:

- Only List, Set and Map are supported. Also it should be noted that the Java Collection API does not fully describe the behavior of implementations, so the cache versions may differ slightly from the common Java implementations (e.g. handling of NULL)
- As of PojoCache 2.0, HashMap keys must be serializable. Prior to PojoCache 2.0, HashMap keys were converted to String. This was fixed as you couldn't get the key back in its original form. See issue JBCACHE-399 for more details.

Prev |  | Next
Chapter 2. Introduction | Home | Chapter 4. API Overview