39. Groovy 类型检查扩展,最终篇 高级类型检查扩展

2023-02-23 17:55:17 浏览数 (2)

1. 介绍

本篇内容为Groovy类型检查扩展的最终篇。高级类型检查扩展。本篇结束后,关于Groovy中的类型检查扩展的相关知识就分享结束了。

2. 高级类型检查扩展

想了解前面两篇关于类型检查扩展的知识可以访问:

  • 38. Groovy 类型检查扩展,第二篇 使用扩展
  • 37. Groovy 类型检查扩展,第一篇 编写类型检查扩展

2.1 预编译的类型检查扩展

在前面两篇文章中的所有示例都使用类型检查脚本。它们在类路径中以源形式存在,这意味着:

  • 对应于类型检查扩展的Groovy源文件在编译类路径上可用
  • 这个文件由Groovy编译器为每个被编译的源单元编译(通常,一个源单元对应一个文件)

这是开发类型检查扩展的一种非常方便的方法,但是它意味着一个较慢的编译阶段,因为要为正在编译的每个文件编译扩展本身。

出于这些原因,依赖预编译的扩展是可行的。通常有两个选择:

  • 在Groovy中编写扩展,编译它,然后使用扩展类的引用而不是源代码(简单)
  • 用Java编写扩展,编译它,然后使用扩展类的引用

用Groovy编写类型检查扩展是最简单的方法。基本上,其思想是,类型检查扩展脚本成为类型检查扩展类的主要方法的主体,如下所示:

代码语言:javascript复制
import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport

class PrecompiledExtension extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {//扩展TypeCheckingDSL类是最简单的     
    @Override
    Object run() {  //然后扩展代码需要进入run方法内部                                                                     
        unresolvedVariable { var ->
            if ('robot'==var.name) {
                storeType(var, classNodeFor(Robot))  //可以使用相同的事件作为以源代码形式编写的扩展                                       
                handled = true
            }
        }
    }
}

设置扩展非常类似于使用源表单扩展:

代码语言:javascript复制
config.addCompilationCustomizers(
    new ASTTransformationCustomizer(
        TypeChecked,
        extensions:['typing.PrecompiledExtension'])
)

不同之处在于,只需指定预编译扩展的完全限定类名,而不是在类路径中使用路径。

如果真的想用Java编写扩展,那么将无法从类型检查扩展DSL中获益。上面的扩展可以用Java重写:

代码语言:javascript复制
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.transform.stc.AbstractTypeCheckingExtension;


import org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor;
//扩展AbstractTypeCheckingExtension类
public class PrecompiledJavaExtension extends AbstractTypeCheckingExtension {                   

    public PrecompiledJavaExtension(final StaticTypeCheckingVisitor typeCheckingVisitor) {
        super(typeCheckingVisitor);
    }

    //然后根据需要重写handleXXX方法
    @Override
    public boolean handleUnresolvedVariableExpression(final VariableExpression vexp) {          
        if ("robot".equals(vexp.getName())) {
            storeType(vexp, ClassHelper.make(Robot.class));
            setHandled(true);
            return true;
        }
        return false;
    }

}

除此之外,还有其他的扩展方法。

2.2 在类型检查扩展中使用@Grab

在类型检查扩展中使用@Grab注释。这意味着可以包含仅在编译时可用的库。

在这种情况下,我们必须明白这会显著增加编译时间(至少在第一次获取依赖项时)。

PS:官方文档上也没有个示例,所以我也没有示例了。

2.3 共享或打包类型检查扩展

类型检查扩展只是一个需要在类路径上的脚本。

因此,可以按原样共享它,或者将其捆绑在一个jar文件中,并添加到类路径中。

2.4 全局类型检查扩展

虽然可以配置编译器透明地将类型检查扩展添加到脚本中,

但目前还无法通过将扩展放在类路径中透明地应用扩展。

2.5 类型检查扩展和@CompileStatic

类型检查扩展与@typecheck一起使用,但也可以与@CompileStatic一起使用,但是使用时需要注意两点:

  • @CompileStatic一起使用的类型检查扩展通常不足以让编译器知道如何从“不安全”的代码生成静态可编译的代码。
  • 可以使用@CompileStatic类型检查扩展来增强类型检查,也就是说引入更多的编译错误,而不实际处理动态代码。

让我们解释第一点,也就是说即使使用扩展,编译器也不知道如何静态编译我们的代码:从技术上讲,即使我们告诉类型检查器动态变量的类型是什么,例如,它也不知道如何编译它。

