【问题标题】:Not able to override a script method using metaClass无法使用 metaClass 覆盖脚本方法
【发布时间】:2020-05-01 19:38:18
【问题描述】:

我试图通过对象的metaClass 重新定义方法来覆盖 Groovy 自定义 Script 类中定义的方法,但是更改被完全忽略,原始方法按原样调用。我在普通课程上尝试了同样的方法,它按预期工作,所以我想知道在自定义Script 的情况下有什么不同。这是我保存为 `/tmp/t.groovy 的 sn-p:

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')
    }
}

这是我在 Mac OS 上使用 Groovy 2.4.14 运行时的输出:

$ 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
    ...

您可以看到startinit 都被记录了。当我在脚本对象上尝试相同时:

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。我在这里错过了什么?

【问题讨论】:

  • 绝对不行。但是为什么需要脚本解析呢?为什么不只是一个类解析?
  • 我正在测试作为脚本文件加载的 Jenkins 管道 var 文件。知道为什么脚本可能会违反此处记录的常规 Groovy 元编程模型:groovy-lang.org/metaprogramming.html
  • 你试过这个库来测试 Jenkins 管道 - github.com/jenkinsci/JenkinsPipelineUnit 吗?
  • 是的,我已经这样做了,但遗憾的是使用这种方法测试的 vars(可能是 JaCoCo 问题)不会生成任何覆盖率数据,所以我一直在尝试从 var 文件中移动代码的另一种方法到扩展 CpsScript 的自定义脚本中,并将其声明为 var 文件中的 BaseScript。到目前为止,除了一些奇怪的东西之外,这一直运作良好。
  • @haridsv,实际上如果你这样做def script = new TestScript(),那么一切正常。因此,通过将 TestScript 作为类加载来解决问题。只有使用 @groovy.transform.BaseScript 才会出现问题

标签: groovy metaprogramming


【解决方案1】:

TL;DR:实现 GroovyInterceptable 使脚本方法可通过元类覆盖。

我开始通过将def t = new Throwable(); t.fillInStackTrace(); t.printStackTrace() 添加到这两种方法来比较导致Worker.initTestScript.init 的标准化堆栈跟踪来寻找线索。看起来明显不同的部分是从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)

您可以看到从startinit 的调用通过Worker 中的常规代码,而它直接在TestScript 中进行。

然后我注意到Script 扩展了GroovyObjectSupport,这意味着当Java 类的对象应该显示为Groovy 对象时扩展它的基类。当我在metaprogramming documentation 下搜索GroovyObjectSupport 时,我发现invokeMethod 只能与标记接口GroovyInterceptable 一起使用,并且在Script 类的类继承链中缺少它。一旦我让TestScript 实现GroovyInterceptable,覆盖就按预期工作了。

我还是要检查一下添加这个接口是否会让 Jenkins 管道 CPS 对我嗤之以鼻。

【讨论】:

  • 这里还有一个我看不懂的谜题。我注意到一个没有特殊 Groovy 装饰器的简单 Java 类可以在 Groovy 中修改而无需任何额外工作。纯 Java 类带有关联的元类,我可以添加方法并覆盖现有方法,就像纯 Groovy 类一样。我不知道为什么 Script 类是不同的并且需要GroovyInterceptable,更重要的是,显式实现GroovyObject 的好处是如果Groovy 将所有东西都提供给Java 类,即使没有实现这个接口。
  • 在实现 GroovyInterceptable 解决了独立用例中的问题时,我无法让它在使用 JenkinsPipelineUnit 框架的 Spock 测试中工作。出于某种原因,对对象的所有调用最终都会被委托给忽略它们的东西。我最终最终做的是直接实例化对象,而不是通过GroovyShell(参见@daggett 的评论和我的回复)。
猜你喜欢
  • 2010-12-28
  • 1970-01-01
  • 2016-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多