GroovyShell и Binding.setVariable () для вызываемых объектов - PullRequest
2 голосов
/ 05 мая 2019

Я экспериментирую с созданием динамической переменной с 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 Ответ

2 голосов
/ 06 мая 2019

Когда вы используете свойство как вызываемый запасной вариант, метод 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 вместо резервной переменной привязки.Это может значительно облегчить чтение и анализ вашего кода.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...