Reputation: 13415
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
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
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...
...Map
s 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