GEP-11


元数据
编号

GEP-11

标题

Groovy 3 语义和新的 MOP

版本

4

类型

功能

状态

草稿

负责人

Jochen "blackdrag" Theodorou

创建日期

2012-06-26

上次修改 

2018-10-28

摘要:Groovy 3 语义和新的 MOP

一段时间以来,我们一直在考虑为 Groovy 开发一个新的 MOP,但一直无法决定最终的设计,该设计在很大程度上保持向后兼容性,但也改进我们想要改进的各种情况。在用户友好性和效率之间找到正确的方法很困难。因此,新的 MOP 将包含一些旧 MOP 中没有的功能,但也将删除一些旧功能,以实现更直接、更容易理解的 MOP。

基本原理

删除默认空参数

默认空参数用于具有一个参数的方法调用,但调用是在没有参数的情况下进行的。Groovy 在这里将使用空值,只要参数的类型不是基本类型。如果是基本类型,调用将失败。此功能主要是在 Groovy 1.0 之前的早期版本中添加的,当时 Groovy 不支持参数的默认值。此逻辑的另一个限制是它仅适用于单参数方法。这种逻辑已经导致了多个用户感到困惑的情况。从性能的角度来看,这种逻辑的成本并不高,当然也不比带有实际参数的调用更高。但由于这是一个使用范围有限的令人困惑的功能,因此应该将其删除。

删除自动列表扩展

使用列表进行的方法调用,如果找不到与该列表匹配的方法(具有类型为 List、Collection、Object 等的一个参数的方法),将导致第二次方法选择迭代。这次将“解包”列表,并将列表的所有元素视为方法调用使用元素而不是列表的方式进行。Groovy 还支持通过语法元素扩展列表,使此自动功能变得不再必要。实际上,这对用户来说可能非常令人惊讶,并且会影响性能。扩展版本在性能方面可能仍然不好,但至少用户将不得不使用一个额外的符号,从而具有视觉指示器。关于最初添加此功能的原因尚不清楚。查看用户代码,您会发现几乎没有预期的用法。因此,应该将其删除。

更改安全导航以停止评估

目前,像 a?.b.c 这样的表达式如果 a 为空将失败。它不会评估 b,但它会尝试在空值上评估 c。这违背了安全导航的意图,即避免 NullPointerException。因此,应该更改为停止路径表达式的评估。

用户愿望清单

  • Wujek Srujek 的基于实例的类别
    与其提供一个带有静态方法的类,不如将一个实例提供给 use-construct,然后使用这个实例及其实例方法。这允许使用实例状态。

  • 由 blackdrag 替换 Introspector
    不仅因为像 GROOVY-5019 这样的错误,应该替换 Introspector

此 MOP 的一些领先设计理念

计划是让我们在 JVM 提供的开放调用站点缓存 invokedynamic 上进行大量方向。为了使这起作用,MOP 的所有部分都不应再被视为执行某些操作的地方,而应被视为返回某些内容的地方,然后将执行这些内容。例如,invokeMethod 将返回一个充当某种处理程序的对象,该对象可以被调用。Groovy 然后将存储它并避免重新选择,除非您使它无效。在旧的 MOP 中,一旦您使用元编程进行交互,这种缓存通常不会发生。为此要解决的任务是提供一个“扩展点”来拦截方法并对缺失方法做出反应,以及能够使缓存版本无效和/或使未缓存版本成为可能。

MOP2 概述

image

在伪代码中的 x 的元类中

cachable = getMethodInterceptor(...)
if (cachable==null) cachable = getFittingMethod(...)
if (cachable==null) getMethodMissingHandler(...)
if (cachable==null) throw new MethodMissingException(...)
storeInCallSite(cachable)
invoke(cachable)

作为对方法的解释

  • getMethodInterceptor
    用户注册拦截器的扩展点,该拦截器将用于所有方法调用。这与 GroovyInterceptable 使用 invokeMethod 所做的事情相同,只是在 MOP1 中直接调用了该方法。在 MOP2 中,我们将改为返回一个处理程序,然后该处理程序将执行所需的操作。例如,这可以是想要使用的 invokeMethod 方法的 method handle。TODO:定义保护

  • getFittingMethod
    通过元类选择方法的预定义方法。此方法将返回一个对象,然后可以使用该对象来调用所选方法。

  • getMethodMissingHandler
    用户注册缺失方法处理程序的扩展点。然后调用的方法类似于 MOP1 中的 methodMissing 会做的事情。

  • MethodMissingException
    如果上述方法都没有产生非空结果,则抛出该异常

  • storeInCallSite
    预定义方法,用于将上述过程的可缓存结果存储在调用站点中。只要缓存对象有效,上述方法将不会再次为该调用站点调用。

  • invoke
    预定义方法,用于执行可缓存对象的初始调用。后续调用可以直接由 invokedynamic 执行。