它是getBinding('foo')getProperty('foo')delegate.getFoo(),…?即使使用类型检查扩展,也绝对没有直接的方法告诉静态编译器如何编译这样的代码(同样,这只会给出关于类型的提示)。

对于这个特殊示例,一个可能的解决方案是指示编译器使用混合模式编译。更高级的方法是在类型检查期间使用AST转换,但这种方法要复杂地多。

类型检查扩展允许在类型检查失败的地方帮助它,但它也允许在它没有失败的地方失败。在这种情况下,支持@CompileStatic的扩展也很有意义。想象一个能够对SQL查询进行类型检查的扩展。在这种情况下,扩展在动态和静态上下文中都是有效的,因为没有扩展,代码仍然可以通过。

2.6 混合模式编译

在上面中,我们强调了可以使用@CompileStatic激活类型检查扩展。在这种情况下,类型检查器不会再抱怨一些未解析的变量或未知的方法调用,但它仍然不知道如何静态编译它们。

混合模式编译提供了第三种方法,即指示编译器无论何时发现未解析的变量或方法调用,都应该退回到动态模式。

这要归功于类型检查扩展和一个特殊的makdynamic调用。

让我们回到Robot的例子来介绍:(前面两篇内容中有介绍Robot的示例)

代码语言:javascript复制
robot.move 100

让我们尝试使用@CompileStatic而不是@TypeChecked来激活类型检查扩展:

代码语言:javascript复制
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
    new ASTTransformationCustomizer(
        CompileStatic,                  //透明的应用@CompileStatic                    
        extensions:['robotextension.groovy'])  //激活类型检查扩展             
)
def shell = new GroovyShell(config)
def robot = new Robot()
shell.setVariable('robot', robot)
shell.evaluate(script)

该脚本将正常运行,因为静态编译器被告知机器人变量的类型,因此它能够直接调用move

但在此之前,编译器是如何知道如何获取机器人变量的呢? 事实上,默认情况下,在类型检查扩展中,对未解析变量设置handled=true将自动触发动态解析。

因此在这种情况下,没有任何特殊的东西可以让编译器使用混合模式。

然而,让我们稍微更新一下我们的例子,从robot代码开始:

代码语言:javascript复制
move 100

可以注意到,这里不再提到robot了。我们的扩展将无法提供帮助,因为我们将无法指示编译器在Robot实例上完成移动。由于groovy.util.DelegatingScript的帮助,这个代码示例可以以完全动态的方式执行:

代码语言:javascript复制
def config = new CompilerConfiguration()
config.scriptBaseClass = 'groovy.util.DelegatingScript' //我们将编译器配置为使用DelegatingScript作为基类    
def shell = new GroovyShell(config)
def runner = shell.parse(script) //脚本源需要被解析,并将返回一个DelegatingScript实例              
runner.setDelegate(new Robot())  //然后我们可以调用setDelegate来使用Robot作为脚本的委托                           
runner.run()  //然后执行脚本。Move将直接在委托上执行

如果我们想通过@CompileStatic传递这个,我们必须使用类型检查扩展,所以让我们更新我们的配置:

代码语言:javascript复制
config.addCompilationCustomizers(
    new ASTTransformationCustomizer(
        CompileStatic,    //透明的应用@CompileStatic                                        
        extensions:['robotextension2.groovy'])   //使用另一种类型检查扩展来识别移动调用           
)

然后在前面的章节中,我们已经学习了如何处理无法识别的方法调用,所以我们能够编写这个扩展:

首先创建一个:robotextension2.groovy文件,然后添加以下内容:

代码语言:javascript复制
methodNotFound { receiver, name, argList, argTypes, call ->
    if (isMethodCallExpression(call) //如果调用是一个方法调用(不是静态方法调用)                       
        && call.implicitThis          //这个调用是在“隐式this”(没有显式this)上进行的。
        && 'move'==name               // 被调用的方法是move                     
        && argTypes.length==1          // 调用是用一个参数完成的                    
        && argTypes[0] == classNodeFor(int)   //这个参数的类型是int              
    ) {
        handled = true        // 首先告诉类型检查器调用是有效的                             
        newMethod('move', classNodeFor(Robot))     //并且调用的返回类型是Robot         
    }
}

如果你尝试执行这段代码,那么你可能发现它在运行时实际上失败了,错误日志为:

