This page is dedicated to the new Design of the MetaClass.
It seems that we need 2 parts for MetaClass, one for the implementation and one for the user to customize the behavior. The reason for this is, that the customization is done by subclassing the implementation part. This means it is impossible to stay in the specification part (classes from groovy.*) to do a all day thing. Additionally the method signatures must be fixed to not to break code.
Currently we do something like:
class MyMetaClass extends MetaClassXY {
public Object invokeMethod(Object o, String name, Object[] args) {
preProcessing();
super.invokeMethod(o, name,args);
postProcessing();
}
}
This is very flexible, it allows interception and logging as well as transformation at the same time.
If we want to separate the two cases (runtime MetaClass, user defined MetaClass) in two classes, then one must call the other. As the user part should stay in the specification, it can't know the exact class of the runtime, or which method to call there. This means the user part must be called from the MetaClass in the runtime. Since we are not able to call "super" then we need an additonal Object, that we make the call on:
class MyMetaClass extends MetaClassXY {
public Object invokeMethod(Object o, String name, Object[] args, CallChain chain) {
preProcessing();
chain.invoke(o, name,args);
postProcessing() ;
}
}
interface CallChain {
Object invoke(Object base, String name, Object[] args)
}
class RuntimeMetaClass {
...
Object invokeMethod(Object base, String name, Object[] args, ....) {
process(args)
MetaClass mc = getMetaClass()
if (mc!=null) {
return mv.invokeMethod(base,name,args, new CallChain() {
Object invoke(Object base, String name, Object[] args) {
return doRealInvoke(base,name,args...)
}
}
} else {
return doRealInvoke(base,name,args...)
}
}
...
}
That's a bit like the continuation passing style.
If we want to split the behavior, then we could have an interface for each of the cases
interface MetaClassInterceptor {
// thorws MissingMethodException if no interception is done, which results in normal method invocation
Object invokeMethod(Object base, String name, Object[] args);
}
interface MetaClassLogger {
// throws never MissingMethodException and does not change the arguments
void invokeMethodEntry(Object base, String name, Object[] args);
void invokeMethodExit(Object base, String name, Object[] args);
}
interface MetaClassTransformer {
// basically the same as above
Object invokeMethod(Object base, String name, Object[] args, CallChain chain);
}
class RuntimeMetaClass {
...
Object invokeMethod(Object base, String name, Object[] args, ....) {
process(args)
MetaClass mc = getMetaClass()
if (mc!=null) {
Object result = null;
MetaClassLogger logger=null
MetaClassInterceptor interceptor=null
MetaClassTransformer transformer=null
if (mc instanceof MetaClassLogger) logger = (MetaClassLogger ) mc
if (mc instanceof MetaClassInterceptor) interceptor = (MetaClassInterceptor) mc
if (mc instanceof MetaClassTransformer ) interceptor = (MetaClassTransformer ) mc
try {
if (logger!=null) logger.invokeMethodEntry(base,name,args)
if (transformer!=null) {
transformer.invokeMethod(base,name,args, new CallChain() {
Object invoke(Object base, String name, Object[] args) {
return invokeWithInterceptor(interceptor, base,name,args...)
})
} else if (interceptor!=null) {
return invokeWithInterceptor(interceptor, base,name,args...)
}
} finally {
if (logger!=null) logger.invokeMethodExit(base,name,args)
}
} else {
return doRealInvoke(base,name,args...)
}
}
...
}
Maybe it is a bit overkill, since a transformer can act as interceptor, but a logger is surely more lightweight than a interceptor or transformer.
The general disadvantage of the seperation is that we need to associate the custom MetaClass and the runtime MetaClass. Again I see two ways:
class RuntimeMetaClass {
Class theClass;
MetaClass getMetaClass() {
return registry.getMetaClass(theClass)
}
...
}
metaClass.getProperties()
class MyCustomMetaClass extends MetaClass {
MyCustomMetaClass(MetaClass rmc, MetaClassRegistry mcr){
super(rmc,mcr)
}
...
}
interface MetaClass {
MetaClass getChainedMetaClass()
List getProperties()
...
}
class MetaClass {
...
List getProperties(){return runtimeMetaClass.getProperties()}
MetaClass getInnerMetaClass() {return runtimeMetaClass}
}
class MetaClass {
QueryableMetaClass getQueryableMetaClass() {
MetaClass mc = this
while (!(mc instanceof QueryableMetaClass)){
mc = mc.getChainedMetaClass()
}
return mc;
}
...
}
Where getChainedMetaClass behaves like above, meaning, it returns the next MetaClass, the last MetaClass is then the runtime implementation. This version means the usage is
metaClass.queryableMetaClass.getProperties()
instead of the old
metaClass.getProperties()
but it means also, that we can keep the MetaClass interface/class very clean and don't need to add other methods but the methods required for chaining, which we could even make final. Any additional behavior is then controlled by the interfaces I have shown above. This way we could make an MetaClass that intercepts methods, but not properties or fields. Or a version that does logging only on Properties, but does not interfere with normal method invocation. I am aware that this means to have many interface.. 9 then (Interceptor + Transformer + Logger for each of Property, Field and Method) or 6 (if Transformer and Interceptor are unified)
Additionally to the traditional Design we could also have a new Design, where we don't take part in the CallChain, but produce a MetaMethod (or property).
interface MetaClass2 {
MetaMethod getMetaMethod(Object base, String name, Object[] args)
}
class MyMetaClass extends MetaClass implements MetaClass2 {
MetaMethod getMetaMethod(Object base, String name, Object[] args) {
if (name.equals("foo")) return myFooMetaMethod()
return null
}
}
which means this method is called in case of a cache miss, and not every time. Returning a MetaMethod we can control if we would like to add the method to the cache or not through the isCachable() method on MetaMethod. We could also change the interface a little and force the call to getMetaMethod even if there is no cache miss. The advantage is, we can still intercept methods, but in a more efficient way, because we don't need to throw a MissingMethodException. And we can use the cache directly if we do not force method selection through this clsss each time, which makes it very fast.
So my proposal is to chain the MetaClasses as shown above, to have a more or less empty MetaClass class which does only the chaining and to have interfaces controlling all aspects of the method invocation (or for properties/fields) like I have shown with logger and transformer or MetaClass2. Having metaClass.queryableMetaClass.getProperties() means to have less methods on MetaClass and since these MetaClasses do normally not add real methods. It would make sense to do so, but metaClass.getProperties() is not really a problem.