上下文元类

对于 MOP2,Groovy 将使用一个具有上下文元素和始终存在的默认值的元类系统。在每个调用站点,只有一个这样的视图有效,并且它将是恒定的。这些视图可用于定义“密封”元类,该元类不会受到外部元类的影响,或允许例如调用私有方法,而不允许在其他情况下调用它们。这使得目前有 3 种可能的元类变体

  • 默认元类
    在方法调用方面(相同类别的私有方法可用)的行为与 Java 允许的行为非常相似,并且所有元编程更改都是可见的

  • 密封元类
    与默认元类类似,但来自类外部的元编程更改不可见。这对于库编写者来说尤其有用。

  • 测试元类(未)密封
    与前两者类似,但增加了私有成员始终可用的功能。

获取元类

由于上下文的概念,类没有一个直接的元类,可以在没有其上下文的情况下生成。调用站点定义上下文的放置位置。上下文本身是如何定义的,这是一个 TODO。作为一种实现策略,可以使用 ClassValue 来存储一个表,其中上下文是一个键。该键可能需要作为静态信息或易于计算的信息提供。由于生成的元类可以稍后存储在调用站点对象中,因此应避免上下文更改,因为它意味着使用该上下文的调用站点的失效。

方法(和属性)的通用调度规则

为了正确地定义调度规则,我们需要首先定义一些术语
静态发送者类 (SSC):这是关于调用来自哪个类的静态信息。例如,如果有一个类 A 和一个类 B 扩展 A,并且在 A 中的一个方法中进行了调用,那么即使您的实例实际上是 B,SSC 仍然是 A。
基于继承的多方法(从现在开始简称为多方法):给定一个类 A 和一个类 B 扩展 A,在 A 内部进行的调用可能会看到在 B 上定义的方法,只要该方法是可见的(不是私有的)。然而,Groovy 对此定义了一个特殊的例外。如果在 A 内部进行的方法调用正在调用名为 m 的方法,那么来自 B 的 m 只有在 A 中没有定义私有 m 时才可见。

鉴于这两个定义,A 中的方法调用将根据以下内容选择要从中决定的方法集:使用 SSC A 在 B(扩展 A)的实例上进行的调用 m() 将使用在 A 中定义的方法,如果 A 具有私有 m,否则调用将使用 B 进行。

对 Super 的调用
对 B 扩展 A 中的 super 的调用将具有 SSC B,但对于方法选择过程,将使用 SSC 的超类 (super(SSC))。在 super 调用中,多方法不可见。因此,我们可以直接使用元类 super(SSC),但我们只会在该元类的公有方法上进行调度。

模块扩展方法阴影规则

模块扩展方法在旧的和新的 MOP 中由 DefaultGroovyMethods 相关类和模块扩展定义,例如 groovy-swing。在此定义中,我们将使用来自“内部”和来自“外部”的术语来定义一个调用站点,该调用站点位于与目标方法相同的类中(内部)或不在(外部)。一般规则是

  • 公有方法被遮蔽

  • 私有方法对于外部调用站点被遮蔽,但对于内部调用站点不被遮蔽

已应用模块扩展方法的类的子类具有这些扩展规则

  • 如果子类定义了与模块扩展方法具有相同签名的私有方法,那么外部调用站点将仍然看到扩展方法,内部调用站点将看到私有方法

  • 对“super”或“this”的调用将调用模块扩展方法。因此,子类被视为外部调用站点。

开放块不被视为单独的类。

属性发现

目前,MetaClass 根据 Java Beans 约定发现属性。它还允许与 java.beans.EventSetDescriptor 上的约定匹配的伪属性。这允许在 SwingBuilder 中使用以下技巧,例如

button(actionPerformed: { println it })

伪属性 actionPerformed 是从 ActionListener 公开的单个方法推断出来的,ActionListener 是一种可以注册到 JButton 上的监听器类型。负责发现这些属性的代码埋藏在 MetaClassImpl 中,外部无法访问。如果这种机制可以插入式就好了。

领域概念

在 MOP2 中,领域是一个树状结构,包含元类。有一个根领域,用作默认领域,但可以有任意数量的子领域。如果对同一领域中的元类或更高领域中的元类进行更改,则元类更改在该领域中可见。脚本执行引擎能够设置一个领域,例如,为了防止它们更改它们不应该更改的元类。这可以用于单元测试,以隔离测试期间进行的元类更改。一个库可以有自己的领域(通过注释定义),以防止其他类将它们的更改泄漏到该库中,而该库仍然可以使用更高领域来使更改更公开可见,如果该领域允许的话。领域可以有一个设置,以防止从那里执行的代码对更高领域进行更改。调用方法总是使用当前领域的元类来完成,即使被调用类随后使用它自己的领域来调用其他类。因此,领域不是线程本地结构,它更像是词法作用域。一个领域也可以使用不同的元类风格,例如,允许访问私有方法和字段。

