【问题标题】:Metaclass constructor overriding not working for a method inside of @CompileStatic annotated class元类构造函数覆盖不适用于 @CompileStatic 注释类中的方法
【发布时间】:2020-04-15 23:11:44
【问题描述】:

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

当我们删除 @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 文档:

MockForStubFor 不能用于测试静态编译的类,例如使用 @CompileStatic 的 Java 类或 Groovy 类。要存根和/或模拟这些类,您可以使用 Spock 或 Java 模拟库之一。

预期行为

在这种情况下,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

【问题讨论】:

    标签: java unit-testing groovy metaprogramming spock


    【解决方案1】:

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

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

    【讨论】:

    • 一个关于使用 DI 的好建议,但 @CompileStatic 在另一个类中使用,而我们试图覆盖在第一个类中使用的不同类的构造函数。所以这也很重要?
    • Yes @CompileStatic 将在编译时解析 RESTClient 的构造函数在您的 FooClass 中,因此它不会使用元类在运行时将其锁定。如果你想看看它的外观,我建议使用反编译器,例如ByteCode 查看器并查看生成的字节码。
    • 正确,我错过了编译时构造函数。感谢您的帮助。
    【解决方案2】:

    Leonard Brünings评论后:

    是的,@CompileStatic 将在编译时解析 FooClass 中 RESTClient 的构造函数,因此它不会在运行时使用元类将其锁定。如果你想看看它的外观,我建议使用反编译器,例如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 概念。

    【讨论】:

      猜你喜欢
      • 2014-05-22
      • 2019-11-12
      • 1970-01-01
      • 1970-01-01
      • 2015-02-01
      • 1970-01-01
      • 2021-01-17
      • 1970-01-01
      • 2017-04-09
      相关资源
      最近更新 更多