3

I would like to overwrite a dynamic property on a metaClass but it's not working as I would expect. Here is a simple example that demonstrate the problem. You can run it online at Groovy web console

class Bike {}
class Bell { def ring() { println("calling " + this) } }

def createNamespaces = {
    return [ "bell": new Bell() ]
}

def resetNamespaces = { bike ->
    // setting to null means 'set it to the default'
    bike.metaClass = null

    createNamespaces().each { name, namespace ->
        println("setting " + namespace)
        bike.metaClass."$name" = namespace
    }
}

def bike= new Bike()

resetNamespaces(bike)
bike.bell.ring()
resetNamespaces(bike)
bike.bell.ring()

The result is:

setting Bell@14e9bd2b
calling Bell@14e9bd2b
setting Bell@948a7ad
calling Bell@14e9bd2b

So although I changed the property on the metaClass, calling it always returns the first object that was set. Is there some kind of caching?

When I just change the last part of the example to:

resetNamespaces(bike)
resetNamespaces(bike)
bike.bell.ring()

Then the result is as expected. That is, calling the property returns the object that was set on the metaClass as the last:

setting Bell@5b47e0c7
setting Bell@19f373a4
calling Bell@19f373a4

I even tried to set metaClass manually as follows

def resetNamespaces = { bike ->
    def newMetaClass = new ExpandoMetaClass(Bike.class, true, true)
    newMetaClass.initialize()

    bike.metaClass = newMetaClass

    ...
}

But the result is still the same. So there must be some caching mechanism involved. I can't find anything about this behavior in the documentation.

Jan Krakora
  • 2,500
  • 3
  • 25
  • 52

1 Answers1

0

The confusion arises because you are trying to change a property of the metaclass, not a method. As stated in the official documentation of groovy metaprogramming properties are accessed using the setAttribute/getAttribute methods of the metaclass.

// throws MissingFieldException!
def resetNamespaces = { bike ->
    createNamespaces().each { name, namespace ->
        bike.metaClass.setAttribute(bike, name, namespace)
    }
}

Unfortunately this only works if the property is in the original class definition, here it throws a MissingFieldException: No such field: bell for class: Bike.

On the other hand overwriting methods works like a charm and provides the wanted syntax by adding a dynamic getter method:

def resetNamespaces(bike) {
    createNamespaces().each { name, namespace ->
        bike.metaClass."get${name.capitalize()}" = { namespace }
    }
}

def bike = new Bike()
resetNamespaces(bike)
bike.bell.ring()
resetNamespaces(bike)
bike.bell.ring()

Actually expando works nicely as well

class Bell {
    def ring() { println this }
}

def bike = new Expando()
bike.createNamespaces = {
    return [ bell : new Bell() ]
}

bike.resetNamespaces = {
    bike.createNamespaces().each { name, namespace ->
        bike."$name" = namespace
    }
}

bike.resetNamespaces bike
bike.bell.ring()
bike.resetNamespaces bike
bike.bell.ring() 
chriopp
  • 947
  • 7
  • 12
  • So you say I can't change a dynamic property of a metaClass of an object instance? Is it somewhere in the documentation or in the source code so I can find it? I thought every method/property call on a groovy object is delegated to its metaClass. See the added last part in my question. – Jan Krakora Jun 25 '18 at 13:11
  • Sorry, you are completely right, of course you can overwrite the metaclass ... See my edited answer. Could not provide deeper insights into the groovy internals but I hope it helps. – chriopp Jun 25 '18 at 21:00