代码语言:javascript复制
java.lang.NoSuchMethodError: java.lang.Object.move()Ltyping/Robot;

原因很简单:虽然类型检查扩展对于不涉及静态编译的@TypeChecked已经足够了,

但是对于需要额外信息的@CompileStatic来说还不够。

在本例中,我们告诉编译器该方法存在,但没有向它解释它实际上是什么方法,以及消息的接收者(委托)是什么。

修复这个问题非常简单,只需用其他方法替换newMethod调用即可:

代码语言:javascript复制
methodNotFound { receiver, name, argList, argTypes, call ->
    if (isMethodCallExpression(call)
        && call.implicitThis
        && 'move'==name
        && argTypes.length==1
        && argTypes[0] == classNodeFor(int)
    ) {
        makeDynamic(call, classNodeFor(Robot))//告诉编译器调用应该是动态的              
    }
}

makdynamic调用做了3件事:

  • 它返回一个像newMethod一样的虚方法。
  • 自动为您设置handled 标志为true
  • 而且还将call标记为动态完成。

因此,当编译器必须为move调用生成字节码时,由于它现在被标记为动态调用,

它将回落到动态编译器并让它处理调用。由于扩展告诉我们动态调用的返回类型是Robot,因此后续调用将静态完成!

有些人会想,为什么静态编译器在没有扩展的情况下默认不这样做。这是一个设计决策:

  • 如果代码是静态编译的,我们通常希望类型安全和最佳性能
  • 如果无法识别的变量/方法调用是动态进行的,那么您就失去了类型安全,而且还在编译时支持所有的拼写错误!

简而言之,如果您想要混合模式编译,它必须是显式的,通过类型检查扩展,以便编译器和DSL的设计者完全知道他们在做什么。

makdynamic可以用在3种AST节点上:

  • 方法节点:MethodNode
  • 变量:VariableExpression
  • 属性表达式:PropertyExpression

如果这还不够,那么这意味着不能直接进行静态编译,必须依赖AST转换。

2.7 在扩展中转换AST

从AST转换设计的角度来看,类型检查扩展看起来非常有吸引力:

扩展可以访问上下文,比如推断类型,这通常是很好的。

扩展可以直接访问抽象语法树。因为你可以访问AST,理论上没有什么可以阻止你修改AST。

但是,我们不建议你这样做,除非你是一个高级的AST转换设计师,并且很清楚编译器的内部原理:

  • 首先,将显式地破坏类型检查的契约,即只注释AST。类型检查不应该修改AST树,因为我们将无法保证没有@TypeChecked注释的代码在没有注释的情况下行为相同。
  • 如果我们的扩展打算使用@CompileStatic,那么可以修改AST,因为这确实是@CompileStatic最终要做的事情。静态编译在动态Groovy中不能保证相同的语义,因此使用@CompileStatic编译的代码和使用@TypeChecked编译的代码之间实际上存在差异。这取决于我们选择想要更新AST的任何策略,但可能使用在类型检查之前运行的AST转换更容易。
  • 如果不能依赖于类型检查器之前启动的转换,则必须非常小心

类型检查阶段是编译器在字节码生成之前运行的最后一个阶段。

所有其他AST转换都在此之前运行,编译器在“修复”类型检查阶段之前生成的不正确AST方面做得非常好。

一旦您在类型检查期间执行转换,例如直接在类型检查扩展中执行转换,那么您就必须自己完成生成100%编译器兼容的抽象语法树的所有工作,这很容易变得复杂。

这就是为什么不建议从类型检查扩展和AST转换开始使用这种方法的原因。

2.8 示例

现实生活中类型检查扩展的例子很容易找到。可以下载Groovy的源代码,并查看TypeCheckingExtensionsTest类,该类链接到各种扩展脚本。示例地址为:https://github.com/apache/groovy/tree/master/src/test-resources/groovy/transform/stc

在标记模板引擎源代码中可以找到复杂类型检查扩展的一个例子:该模板引擎依赖于类型检查扩展和AST转换来将模板转换为完全静态编译的代码。示例地址为:

https://github.com/apache/groovy/tree/master/subprojects/groovy-templates/src/main/groovy/groovy/text/markup

3. 小结

关于Groovy中的类型检查扩展的知识就分享结束了。

总的来说,我也看着有点晕头转向的。稍微有一些复杂。以上内容觉得不清晰的,可以参考Groovy官方文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#_advanced_type_checking_extensions进行了解。

0 人点赞