Wednesday, December 30, 2009

I don't know which cache this Object goes to

..This was something that came up during a sluggish talk at a customer's site. A developer had just jokingly remarked.

Problem Statement: How to make Coherence objects destination aware?

One way is to annotate the objects that are to be cached. So lets create some simple annotations:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target ({ElementType.TYPE, ElementType.PACKAGE})
public @interface Cache {
String name();
int ttl() default -1;
String pk() default "getId";
}
All this annotation is telling is the name of the cache, what should be the TTL (default being -1) and how to find the primary key for this object? The annotation only complements the coherence-cache-config.xml and does not replace it (Unlike JPA annotations). You still need to define all cache names in the configuration file. Next step is to create a sample domain object annotated with Cache:

@Cache(name = "CacheDestination", ttl=-1, pk="getId")
public class Domain implements Serializable {
private Key id;
private int age;
private String name;

public Domain() {
}

public void setId(Key id) {
this.id = id;
}

public Key getId() {
return Key;
}

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

public int getAge() {
return age;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

@Override
public String toString () {
return id + ":" + age + ":" + name;
}
}
Unfortunately this destination aware class will not do anything unless the class that puts this Object in the cache extracts the destination. Cache annotation also tells which method to call to find it's primary key, in this sample it is getId () (Remember JPA?). For developers who do not want to "know" which cache to put this object to, need an Abstraction layer. Quite a few customers that I have worked with try to create an Abstraction layer on top of Coherence to make "all caching" logic to be centralized in one layer. Coherence APIs are already highly abstracted. NamedCache is probably the most important interface in all of Coherence APIs. NamedCache already extends java.util.Map. So having an abstraction layer that returns an instance of NamedCache is good enough for Map centric applications. Problem is that NamedCache is not just a Map. It also provides quereablility feature on top of this data structure. It also provides support for Events, transactional support and process invocation features that are missing from a vanilla Map contract. Most likely this abstraction layer will miss some of these native features of NamedCache that even JCACHE (JSR107) dictates only a subset of features of. The challenge here is that NamedCache has no knowledge of objects being destination aware or in simple English does not understand the Cache annotation, unless I find Product team drunk enough to put this idea forward to. So we need to build one. Lets create a ReducedMap:
public interface ReducedMap {
public Object put (Object value) throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException,
OperationNotSupportedException;
}
and implement it in a custom Map using Java delegation pattern to pass cache invocations to Coherence.
public class AnnoMap extends AbstractMap implements ReducedMap {
private volatile NamedCache nCache;

public AnnoMap() {
}

public int size() {
return (nCache == null) ? 0 : nCache.size();
}

public boolean isEmpty() {
return (nCache == null) ? false : nCache.isEmpty();
}

public Set entrySet() {
return (nCache == null) ? Collections.EMPTY_SET : nCache.entrySet();
}


public boolean containsKey(Object key) {
if (nCache == null) {
Class clz = key.getClass();
Cache c = (Cache)clz.getAnnotation(Cache.class);
if (c != null) {
String name = (c.name() == null) ? clz.getName() : c.name();
nCache = CacheFactory.getCache(name);
}
}
return (nCache == null) ? false : nCache.containsKey(key);
}

public Object put(Object value) throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException,
OperationNotSupportedException {
Class clz = value.getClass();
Cache c = (Cache)clz.getAnnotation(Cache.class);
if (c != null) {
String name = (c.name() == null) ? clz.getName() : c.name();
nCache = CacheFactory.getCache(name);
Method m = value.getClass().getMethod(c.pk(), new Class[0]);
Object key = m.invoke(value, null);
return nCache.put(key, value, c.ttl());
}
throw new OperationNotSupportedException("Class not annotated");
}


public Object get(Object key) {
if (nCache == null) {
Class clz = key.getClass();
Cache c = (Cache)clz.getAnnotation(Cache.class);
if (c != null) {
String name = (c.name() == null) ? clz.getName() : c.name();
nCache = CacheFactory.getCache(name);
}
}
return (nCache == null) ? null : nCache.get(key);
}

public Object put(Object key, Object value) {
Class clz = value.getClass();
Cache c = (Cache)clz.getAnnotation(Cache.class);
if (c == null)
throw new UnsupportedOperationException("Value not annotated");
String name = (c.name() == null) ? clz.getName() : c.name();
nCache = CacheFactory.getCache(name);
return nCache.put(key, value);
}

}
Now as you can see AnnoMap is very limited in its capability but if you are taking this route probably this Map is the place to enhance. Lets run a quick test:
public static void main(String[] args) throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException,
OperationNotSupportedException {
Domain d = new Domain ();
// -- KeyObject will be similar to Domain object with the same Cache
// -- destination defined in the Cache annotation.

Key key = new KeyObject (...);
d.setId(k);
d.setXXX(..);
AnnoMap map = new AnnoMap ();
map.put(d);

System.out.println(map.get (key));
}
Enjoy!

No comments: