SiKing SiKing - 2 years ago 181
Groovy Question

modify script variable from a Closure in Groovy

I am trying to modify a script variable from inside a closure in a function. The problem can be distilled down to this:

@groovy.transform.Field int myField = 0

incrementField()
assert myField == 1

def incrementField() {
1.times { myField++ }
}


I think the problem has something to do with closure delegates, but I cannot quite wrap my head around the docs.

Answer Source

This behavior is caused by groovy.lang.Script class and the fact that it overrides following methods:

Closure you have shown in the example uses delegate set to a script object and that's why both overridden methods get executed when you try to access or modify field defined in a script.

Now let's see what happens when your example reaches closure

{ myField++ }

Firstly, getProperty("myField") is called to return a value associated with this property. This method is implemented as:

public Object getProperty(String property) {
    try {
        return binding.getVariable(property);
    } catch (MissingPropertyException e) {
        return super.getProperty(property);
    }
}

Source: https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/Script.java#L54

binding object contains only one variable in the beginning - closure's args array. If we take a look at implementation of binding.getVariable(property) method we will see:

public Object getVariable(String name) {
    if (variables == null)
        throw new MissingPropertyException(name, this.getClass());

    Object result = variables.get(name);

    if (result == null && !variables.containsKey(name)) {
        throw new MissingPropertyException(name, this.getClass());
    }

    return result;
}

Source: https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/Binding.java#L56

In our case MissingPropertyException is being thrown, so Script.getProperty(property) method returns a value of field myField defined in our Groovy script - 0. Then Groovy increments this value by 1 and tries to set this new value to a field myField. In this case Script.setProperty(property, value) is being called:

public void setProperty(String property, Object newValue) {
    if ("binding".equals(property))
        setBinding((Binding) newValue);
    else if("metaClass".equals(property))
        setMetaClass((MetaClass)newValue);
    else
        binding.setVariable(property, newValue);
}

Source: https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/Script.java#L62

As you can see it sets this new value using bindings object. If we display binding.variables we will see that now this internal map contains two entries: args -> [:] and myField -> 1. It explains why assertion in your script always fails. Body of the closure you have defined never reaches myField field from the script class.

Workaround

If you are not satisfied with the fact that Script class overrides setProperty(property, value) method you can always override it by hand in your script and use same implementation as GroovyObjectSupport.setProperty(property, value). Simply add below method to your Groovy script:

@Override
void setProperty(String property, Object newValue) {
    getMetaClass().setProperty(this, property, newValue)
}

Now closure defined in incrementField will set a new value to a class field instead of to a bindings object. Of course it may cause some weird side effects, you have to be aware of that. I hope it helps.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download