lapots
lapots

Reputation: 13415

weird behavior when add property inside propertyMissing method

I wanted to implement functionally, which allows to add unknown properties to the class, during the attempt to set it without using map of dynamic properties.

As Groovy allows to do it using metaClass I used it in propertyMissing method.

class Item {
    def propertyMissing(String name, value) {
        this.class.metaClass."$name" = value
    }
}

But I ran into a weird behavior.

def i1 = new Item()

i1.prop = "value"
println i1.properties // [class:class Item]
println i1.prop // null

i1.metaClass.field = "555"
println i1.properties // [prop:null, class:class Item, field:555]
println i1.prop // null

i1.prop = "value1"
println i1.properties // [prop:value1, class:class Item, field:555]
println i1.prop // value1

Also If I access metaClass before trying to set prop in the example it won't add it anymore

def i1 = new Item()
i1.metaClass.unkn = "1111"

i1.prop = "value"
println i1.properties // [class:class Item, unkn:1111]
println i1.prop // null

i1.metaClass.field = "555"
println i1.properties // [class:class Item, unkn:1111, field:555]
println i1.prop // null

i1.prop = "value1"
println i1.properties // [class:class Item, unkn:1111, field:555]
println i1.prop // null

Why it has such behaviour?

Upvotes: 1

Views: 165

Answers (2)

Jérémie B
Jérémie B

Reputation: 11032

When you update dynamically the metaclass of an object, Groovy replace the metaclass with an ExpandoMetaClass. It's a special implementation of a MetaClass which support adding and removing properties/methods.

However, in your example, Item is a GroovyObject, which have a persistent field on the MetaClass. this field is not updated when the MetaClass is exchanged : Only the metaclass in the registry is replaced by an ExpandoMetaClass. This kind of code can work with a javaobject, because this object doesn't have a field, and the resolution class->metaclass is done every time groovy access the metaclass.

In you know you are going to add properties on a groovy object, you should explicitly set an ExpandoMetaClass :

class Item {

    def Item() {
      def mc = new ExpandoMetaClass(Item, false, true)
      mc.initialize()
      this.metaClass = mc
    }

    def propertyMissing(String name, value) {
        this.metaClass."$name" = value
    }  
}  

Upvotes: 2

Emmanuel Rosa
Emmanuel Rosa

Reputation: 9895

One of the issues you have is that you're trying to add a property to the class MetaClass instead of the instance MetaClass. And because you're adding the property after creating the instance, the instance doesn't see it. For example, this code fails to print the property:

class A { }

def a = new A()

A.metaClass.prop = 'value'

println a.prop

The error is rather interesting: groovy.lang.MissingPropertyException: No such property: prop for class: A Possible solutions: prop

However, even if you change the code to use the instance MetaClass it still doesn't work:

class Item {
    def propertyMissing(String name, value) {
        metaClass."$name" = value
    }
}

def i1 = new Item()

i1.prop = 'value'
assert i1.prop == 'value'

The error provides a clue:

groovy.lang.MissingPropertyException: No such property: prop for class: groovy.lang.MetaClassImpl

The MetaClass which provides the Map-like functionality is ExpandoMetaClass. Objects don't typically get this type of MetaClass until you do something like this:

instance.metaClass.prop = 'value'

So the fact that the MetaClass is not an ExpandoMetaClass means that replacement process is not happening. It's likely that propertyMissing() gets called too late in the MOP process to use the MetaClass in this way.

You mentioned that you want to add properties without using a Map of dynamic properties. However, ExpandoMetaClass, which is what you're attempting to use indirectly, uses...

...Maps of dynamic properties! You can see it here.

The easiest way to achieve the behavior you're looking for is to extend Expando:

class Item extends Expando {
    def anotherProperty = 'Hello'
}

def i1 = new Item()

i1.prop = 'value'
assert i1.prop == 'value'
assert i1.anotherProperty == 'Hello'

Expando does all of the work for you. If you want to see how it works, read this.

Upvotes: 1

Related Questions