可以使用Groovy元编程来覆盖Java类上的私有方法吗?



我试图使用元编程重写Java类上的私有方法。代码看起来像这样:

// 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     
    }
}

问题是doSomethingCrazyExpensive的重写实现没有被调用-我认为这是因为私有方法是由init()方法内部调用的,而不是通过元类调用的。如果我直接调用myProxy.doSomethingCrazyExpensive(),则会调用被覆盖的方法,因此元编程在某种程度上确实有效。

是否有一种方法可以使用元编程来覆盖Java类(或实例)上的方法,以便在内部调用时调用被覆盖的实现?

Groovy as操作符非常强大,可以从具体类型创建代理,其更改在Java中是可见的。遗憾的是,似乎它不能覆盖私有方法,虽然我设法改变了一个公共方法:

Java类:

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测试:

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

它打印:

proxy echo
I'm doing something crazy expensive
proxy crazy

所以公共方法被拦截和改变,即使是从Java调用,但私有方法没有。

不能在Groovy中使用metaClass覆盖从Java代码调用的方法。

这就是为什么你不能在Java中"模拟"对这个私有方法的调用:它是由Java类本身调用的,而不是从Groovy调用的。

当然,如果你的类是用Groovy编写的,这个限制就不适用了。

我建议您重构Java类,如果可以的话,这样您就可以使用正常的方法来模拟昂贵的方法调用。或者把这个方法设为protected,然后在子类中重写它。

我无意中发现了这个问题,我认为我应该提供一个不同的答案:是的,您可以覆盖现有的方法-您只需要将元类更改为ExpandoMetaClass。

当你添加第一个方法时,这将自动发生,例如

下面是一个例子:

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()

结果如下:

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]

您可以看到添加了fun方法后,元类更改为expando。现在,当尝试覆盖原来的foo -它工作

似乎您不能使用Groovy元编程来替换Java类的方法—甚至是公共方法—请在Groovy控制台中尝试以下操作来确认:

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

如果你可以修改MyClass的源代码,我会让doSomethingCrazyExpensive受保护,或者最好重构它,使它更适合测试

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();  
}

在进行上述更改之后,在测试MyClass时,可以使用CrazyExpensive的模拟/存根实现轻松地实例化它。

最新更新