JBoss.org Community Documentation
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.
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).
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:
private
, all protected
, all default, and all public
fields will be intercepted.
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.
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.
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!
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
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.
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.
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.
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.
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:
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.
The current implementation has the following limitations with collections: