GroovyShell和Binding.setVariable()用于可调用对象

时间:2019-05-05 18:28:22

标签: groovy

我正在尝试使用GroovyShell创建一些动态变量,但遇到了一个问题。首先,工作代码:

static def defVar(def glob) {
    glob.setVariable('test', new Test())
}

class MyBinding extends Binding {
}

class Test {
    def call() {
        println("--- hello ---")
    }
}

Binding glob = new MyBinding()
GroovyShell shell = new GroovyShell(glob)
defVar(glob)
shell.parse('test()').run()

这给了我预期的输出:

--- hello ---

但是,我想在调用setVariable()时动态调用getVariable(),如下所示:

static def defVar(def glob) {
    glob.setVariable('test', new Test())
}

class MyBinding extends Binding {
    def getVariable(String name) {
        if (! hasVariable('test')) {
            BindingTest.defVar(this)
        }
        return super.getVariable(name)
    }
}

class Test {
    def call() {
        println("--- hello ---")
    }
}

Binding glob = new MyBinding()
GroovyShell shell = new GroovyShell(glob)
//defVar(glob)
shell.parse('test()').run()

但这失败并显示以下错误:

Caught: groovy.lang.MissingMethodException: No signature of method: Script1.test() is applicable for argument types: () values: []
Possible solutions: getAt(java.lang.String), use([Ljava.lang.Object;), use(java.lang.Class, groovy.lang.Closure), use(java.util.List, groovy.lang.Closure), wait(), wait(long)
groovy.lang.MissingMethodException: No signature of method: Script1.test() is applicable for argument types: () values: []
Possible solutions: getAt(java.lang.String), use([Ljava.lang.Object;), use(java.lang.Class, groovy.lang.Closure), use(java.util.List, groovy.lang.Closure), wait(), wait(long)
    at Script1.run(Script1.groovy:1)
    at Script1$run.call(Unknown Source)
    at BindingTest.run(BindingTest.groovy:23)

当我添加这样的跟踪代码时:

class MyBinding extends Binding {
    def getVariable(String name) {
        if (! hasVariable(name)) {
            BindingTest.defVar(this)
        }
        println("getVariable: ${name}: ${super.getVariable(name).getClass().getName()}")
        return super.getVariable(name)
    }

    void setVariable (String name, def val) {
        println("setVariable: ${name}: ${val.getClass().getName()}")
        super.setVariable(name, val)
    }

    def getProperty(String name) {
        println("getProperty: ${name}: ${super.getProperty(name)}")
        return super.getProperty(name)
    }

    void setProperty (String name, def val) {
        println("setProperty: ${name}: ${val.getClass().getName()}")
        super.setProperty(name, val)
    }
}

在可行的情况下,我得到以下输出:

setVariable: test: Test
--- hello ---

在不工作的情况下,我得到以下输出:

setVariable: test: Test
getVariable: test: Test
Caught: groovy.lang.MissingMethodException: No signature of method: Script1.test() is applicable for argument types: () values: []
...

两个问题:

  1. 在可行的情况下,为什么没有getVariable
  2. 在不工作的情况下,为什么Test返回的getVariable对象被拒绝了?

请注意,此问题特定于可调用的值。如果我将一个简单的值(例如字符串)设置为test,则两种方法都可以正常工作。例如,进行此类更改:

...
static def defVar(def glob) {
    glob.setVariable('test', '--- hello ---')
}
...
shell.parse('println(test)').run()

两种方法的输出都相同:

setVariable: test: java.lang.String
getVariable: test: java.lang.String
setVariable: test: java.lang.String
--- hello ---

但是,我不确定为什么setVariable被调用两次。我找不到任何说明这些令人困惑行为的文档。这里有人可以给他们一些启发吗?

请注意,所有代码段均经过简化,以便于演示问题,而不是出于预期目的

1 个答案:

答案 0 :(得分:2)

将属性用作可调用的后备时,Binding.getVariable()方法不会涉及。此行为由元类控制,在您的情况下,所有行为都驱动MetaClassImpl.invokePropertyOrMissing()方法的执行。此方法确定

test()

应调用test.call()(如果存在现有属性),否则应回退到missingMethod()方法。这是此方法的实现形式:

private Object invokePropertyOrMissing(Object object, String methodName, Object[] originalArguments, boolean fromInsideClass, boolean isCallToSuper) {
    // if no method was found, try to find a closure defined as a field of the class and run it
    Object value = null;
    final MetaProperty metaProperty = this.getMetaProperty(methodName, false);
    if (metaProperty != null)
      value = metaProperty.getProperty(object);
    else {
        if (object instanceof Map)
          value = ((Map)object).get(methodName);
    }

    if (value instanceof Closure) {  // This test ensures that value != this If you ever change this ensure that value != this
        Closure closure = (Closure) value;
        MetaClass delegateMetaClass = closure.getMetaClass();
        return delegateMetaClass.invokeMethod(closure.getClass(), closure, CLOSURE_DO_CALL_METHOD, originalArguments, false, fromInsideClass);
    }

    if (object instanceof Script) {
        Object bindingVar = ((Script) object).getBinding().getVariables().get(methodName);
        if (bindingVar != null) {
            MetaClass bindingVarMC = ((MetaClassRegistryImpl) registry).getMetaClass(bindingVar);
            return bindingVarMC.invokeMethod(bindingVar, CLOSURE_CALL_METHOD, originalArguments);
        }
    }
    return invokeMissingMethod(object, methodName, originalArguments, null, isCallToSuper);
}

来源:https://github.com/apache/groovy/blob/GROOVY_2_5_X/src/main/groovy/groovy/lang/MetaClassImpl.java#L1262-L1287

现在,请注意分支if (object instanceof Script)以及如何检索绑定变量。它尝试使用以下方法从绑定对象中检索test变量:

Object bindingVar = ((Script) object).getBinding().getVariables().get(methodName);

您的代码在以下情况下将起作用:

Object bindingVar = ((Script) object).getBinding().getVariable(methodName);

相反。但事实并非如此。

例如,如果您重写getVariables()而不是getVariable(String name)方法,则可以使第二种情况起作用。

class MyBinding extends Binding {
    @Override
    Map getVariables() {
        return super.getVariables() + [
                test: new Test()
        ]
    }
}

当然,您的最终实现可能要复杂得多。 (例如,您可以先获取super.getVariables()映射,检查缺少哪些变量,然后仅在初始映射缺少给定变量的情况下才添加默认变量。)但这取决于您。

或者,考虑使用methodMissing代替绑定变量后备。它可以使您的代码更易于阅读和推理。