In this example we are going to demonstrate how to use JDK interface based proxies to implement a general purpose decorator.
Assume we want to cache data of some expensive methods calls of multiple objects implementing different interfaces.
We will first show how to apply the decorator pattern without using dynamic proxies.
The Interface
public interface IObject {
String getData ();
}
The Implementation
public class MyObject implements IObject {
public String getData () {
return "expensiveData-" + System.nanoTime();
}
}
The Decorator without Proxies
package com.logicbig.example;
import java.util.HashMap;
import java.util.Map;
public class NormalCacheDecorator implements IObject {
private IObject original;
private Map<String, Object> cacheData = new HashMap<>();
public NormalCacheDecorator (IObject original) {
this.original = original;
}
@Override
public String getData () {
Object data = cacheData.get("getData");
if (data == null) {
data = original.getData();
cacheData.put("getData", data);
}
return (String) data;
}
}
Using the decorator:
public static void main (String[] args) {
MyObject object = new MyObject();
IObject decorator = new NormalCacheDecorator(object);
System.out.println(decorator.getData());
System.out.println(decorator.getData());
}
Output:
expensiveData-1781118984883837
expensiveData-1781118984883837
That works but the problem is; caching data is a general requirement. If we have multiple objects implementing different interface, we have to repeat the same decorator logic for each object.
Let's see how we can apply caching logic generically with JDK dynamic proxies
Creating a Generic Decorator with Dynamic Proxies
public class GenericCacheDecorator implements InvocationHandler {
private Map<String, Object> cachedData = new HashMap<>();
private Object EMPTY = new Object();
private Object obj;
private GenericCacheDecorator (Object obj) {
this.obj = obj;
try {
BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass());
for (PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
cachedData.put(desc.getReadMethod().getName(), EMPTY);
}
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
public static <I, T extends I> I decorate (T t, Class<I> interfaceClass) {
GenericCacheDecorator cacheableDecorator = new GenericCacheDecorator(t);
return (I) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class[]{interfaceClass}, cacheableDecorator);
}
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
if (cachedData.containsKey(method.getName())) {
Object o = cachedData.get(method.getName());
if (o == EMPTY) {
Object returned = method.invoke(obj, args);
cachedData.put(method.getName(), returned);
return returned;
} else {
return o;
}
}
return method.invoke(args);
}
}
The above invocation handler implementation is agnostic to any particular interface or object type which is required to cache data returned by getters of the underlying object.
public static void main (String[] args) {
MyObject object = new MyObject();
IObject iObject = GenericCacheDecorator.decorate(object, IObject.class);
System.out.println(iObject.getData());
System.out.println(iObject.getData());
}
Output:
expensiveData-1783122642531745
expensiveData-1783122642531745
Example ProjectDependencies and Technologies Used:
|