工作项

这部分旨在指导实施者进行子任务的行动方针和计划。

  • 使 indy 成为构建中唯一的编译目标

  • 将所有非 indy 字节码接口代码移至一个模块,该模块可能在以后删除。这包括 ScriptBytecodeAdapter 以及所有自定义调用站点缓存类

  • 为 MOP2 创建一个新模块

  • 将元类转换为不可变类

  • 实现元类视图

破坏性更改跟踪

groovy.lang.MetaObjectProtocol (当前位于 groovy.mop.MetaObjectProtocol 中)

  • getProperties() 重命名为 getMetaProperties()

  • getMethods() 重命名为 getMetaMethods()

  • respondsTo(Object, String, Object[]) 更改为 respondsTo(String, Object…​)

  • respondsTo(Object, String)getMetaMethods(String, Class…​) 替换,其中类参数为 null

  • hasProperty(Object,String) 被 getMetaProperty(String) 替换,如果为空则表示不存在

  • getStaticMetaMethod(String, Object[]) 被 respondsTo(String, Object…​) 替换,并检查列表中的静态方法

  • getMetaMethod(name, Object[]) 被 respondsTo(String, Object…​) 替换(如果参数不是类)以及 getMetaMethods(String,Class…​) 替换(如果参数是类)

  • invokeConstructor(Object[]) 无需替换

  • invokeMethod(Object, String, Object[]) 无需替换

  • invokeMethod(Object, String, Object) 无需替换

  • invokeStaticMethod(Object, String, Object[]) 无需替换

  • getProperty(Object, String) 被 MetaProperty#invoke 替换

  • setProperty(Object, String, Object) 被 MetaProperty#invoke 替换

  • getAttribute(Object, String) 被 MetaProperty#getField#invoke 替换

  • setAttribute(Object, String, Object) 被 MetaProperty#getField#invoke 替换

  • getMetaProperty(String), getTheClass() 不变

groovy.lang.MetaMethod 分裂成一个公共接口 groovy.mop.MetaMethod 和一个内部默认实现 groovy.mop.internal.DefaultMetaMethod

与 groovy.mop.internal.DefaultMetaMethod 的区别

  • 不再扩展 ParameterTypes 也不再实现 Clonable

  • 没有受保护的字段 nativeParamTypes、parameterTypes 和 isVargsMethod

  • 构造函数 MetaMethod() 和 MetaMethod(Class[]) 已删除,部分被 DefaultMetaMethod(Class, String, int, MethodHandle) 和 DefaultMetaMethod(Class, String, MethodType) 替换,它们使用 MethodType 或 MethodHandle 来定义参数类

  • coerceArgumentsToClasses(Object[]), correctArguments(Object[]), isValidExactMethod(Class[]), isValidExactMethod(Object[]), isValidMethod(Class[]), isValidMethod(Object[]), isVargsMethod(), isVargsMethod(Object[]) 无需替换

  • getNativeParameterTypes() 被 getParameterClasses() 替换

  • equal(CachedClass[], CachedClass[]), equal(CachedClass[], Class[]), checkParameters(Class[]), clone(), doMethodInvoke(Object, Object[]), getDescriptor() 无需替换

  • getDeclaringClass(), getModifiers(), getName(), getReturnType(), isAbstract(), isPrivate(), isProtected(), isPublic(), isStatic(), toString() 不变

  • getMopName(), getSignature(), invoke(Object, Object[]), isCacheable(), isMethod(MetaMethod), isSame(MetaMethod), processDoMethodInvokeException(Exception, Object, Object[]) 无需替换

groovy.lang.MetaProperty 分裂成一个公共接口 groovy.mop.MetaProperty 和一个内部默认实现 groovy.mop.internal.DefaultMetaProperty

与 groovy.mop.internal.DefaultMetaProperty 的区别

  • 公共静态字段 PROPERTY_SET_PREFIX 已删除 无需替换

  • 受保护的字段 name 和 type 现在是私有的,必须通过 getName 和 getType 请求

  • getModifiers(), getName(), getType(), DefaultMetaProperty(String, Class) 不变

  • getGetterName(String, Class), getSetterName(String) 无需替换

  • getProperty(Object) 被 getter(boolean) 替换

  • setProperty(Object, Object) 被 setter(boolean) 替换

邮件列表讨论

参考实现

更新历史

3 (2013-10-11)

从 Codehaus Wiki 中提取的版本

4 (2018-10-28)

大量细微调整