Using Groovy MetaClass to overwrite Methods

25,680

Solution 1

Your syntax is a tiny bit off. The problem is that pojo is a Java Object and does not have a metaClass. To intercept calls to PlainOldJavaObject's doCallService using ExpandoMetaClass:

Just replace:

    pojo.metaClass.doCallService = { String s ->
        "no service"
    }

With:

    PlainOldJavaObject.metaClass.doCallService = { String s ->
        "no service"
    }

Solution 2

If your POJO really is a Java class, and not a Groovy class, then that is your problem. Java classes don't invoke methods via the metaClass. e.g., in Groovy:

pojo.publicMethod('arg')

is equivalent to this Java:

pojo.getMetaClass().invokeMethod('publicMethod','arg');

invokeMethod sends the call through the metaClass. But this method:

public String publicMethod(String x) {
    return doCallService(x);
}

is a Java method. It doesn't use invokeMethod to call doCallService. To get your code to work, PlainOldJavaObject needs to be a Groovy class so that all calls will go through the metaClass. Normal Java code doesn't use metaClasses.

In short: even Groovy can't override Java method calls, it can only override calls from Groovy or that otherwise dispatch through invokeMethod.

Solution 3

What you have looks fine. I ran a slightly modified version on it on the groovy console webapp and it ran without issue. See for yourself using this code at http://groovyconsole.appspot.com/.

public interface IService {
    String callX(Object o);
}

public class PlainOldJavaObject {

    private IService service;

    public String publicMethod(String x) {
        return doCallService(x);
    }

    public String doCallService(String x) {
        if(service == null) {
            throw new RuntimeException("Service must not be null");
        }
        return service.callX(x);
    }
}

def pojo = new PlainOldJavaObject()
pojo.metaClass.doCallService = { String s ->
    "no service"
}
println pojo.publicMethod("arg")

What version of Groovy are you using. It could very well be a bug in Groovy in the metaclass implementation. The groovy language moves pretty quickly and the metaclass implementation changes from version to version.

Edit - Feedback from Comment:

The version of the groovy console webapp is 1.7-rc-1. So it looks like that version may work as you want it to. They are currently in RC2 so I expect it would be released soon. Not sure if what you are seeing is a bug or just a difference in how it works in the 1.6.x version.

Share:
25,680
raoulsson
Author by

raoulsson

Before: CTO Contovista AG, Zurich, Co-founder Zorp Technologies Inc., SF, Manager and Chief System Architect at Leonteq, and many more... Experienced software engineer and teamlead looking to build/enable useful, delightful, and meaningful products. Passionate, hard-worker interested in contributing to team-oriented, strong engineering cultures. Proven track record of hiring and running successful teams.

Updated on April 21, 2021

Comments

  • raoulsson
    raoulsson about 3 years

    I have a POJO that uses a service to do something:

    public class PlainOldJavaObject {
    
        private IService service;
    
        public String publicMethod(String x) {
            return doCallService(x);
        }
    
        public String doCallService(String x) {
            if(service == null) {
                throw new RuntimeException("Service must not be null");
            }
            return service.callX(x);
        }
    
        public interface IService {
            String callX(Object o);
        }
    }
    

    And I have a Groovy test case:

    class GTest extends GroovyTestCase {
    
        def testInjectedMockIFace() {
            def pojo = new PlainOldJavaObject( service: { callX: "very groovy" } as IService )
            assert "very groovy" == pojo.publicMethod("arg")
        }
    
        def testMetaClass() {
            def pojo = new PlainOldJavaObject()
            pojo.metaClass.doCallService = { String s ->
                "no service"
            }
            assert "no service" == pojo.publicMethod("arg")
        }
    }
    

    The first test method, testInjectedMockIFace works as expected: The POJO is created with a dynamic implementation of IService. When callX is invoked, it simply returns "very groovy". This way, the service is mocked out.

    However I don't understand why the second method, testMetaClass does not work as expected but instead throws a NullPointerException when trying to invoke callX on the service object. I thought I had overwritten the doCallService method with this line:

    pojo.metaClass.doCallService = { String s ->
    

    What am I doing wrong?

    Thanks!

  • raoulsson
    raoulsson over 14 years
    Hi Chris, I run Groovy Version: 1.6.5 JVM: 1.6.0_13
  • noah
    noah over 14 years
    The version has nothing to do with it. The problem is that doCallService(x) is Java code, not Groovy code, so it isn't metaClass aware.
  • Tomato
    Tomato over 11 years
    How do you properly distinguish between Groovy code and Java code? How would you make PlainOldGroovyObject instead of PlainOldJavaObject?
  • noah
    noah over 11 years
    If it's in a .groovy file, it's a Groovy class.
  • A.J. Brown
    A.J. Brown about 11 years
    One thing to keep in mind here is when you manipulate the Class's metaClass, every instance from that point forward will be manipulated. This can have a big impact on other tests that run in the same session. When you manipulate an instance of a class, only that instance is affected.
  • Mate Šimović
    Mate Šimović over 5 years
    To achieve complete test isolation, so that your manipulation on the metaClass won't impact other tests you could do the following. Remember the old method (eg., def oldMethod = pojo.&doCallService) override it (pojo.metaClass.doCallService = { String s -> "no service" }) at the beginning of your test and return the old method at the end of the test (pojo.metaClass.doCallService = oldMethod). NOTE: this will only work if the method doCallService is NOT overloaded (multiple doCallService methods with different arguments) since 'def oldMethod = pojo.&doCallService' can't know which to take.