Can you use Groovy meta programming to override a

2019-01-20 15:25发布

I'm trying to override a private method on a Java class using meta programming. The code looks something like this:

// Java class
public class MyClass{

    private ClassOfSomeSort property1;
    private ClassOfSomeOtherSort property2;

    public void init(){

        property1 = new ClassOfSomeSort();
        property2 = new ClassOfSomeOtherSort();

        doSomethingCrazyExpensive();
    }

    private void doSomethingCrazyExpensive(){
        System.out.println("I'm doing something crazy expensive");
    }
}

// Groovy class
public class MyClassTest extends Specification{

    def "MyClass instance gets initialised correctly"(){

        given:
        ExpandoMetaClass emc = new ExpandoMetaClass( MyClass, false )
        emc.doSomethingCrazyExpensive = { println "Nothing to see here..." }
        emc.initialize()
        def proxy = new groovy.util.Proxy().wrap( new MyClass() )
        proxy.setMetaClass( emc )
        when:
        proxy.init()
        then:
        proxy.property1 != null
        proxy.property2 != null     
    }
}

The problem is that the overridden implementation of doSomethingCrazyExpensive isn't called - I think that this is because the private method is called by the init() method internally and not called through the metaClass. If I call myProxy.doSomethingCrazyExpensive() directly, the overridden method is invoked, so the meta-programming does work to some degree.

Is there a way to use meta programming to override a method on a Java class (or instance) in such a way that the overridden implementation is called when it is invoked internally?

4条回答
ら.Afraid
2楼-- · 2019-01-20 16:13

I stumbled on this question and thought I should provide a different answer: Yes you can override an existing method - you just have to change the meta class to ExpandoMetaClass.

This happens automatically when you add your first method, for example.

Here's an example:

println ""
class Bob {
    String name
    String foo() { "foo" }
    void print() { println "$name = ${foo()} ${fum()}  metaclass=${Bob.metaClass}"}
    def methodMissing(String name, args) { "[No method ${name}]"  }
}

new Bob(name:"First ").print()

Bob.metaClass.fum = {-> "fum"}

new Bob(name:"Second").print()

Bob.metaClass.fum = {-> "fum"}

new Bob(name:"Third ").print()

Bob.metaClass.foo = {-> "Overriden Foo"}

new Bob(name:"Fourth").print()

The results are:

First  = foo [No method fum]  metaclass=org.codehaus.groovy.runtime.HandleMetaClass@642a7222[groovy.lang.MetaClassImpl@642a7222[class Bob]]
Second = foo fum  metaclass=groovy.lang.ExpandoMetaClass@21be3395[class Bob]
Third  = foo fum  metaclass=groovy.lang.ExpandoMetaClass@21be3395[class Bob]
Fourth = Overriden Foo fum  metaclass=groovy.lang.ExpandoMetaClass@21be3395[class Bob]

You can see after the fum method was added the meta class changed to an expando. Now when the attempt is made to override the original foo - it works.

查看更多
叼着烟拽天下
3楼-- · 2019-01-20 16:21

It seems you can't use Groovy metaprogramming to replace methods of Java classes - even public methods - try the following in the Groovy console to confirm:

ArrayList.metaClass.remove = { obj ->
  throw new Exception('remove')
}

ArrayList.metaClass.remove2 = { obj ->
  throw new Exception('remove2')
}

def a = new ArrayList()
a.add('it')

// returns true because the remove method defined by ArrayList is called, 
// i.e. our attempt at replacing it above has no effect
assert a.remove('it')

// throws an Exception because ArrayList does not define a method named remove2, 
// so the method we add above via the metaClass is invoked
a.remove2('it')

If you can modify the source code of MyClass, I would either make doSomethingCrazyExpensive protected, or preferably, refactor it so that it's more test-friendly

public class MyClass {

    private ClassOfSomeSort property1;
    private ClassOfSomeOtherSort property2;
    private CrazyExpensive crazyExpensive;

    public MyClass(CrazyExpensive crazyExpensive) {
        this.crazyExpensive = crazyExpensive;
    }

    public void init(){

        property1 = new ClassOfSomeSort();
        property2 = new ClassOfSomeOtherSort();

        crazyExpensive.doSomethingCrazyExpensive();
    }
}

public interface CrazyExpensive {
    public void doSomethingCrazyExpensive();  
}

After making the changes above, when testing MyClass you can easily instantiate it with a mock/stub implementation of CrazyExpensive.

查看更多
Root(大扎)
4楼-- · 2019-01-20 16:23

Groovy as operator is quite powerful, and can create proxies out of concrete types whose changes are visible in Java. Sadly, seems like it can't override private methods, though i managed to change a public method:

Java class:

public class MyClass{

    public void init(){
        echo();
        doSomethingCrazyExpensive();
    }

    public void echo() { System.out.println("echo"); }

    private void doSomethingCrazyExpensive(){
        System.out.println("I'm doing something crazy expensive");
    }
}

Groovy test:

class MyClassTest extends GroovyTestCase {
    void "test MyClass instance gets initialised correctly"(){

        def mock = [
          doSomethingCrazyExpensive: { println 'proxy crazy' },
          echo: { println 'proxy echo' }
        ] as MyClass

        mock.init()

        mock.doSomethingCrazyExpensive()
    }
}

It prints:

proxy echo
I'm doing something crazy expensive
proxy crazy

So the public method got intercepted and changed, even when being called from Java, but not the private one.

查看更多
Juvenile、少年°
5楼-- · 2019-01-20 16:27

You cannot override a method called from Java code in Groovy using metaClass.

That's why you won't be able to "mock" the call to this private method in Java: it is being called by the Java class itself, not from Groovy.

This limitation wouldn't apply, of course, if your class was written in Groovy.

I would suggest that you refactor the Java class if you can so that you can use normal means to mock the expensive method call. Or even make the method protected, then override it in a sub-class.

查看更多
登录 后发表回答