maojf
maojf

Reputation: 37

Groovy - interceptor of ProxyMetaClass does not affect inner methods calls

The code snippet is from the book < Groovy in action 2nd >, with minor modifications.

1 this code works as expected

package test

class InspectMe {
    int outer(){
        return inner()
    }
    int inner(){
        return 1
    }
}

def tracer = new TracingInterceptor(writer: new StringWriter())
def proxyMetaClass = ProxyMetaClass.getInstance(InspectMe)
proxyMetaClass.interceptor = tracer
InspectMe inspectMe = new InspectMe()

inspectMe.metaClass = proxyMetaClass
inspectMe.outer()
println(tracer.writer.toString())

output:

before test.InspectMe.outer()
  before test.InspectMe.inner()
  after  test.InspectMe.inner()
after  test.InspectMe.outer()

2 but this code's output is different

package test

class InspectMe {
    int outer(){
        return inner()
    }
    int inner(){
        return 1
    }
}

def tracer = new TracingInterceptor(writer: new StringWriter())
def proxyMetaClass = ProxyMetaClass.getInstance(InspectMe)
proxyMetaClass.interceptor = tracer
InspectMe inspectMe = new InspectMe()

proxyMetaClass.use(inspectMe){
    inspectMe.outer()
}
println(tracer.writer.toString())

output:

before test.InspectMe.outer()
after  test.InspectMe.outer()

It seems TracingInterceptor dosen't intercept inner methods in the second code. Maybe it's normal behavior, But it seems to me like a bug. Can somebody please explain this?

Upvotes: 1

Views: 458

Answers (1)

Szymon Stepniak
Szymon Stepniak

Reputation: 42184

I don't know if this is a bug or not, but I can explain why this different behavior happens. Let's start with analyzing what InspectMe.outer() method implementation looks like at the bytecode level (we decompile .class file):

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

public class InspectMe implements GroovyObject {
    public InspectMe() {
        CallSite[] var1 = $getCallSiteArray();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    public int outer() {
        CallSite[] var1 = $getCallSiteArray();
        return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass() ? this.inner() : DefaultTypeTransformation.intUnbox(var1[0].callCurrent(this));
    }

    public int inner() {
        CallSite[] var1 = $getCallSiteArray();
        return 1;
    }
}

As you can see, the outer() method tests the following predicate

!__$stMC && !BytecodeInterface8.disabledStandardMetaClass()

and if it evaluates to true, it invokes directly this.inner() method avoiding Groovy's MOP (meta-object protocol) layer (no metaclass involved in this case). Otherwise, it invokes var1[0].callCurrent(this) which means that inner() method gets invoked through Groovy's MOP with metaclass and interceptor involved in its execution.

The two examples you have shown in the question present a different way of setting metaclass field. In the first case:

def tracer = new TracingInterceptor(writer: new StringWriter())
def proxyMetaClass = ProxyMetaClass.getInstance(InspectMe)
proxyMetaClass.interceptor = tracer
InspectMe inspectMe = new InspectMe()

inspectMe.metaClass = proxyMetaClass // <-- setting metaClass with DefaultGroovyMethods
inspectMe.outer()

println(tracer.writer.toString())

we are invoking inspectMe.setMetaClass(proxyMetaClass) method using Groovy's MOP layer. This method gets added to InspectMe class by DefaultGroovyMethods.setMetaClass(GroovyObject self, MetaClass metaClass).

Now, if we take a quick look at how this setMetaClass method is implemented we will find something interesting:

/**
 * Set the metaclass for a GroovyObject.
 * @param self the object whose metaclass we want to set
 * @param metaClass the new metaclass value
 * @since 2.0.0
 */
public static void setMetaClass(GroovyObject self, MetaClass metaClass) {
    // this method was introduced as to prevent from a stack overflow, described in GROOVY-5285
    if (metaClass instanceof HandleMetaClass)
        metaClass = ((HandleMetaClass)metaClass).getAdaptee();

    self.setMetaClass(metaClass);
    disablePrimitiveOptimization(self);
}

private static void disablePrimitiveOptimization(Object self) {
    Field sdyn;
    Class c = self.getClass();
    try {
        sdyn = c.getDeclaredField(Verifier.STATIC_METACLASS_BOOL);
        sdyn.setBoolean(null, true);
    } catch (Throwable e) {
        //DO NOTHING
    }
}

It invokes at the end private method disablePrimitiveOptimization(self). This method is responsible for assigning true to __$stMC class field (the constant Verifier.STATIC_METACLASS_BOOL stores __$stMC value). What does it mean in our case? It means that the predicate in outer() method:

return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass() ? this.inner() : DefaultTypeTransformation.intUnbox(var1[0].callCurrent(this));

evaluates to false, because __$stMC is set to true. And in this case inner() method gets executed via MOP with metaClass and interceptor.

OK, but it explains the first case that works as expected. What happens in the second case?

def tracer = new TracingInterceptor(writer: new StringWriter())
def proxyMetaClass = ProxyMetaClass.getInstance(InspectMe)
proxyMetaClass.interceptor = tracer
InspectMe inspectMe = new InspectMe()

proxyMetaClass.use(inspectMe){
    inspectMe.outer()
}

println(tracer.writer.toString())

Firstly, we need to check what does proxyMetaClass.use() look like:

/**
 * Use the ProxyMetaClass for the given Closure.
 * Cares for balanced setting/unsetting ProxyMetaClass.
 *
 * @param closure piece of code to be executed with ProxyMetaClass
 */
public Object use(GroovyObject object, Closure closure) {
    // grab existing meta (usually adaptee but we may have nested use calls)
    MetaClass origMetaClass = object.getMetaClass();
    object.setMetaClass(this);
    try {
        return closure.call();
    } finally {
        object.setMetaClass(origMetaClass);
    }
}

It's pretty simple - it replaces metaClass for the time of closure execution and it sets the old metaClass back when closure's execution completes. Sounds like something similar to the first case, right? Not necessarily. This is Java code and it invokes object.setMetaClass(this) method directly (the object variable is of type GroovyObject which contains setMetaClass method). It means that the field __$stMC is not set to true (the default value is false), so the predicate in outer() method has to evaluate:

BytecodeInterface8.disabledStandardMetaClass()

If we run the second example we will see that this method call returns false:

enter image description here

And that is why the whole expression

!__$stMC && !BytecodeInterface8.disabledStandardMetaClass()

evaluates to true and the branch that invokes this.inner() directly gets executed.

Conclusion

I don't know if it was intended or not, but as you can see dynamic setMetaClass method disables primitive optimizations and continues using MOP, while ProxyMetaClass.use() sets the metaClass keeping primitive optimizations enabled and caused a direct method call. I guess this example shows a corner case no one thought about when implementing ProxyMetaClass class.

UPDATE

It seems like the difference between these two methods exists because ProxyMetaClass.use() was implemented in 2005 for Groovy 1.x and it got updated for the last time in 2009. This __$stMC field was added in 2011 and the DefaultGroovyMethods.setMetaClass(GroovyObject object, Closure cl) was introduced in 2012 according to its javadoc that says this method is available since Groovy 2.0.

Upvotes: 4

Related Questions