Невозможно переопределить метод сценария с помощью metaClass - PullRequest
0 голосов
/ 01 мая 2020

Я пытаюсь переопределить метод, определенный в Groovy пользовательском Script классе, переопределив метод через metaClass объекта, однако изменение полностью игнорируется, и оригинальный метод вызывается как есть. Я попробовал то же самое в обычном классе, и это сработало, как и ожидалось, поэтому мне интересно, что отличается в случае пользовательского Script. Вот фрагмент, который я сохранил как `/tmp/t.groovy:

class Worker {
    def init() {
        throw new Exception('I always fail')
    }

    void start() {
        init()
        println('I never get to work')
    }
}

def worker = new Worker()
worker.metaClass.init = { println("dummy init") }
worker.start()

def shell = new GroovyShell()
def script = shell.parse('@groovy.transform.BaseScript TestScript _')
script.metaClass.init = { println("dummy init") }
script.start()

Где класс TestScript должен быть определен в своем собственном файле для GroovyShell, чтобы найти его, поэтому /tmp/TestScript.groovy имеет класс:

abstract class TestScript extends Script {
    def init() {
        throw new Exception('I always fail')
    }

    void start() {
        init()
        println('I never get to work')
    }
}

Вот вывод, когда я бежал, используя Groovy 2.4.14 на Ма c ОС:

$ groovy /tmp/t.groovy
dummy init
I never get to work
Caught: java.lang.Exception: I always fail
java.lang.Exception: I always fail
    at TestScript.init(TestScript.groovy:3)
    at TestScript.start(TestScript.groovy:7)
    at TestScript$start.call(Unknown Source)
    at t.run(t.groovy:19)

Вы можете видеть, что изменение, внесенное в worker.metaClass, было эффективным, но то же самое, что было сделано в script.metaClass, было проигнорировано. Сначала я попытался отследить вызовы в рабочем сценарии, используя что-то вроде этого:

def worker = new Worker()
def m = new DelegatingMetaClass(worker.metaClass) {
        Object invokeMethod(Object object, String methodName, Object[] args) {
            println("invokeMethod ${methodName}(${args})")
            return super.invokeMethod(object, methodName, args)
        }

    }
m.initialize()
worker.metaClass = m
worker.start()

Вывод выглядит так:

invokeMethod start([])
invokeMethod init([])
Caught: java.lang.Exception: I always fail
java.lang.Exception: I always fail
    ...

Вы можете видеть, что и start, и init войти в систему. Когда я попробовал то же самое на объекте сценария:

def shell = new GroovyShell()
def script = shell.parse('@groovy.transform.BaseScript TestScript _')
def m = new DelegatingMetaClass(script.metaClass) {
        Object invokeMethod(Object object, String methodName, Object[] args) {
            println("invokeMethod ${methodName}(${args})")
            return super.invokeMethod(object, methodName, args)
        }

    }
m.initialize()
script.metaClass = m
script.start()

Вывод выглядит так:

invokeMethod start([])
Caught: java.lang.Exception: I always fail
java.lang.Exception: I always fail
    ...

Вы видите, что вызов init никогда не делегируется invokeMethod , Что мне здесь не хватает?

1 Ответ

0 голосов
/ 02 мая 2020

TL; DR: Реализация GroovyInterceptable делает методы сценария переопределяемыми через метакласс.

Я начал искать ключи, сравнивая нормализованные трассировки стека, ведущие к Worker.init и TestScript.init, добавляя def t = new Throwable(); t.fillInStackTrace(); t.printStackTrace() обоим методам. Часть, которая выглядит значительно иначе, это стек, ведущий от start() до init(), как показано ниже:

For Worker class:

    at Worker.init(t.groovy:3)
    at Worker$init$0.callCurrent(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:51)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:157)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:161)
    at Worker.start(t.groovy:8)
    at Worker$start.call(Unknown Source)

For TestScript class:

    at TestScript.init(TestScript.groovy:3)
    at TestScript.start(TestScript.groovy:8)
    at TestScript$start.call(Unknown Source)

Вы можете видеть, что вызов на init с start прошел через groovy код в Worker, где, как это было непосредственно в TestScript.

Тогда я заметил, что Script расширяется GroovyObjectSupport, который предназначен для быть базовым классом для Java классов, расширяемых, когда их объекты должны отображаться как Groovy объектов. Когда я искал GroovyObjectSupport в документации по метапрограммированию , я обнаружил, что invokeMethod работает только в сочетании с интерфейсом маркера GroovyInterceptable и что ему не хватает цепочка наследования классов Script класса. Как только я заставил TestScript реализовать GroovyInterceptable, переопределение сработало, как и ожидалось.

Мне все еще нужно проверить, не добавит ли этот интерфейс Jenkins конвейерная CPS насмешка.

...