To start off I just want to mention that this probably isn't the best solution and I'm sure there are ways to optimize this. That said, I wanted to try my hand at CGLIB and ObjenesisHelper again.
Using CGLIB and ObjenesisHelper we can wrap the data object in a proxy which will intercept the get
methods. Using this interceptor we can add the logic you described in your post. Lets start off by assume these are our data types (using lombok for brevity).
@Data class W { private X x; }
@Data class X { private Y y; }
@Data class Y { private Z z; }
@Data class Z { private int alpha; }
Our final solution can be used like the following:
public static void main(String[] args) {
final W w = ProxyUtil.withLazyDefaults(new W());
System.out.println(w.getX().getY().getZ().getAlpha());
}
Implementation
Currently, if we try to invoke new W().getX().getY().getZ().getAlpha()
we will get a NullPointerException
when invoking getY()
because getX()
returned null. Even if we manage to produce a default X
value, we will still need a default Y
value to not get a null pointer on getZ()
and getAlpha()
and so forth. The proxy we create needs to be generic and be able to wrap its sub components recursively.
Okay, so lets start. The first thing we need to do is create a MethodInterceptor
. Whenever any call hits our proxy instance it will perform the logic of our MethodInterceptor
. We need to first determine if the method called is a getter. If not we will ignore it. During this getter call, if the value is not present in our data we will create it and update the object. If the value contained by the getter is an original unwrapped class, we will replace it with a wraped version. Finally we will return the wrapped instance. Edit I updated this to not inject wrapped instances into the real Data objects. This will be less performant if the object is accessed mutliple times this way
public class ProxyUtil {
public static <T> T withLazyDefaults(final T data) {
final MethodInterceptor interceptor = (object, method, args, proxy) -> {
if (method.getName().startsWith("get")) {
final Class<?> returnType = method.getReturnType();
Object response = method.invoke(data, args);
if (response == null) {
response = returnType.newInstance();
data.getClass()
.getDeclaredMethod(
method.getName().replaceFirst("get", "set"),
returnType)
.invoke(data, response);
}
if (!returnType.isPrimitive()) {
response = withLazyDefaults(response);
}
return response;
}
return method.invoke(data, args);
};
...
The rest of this method involves using CGLIB and Objenisis Helper to construct the wrapper instance. CGLib will allow you to proxy both classes and interfaces and ObjenesisHelper will allow you to construct an instance of a class without having to invoke a constructor. See here for a CGLib example and here for a ObjenesisHelper example.
...
final Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(data.getClass());
final Set<Class<?>> interfaces = new LinkedHashSet<>();
if (data.getClass().isInterface()) {
interfaces.add(data.getClass());
}
interfaces.addAll(Arrays.asList(data.getClass().getInterfaces()));
enhancer.setInterfaces(interfaces.toArray(new Class[interfaces.size()]));
enhancer.setCallbackType(interceptor.getClass());
final Class<?> proxyClass = enhancer.createClass();
Enhancer.registerStaticCallbacks(proxyClass, new Callback[]{interceptor});
return (T) ObjenesisHelper.newInstance(proxyClass);
}
}
Caveats
- This is not a thread safe operation.
- Reflection will slow down your code.
- Better error handling needs to added for the reflection calls.
- If a class does not have a no-arg constructor this will not work.
- Does not account for inheritance of data classes
- This could be best effort by checking for a no-arg ctor/setter first.