'Riddle me this: Inconsistent groovy meta programming behaviour

I stumbled across this when updating a large app from groovy 2 to 3 (and also to corresponding newer spock and geb versions).

This code behaves strange and also a different kind of strange in groovy 2 versus groovy 4.

I think we are running without "indy" here. I guess because all the transitive dependencies of our large app bring in specific groovy jars without indy. I should probably goe through them carefully and adapt our gradle build so that only "indy" versions of all jars are picked.

class A {
    def foo() {
        bar('hello')
        beep(Optional.of('hello'))
    }

    protected void bar(String value) { println 'A.bar' }
    protected void beep(Optional<String> value) { println 'A.beep' }
}

class B extends A {
    protected void bar(String value) { println 'B.bar' }
    protected void beep(Optional<String> value) { println 'B.beep' }
}

class C extends B implements GroovyInterceptable {
    def invokeMethod(String name, Object args) {
        super."$name"(*args)
    }
}

static void main(String[] args) {
    new C().foo()
    println '---'
    C c = new C()
    c.bar('hello')
    c.beep(Optional.of('hello'))
}

Output for groovy 2.5.15:

B.bar
A.beep
---
A.bar
A.beep

Output for groovy 4.0.0:

A.bar
A.beep
---
A.bar
A.beep

What I would have expected:

B.bar
B.beep
---
B.bar
B.beep

What's going on here? Bug or some strange, but expected corner case?

Where is the difference in behavior in between groovy 2 and 4 documented?

In our real app there was a difference already in between groovy 2 and 3 but I have been unable so far to create example code for that.

Is there a way to call the original method inside of invokeMethod? (Can't find anything in the docs, which are very sparse btw.)



Solution 1:[1]

I get your 3.0.9 output for Groovy 2.5.16, 3.0.10 and 4.0.1 -- indy enabled for all three.

Your implementation of invokeMethod relies on the behavior of ScriptBytecodeAdapter#invokeMethodOnSuperN which is what is behind super."$name"(*args). When handling "bar" message, the meta-method index has B.bar(java.lang.String) for "this" and B.super$2$bar(java.lang.String) for "super". super$2$bar is a meta-object protocol (MOP) method that provides the necessary INVOKESPECIAL instruction to reach A#bar(java.lang.String).

If you want the output of all calls to be from B then you can use this."$name"(*args) instead. In your specific case, there is no need to implement C as GroovyInterceptable and to try and route "foo", "bar" and "beep" yourself.

Solution 2:[2]

You can make your code produce the expected output by making the B class compiled statically:

import groovy.transform.CompileStatic

class A {
    def foo() {
        bar('hello')
        beep(Optional.of('hello'))
    }

    protected void bar(String value) { println 'A.bar' }
    protected void beep(Optional<String> value) { println 'A.beep' }
}

@CompileStatic
class B extends A {
    protected void bar(String value) { println 'B.bar' }
    protected void beep(Optional<String> value) { println 'B.beep' }
}

class C extends B implements GroovyInterceptable {
    def invokeMethod(String name, Object args) {
        super."$name"(*args)
    }
}

static void main(String[] args) {
    new C().foo()
    println '---'
    C c = new C()
    c.bar('hello')
    c.beep(Optional.of('hello'))
}

Output:

B.bar
B.beep
---
B.bar
B.beep

As it was mentioned by emilies in his answer, in the MOP use case scenario something like this happens:

  • c.bar('Hello')
  • invokeMethod('bar', ['Hello'] as Object[])
  • super."bar"(['Hello'] as Object[])

This super."bar"(['Hello'] as Object[]) is represented by B.super$2$bar(java.lang.String) method object which forces A.bar(java.lang.String) to be invoked right in the next call frame.

However, if you make the B class to be compiled statically, the method that is found to satisfy the super."bar"(['Hello'] as Object[]) expression, in that case, is B.bar(java.lang.String), and thus it gets invoked directly.

Regarding the differences between Groovy 2.5 and Groovy >=3.0, it looks like you have encountered a compiler bug. The bar('hello') inside the A.foo() method ignores the MOP and goes directly to this.bar(java.lang.String) which in this case is B.bar(java.lang.String).

enter image description here

It looks like it happens for the java.lang.String type (didn't check other types). However, when the type is java.util.Optional, then a call like beep(Optional.of('Hello')) inside the A.foo() method goes through the MOP and thus it discovers B.super$2$beep(java.util.Optional) method to be invoked:

enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 emilles
Solution 2 Szymon Stepniak