Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Next »

 This page is dedicated to the new Design of the MetaClass.

 General Problem

 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.

 Current Behavior

 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();
  }
}




  • preProcessing might change the arguments, name or call object, or might just do some logging.
  • postProcessing might evaluate the return value of the method call, call additional methods, or just do logging
  • every part here might end the invokeMethod method, that means super.invokeMethod might not be called

 This is very flexible, it allows interception and logging as well as transformation at the same time.

 Basic Design

  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:

 Call Chain


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.  

 Disadvantages

  • A custom MetaClass results in creating a ChainCall object each time an method of that class is invoked.

 Advantages

  • The old behavior can be preserved and guranteed 

 Multiple Behaviors controlled by Class

 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.

 Disadvantages

  • the common case still requires a ChainCall object each time an method of that class is invoked

 Advantages

  • the logging case does not influence the invokation so much

General Disadvantage

The general disadvantage of the seperation is that we need to associate the custom MetaClass and the runtime MetaClass. Again I see two ways:

  1. let the runtime get the custom MetaClass:
    class RuntimeMetaClass {
      Class theClass;
      MetaClass getMetaClass() {
        return registry.getMetaClass(theClass)
      }
      ...
    }
    



    A null entry in the registry would then mean, that no custom MetaClass is set, in case of GroovyObject, the logic must be changed a little because null here means just no per instance MetaClass. As there is only one runtime MetaClass and several behaviors this means that it is not possible to do
    metaClass.getProperties()
    



    or something alike. Again we could add an interface and make the runtime MetaClass ask the custom MetaClass or to provide an additional set of Properties and Methods at initialization, but then we get another set of interfaces and the runtime MetaClass msut keep track of the changes in case a new custom MetaClass is set.
  2. let the runtime assign the runtime MetaClass to the custom MetaClass:
    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}
    
    }
    


    So this works much like our ProxyMetaClass. The behavior is much like the old MetaClass, just the signature of invokeMethod for example changes from (Object,String,Object[]) to (Object,String,Object[],ChainCall). It is then no longer possible to invoke a method using invokeMethod directly, it is then only for internal use. Which is no problem, as long as we keep an alternative way for Java as we currently have with InvokerHelper. Multiple chained MetaClasses are also possible this way, the implementation must just keep that in mind and iterate through all of them when making a call. This again is very unlike the ProxyMetaClass we currently have. I would strongly suggest to make MetaClass no interface then and to remove ProxyMetaClass, because the MetaClass then fulfills already the requirements of ProxyMetaClass. Another solution would be to not to let it act as proxy, but to require an additional method:
 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)

  1. The custom behavior is unable to tell the difference between a call to super, a call from inside the class, or a normal call from a context outside (could be solved with additional different interfaces)

General Advantage

  • implementation and custom behavior are separated
  • we can split the runtime MetaClass in a mutable and immutable part and reconstruct the immutable part on demand
  • custom MetaClasses becomes more lightweight, since no full initialization step is required for each custom MetaClass and the heavy part can be shared by multiple custom MetaClasses

More efficient custom MetaClasses

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.

Proposal

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.

 Variations

  •  Instead of chaining the MetaClasses like above, we could also have a central MetaClass and just add behavior using interface like the ones I shown (transformer, interceptor...). This would mean to change GroovyObject, because get/setMetaClass becomes meaningless then. If we use a proxy like mechanism, then we end in something that looks almost as the one above, but even more complicated.
  • I tried to find a way to keep the call to super, but I was unable to find out how wihtout using a ThreadLocal workaround, which I would like to avoid.
  • No labels