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重写:
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
来激活类型检查扩展:
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
代码开始:
move 100
可以注意到,这里不再提到robot
了。我们的扩展将无法提供帮助,因为我们将无法指示编译器在Robot
实例上完成移动。由于groovy.util.DelegatingScript
的帮助,这个代码示例可以以完全动态的方式执行:
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
传递这个,我们必须使用类型检查扩展,所以让我们更新我们的配置:
config.addCompilationCustomizers(
new ASTTransformationCustomizer(
CompileStatic, //透明的应用@CompileStatic
extensions:['robotextension2.groovy']) //使用另一种类型检查扩展来识别移动调用
)
然后在前面的章节中,我们已经学习了如何处理无法识别的方法调用,所以我们能够编写这个扩展:
首先创建一个:robotextension2.groovy
文件,然后添加以下内容:
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
调用即可:
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进行了解。