Metaclass 构造函数重写不适用于 @CompileStatic 注释 class 内部的方法

Metaclass constructor overriding not working for a method inside of @CompileStatic annotated class

当我们对任何特定的 class(如 RESTClient)使用 someClass.metaClass.constructor 时,可在 class 的方法中使用 @CompileStatic 注释,构造函数覆盖根本不起作用。

当我们删除 @CompileStatic 注释时,它可以正常工作。我错过了什么吗?

示例代码:

@CompileStatic
class FooClass {

    String getDataFromProvider() {
        String url = "https://www.example.com"
        RESTClient restClient = new RESTClient(url)

        HttpResponseDecorator response = restClient.post([:]) as HttpResponseDecorator
        return response
    }
}

和测试用例:

import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.RESTClient
import spock.lang.Specification

class FooContentSpec extends Specification {

    void "test getDataFromProvider method"() {
        given: "Rest url"
        String restURL = "https://www.example.com"

        and: "Mock RESTClient"
        RESTClient mockedRestClient = Mock(RESTClient)

        // THIS IS NOT WORKING
        RESTClient.metaClass.constructor = { Object url ->
            assert restURL == url
            return mockedRestClient
        }

        mockedRestClient.metaClass.post = { Map<String, ?> args ->
            return ""
        }

        when: "We hit the method"
        HttpResponseDecorator response = Content.getDataFromProvider()

        then: "We should get status 200"
        response.statusCode == 200
    }
}

根据 Groovy Lang 文档:

MockFor and StubFor can not be used to test statically compiled classes e.g for Java classes or Groovy classes that make use of @CompileStatic. To stub and/or mock these classes you can use Spock or one of the Java mocking libraries.

预期行为

在这种情况下,RESTClient 的构造函数覆盖应该在我们的测试用例中起作用,因为我们不想在每个测试用例中都攻击第三方 API。

实际行为

不幸的是,RESTClient 不会因为 @CompileStatic 注释而被嘲笑,它每次都会点击 API。

环境信息

------------------------------------------------------------
Gradle 3.5
------------------------------------------------------------

Groovy:       2.4.10,
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015,
JVM:          1.8.0_221 (Oracle Corporation 25.221-b11),
OS:           Mac OS X 10.15.2 x86_64

吉拉:https://issues.apache.org/jira/browse/GROOVY-9353

你是对的 @CompileStatic 不能与元类操作结合使用。原因是,顾名思义,所有内容都在编译时解析和绑定,因此没有元类查找,因此无法覆盖它。

我建议研究 IoC/dependency 注入,这样您就可以将模拟注入到您的代码中。使用经典单例会使您的代码更难测试。

来自 Leonard Brünings 的评论后:

Yes @CompileStatic will resolve the constructor of RESTClient in your FooClass at compile time so it will not use the metaclass to lock it up at runtime. If you want to see how it looks, I'd suggest using a decompiler, e.g. ByteCode viewer and look at the generated bytecode.

我们针对两种情况对生成的字节码进行了反编译:

@CompileStatic

public class FooClass implements GroovyObject {
    public FooClass() {
        MetaClass var1 = this.$getStaticMetaClass();
        this.metaClass = var1;
    }

    public String getDataFromProvider() {
        String url = "https://www.example.com";

        // Directly constructor is getting used
        RESTClient restClient = new RESTClient(url);

        HttpResponseDecorator response = (HttpResponseDecorator)ScriptBytecodeAdapter.asType(restClient.post(ScriptBytecodeAdapter.createMap(new Object[0])), HttpResponseDecorator.class);
        return (String)ShortTypeHandling.castToString(response);
    }
}

没有@CompileStatic

public class FooClass implements GroovyObject {
    public FooClass() {
        CallSite[] var1 = $getCallSiteArray();
        super();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    public String getDataFromProvider() {
        CallSite[] var1 = $getCallSiteArray();
        String url = "https://www.example.com";

        // Here Groovy's metaprogramming is into play instead of directly calling constructor
        RESTClient restClient = (RESTClient)ScriptBytecodeAdapter.castToType(var1[0].callConstructor(RESTClient.class, url), RESTClient.class);

        HttpResponseDecorator response = (HttpResponseDecorator)ScriptBytecodeAdapter.asType(var1[1].call(restClient, ScriptBytecodeAdapter.createMap(new Object[0])), HttpResponseDecorator.class);
        return (String)ShortTypeHandling.castToString(response);
    }
}

所以@Leonard 给出的答案是完全正确的。我们错过了这个简单的 Java 概念。