返回介绍

6 编译自定义器(Compilation customizers)

发布于 2025-01-04 00:44:53 字数 13106 浏览 0 评论 0 收藏 0

6.1 简介

无论你使用 groovyc 还是采用 GroovyShell 来编译类,要想执行脚本,实际上都会使用到 编译器配置compiler configuration)信息。这种配置信息保存了源编码或类路径这样的信息,而且还用于执行更多的操作,比如默认添加导入,显式使用 AST 转换,或者禁止全局 AST 转换,等等。

编译自定义器的目标在于使这些常见任务易于实现。 CompilerConfiguration 类就是切入点。基本架构通常都会基于下列代码:

import org.codehaus.groovy.control.CompilerConfiguration
// 创建配置信息  
def config = new CompilerConfiguration()
// 微调配置信息  
config.addCompilationCustomizers(...)
// 运行脚本   
def shell = new GroovyShell(config)
shell.evaluate(script)

编译自定义器必须扩展自 org.codehaus.groovy.control.customizers.CompilationCustomizer 类。自定义器适用于:

  • 特定的编译过程。
  • 正在编译的 每个 类节点。

当然,你可以实现自己的编译自定义器,但 Groovy 包含了一些最常见的操作。

6.2. 导入自定义器

使用这种编译自定义器,代码可以显式地添加导入。假如脚本想实现一种能够避免用户不得不手动导入的 DSL,那么这就非常有用了。导入自定义器将使你添加 Groovy 所允许的所有导入形式,包括:

  • 类导入,可选别名。
  • 星号导入。
  • 静态导入,可选别名。
  • 静态星号导入。

import org.codehaus.groovy.control.customizers.ImportCustomizer

def icz = new ImportCustomizer()
// 通常的导入
icz.addImports('java.util.concurrent.atomic.AtomicInteger', 'java.util.concurrent.ConcurrentHashMap')
// 别名导入
icz.addImport('CHM', 'java.util.concurrent.ConcurrentHashMap')
// 静态导入  
icz.addStaticImport('java.lang.Math', 'PI') // import static java.lang.Math.PI
// 别名静态导入
icz.addStaticImport('pi', 'java.lang.Math', 'PI') // import static java.lang.Math.PI as pi
// 星号导入
icz.addStarImports 'java.util.concurrent' // import java.util.concurrent.*
// 静态星号导入
icz.addStaticStars 'java.lang.Math' // import static java.lang.Math.*

详细描述见于 org.codehaus.groovy.control.customizers.ImportCustomizer

6.3 AST 转换自定义器

AST 转换自定义器可以用来显式地应用 AST 转换。对于全局型 AST 转换而言,只要转换存在于类路径中,被编译的每个类都会应用转换(相应的缺点是增加编译时间,或者转换了不该转换的)。自定义转换器能实现选择应用转换,只针对特定的脚本或类应用转换。

比如想在脚本中能够使用 @Log ,那么问题在于 @Log 一般应用于类节点上,而根据定义,脚本并不需要。但如果实现得好,脚本也就是类,只是你不能把这种隐式的类节点用 @Log 来注释,而使用 AST 自定义器,我们可以进行一个全变措施:

import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.util.logging.Log

def acz = new ASTTransformationCustomizer(Log)
config.addCompilationCustomizers(acz)

只需这样即可! @Log AST 转换被应用到编译单元的每个类节点上。这意味着它将应用到脚本上,以及脚本内所定义的类上。

如果使用的 AST 转换接收一些参数,也可以在构造函数中使用这些参数:

def acz = new ASTTransformationCustomizer(Log, value: 'LOGGER')
// 使用 'LOGGER' 而非默认的 'log'  
config.addCompilationCustomizers(acz)

因为 AST 转换自定义器用于对象而不是 AST 节点,所以并不是所有值都被转换为 AST 转换参数。比如说,原始类型被转换为 ConstantExpressionLOGGER 被转换为 new ConstantExpression('LOGGER') ),但如果你的 AST 转换将闭包作为参数,那么必须要给它一个 ClosureExpression ,如下例所示:

def configuration = new CompilerConfiguration()
def expression = new AstBuilder().buildFromCode(CompilePhase.CONVERSION) { -> true }.expression[0]
def customizer = new ASTTransformationCustomizer(ConditionalInterrupt, value: expression, thrown: SecurityException)
configuration.addCompilationCustomizers(customizer)
def shell = new GroovyShell(configuration)
shouldFail(SecurityException) {
  shell.evaluate("""
    // 等于添加了 @ConditionalInterrupt(value={true}, thrown: SecurityException)
    class MyClass {
      void doIt() { }
    }
    new MyClass().doIt()
  """)
}

完整的选项列表参见: org.codehaus.groovy.control.customizers.ASTTransformationCustomizer

6.4 安全 AST 自定义器

该自定义器允许 DSL 的开发者限制语言的 语法 ,从而防止用户使用一些结构。它只有在这种意义上说才是安全的,而且重要的是,它不能代替安全管理器。它存在的唯一理由就是为了限制语言的表现力。自定义器只适用于 AST(抽象语法树)级别,而不是在运行时。乍看起来,比较奇怪,但如果把 Groovy 看成是 DSL 的构建平台的话,就顺理成章了。你可能不希望用户利用完整的语言。在下例中,只允许使用运算操作。该自定义器可以实现:

  • 允许/不允许创建闭包。
  • 允许/不允许导入。
  • 允许/不允许包定义。
  • 允许/不允许方法定义。
  • 限制方法调用的接收者。
  • 限制用户所能使用的 AST 表达式种类。
  • 限制用户所能使用的令牌(语法明智)。
  • 限制代码中常量的类型。

安全 AST 自定义器使用白名单(允许的元素列表)或黑名单(不允许的元素列表)策略来实现这些功能。对于每一类功能(导入、令牌,等等),都可以选择究竟使用白名单还是黑名单。还可以混合使用两种名单来实现一些独特的功能。一般选择白名单(不允许选择还是允许选择)即可。


import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import static org.codehaus.groovy.syntax.Types.*       1⃣️

def scz = new SecureASTCustomizer()
scz.with {
  closuresAllowed = false // 用户不能写闭包
  methodDefinitionAllowed = false // 用户不能定义方法
  importsWhitelist = [] // 白名单为空意味着不允许导入
  staticImportsWhitelist = [] // 同样,对于静态导入也是这样
  staticStarImportsWhitelist = ['java.lang.Math'] // 只允许 java.lang.Math 
  // 用户能找到的令牌列表  
  // org.codehaus.groovy.syntax.Types 中所定义的常量  
  tokensWhitelist = [          1⃣️
      PLUS,
      MINUS,
      MULTIPLY,
      DIVIDE,
      MOD,
      POWER,
      PLUS_PLUS,
      MINUS_MINUS,
      COMPARE_EQUAL,
      COMPARE_NOT_EQUAL,
      COMPARE_LESS_THAN,
      COMPARE_LESS_THAN_EQUAL,
      COMPARE_GREATER_THAN,
      COMPARE_GREATER_THAN_EQUAL,
  ].asImmutable()
  // 将用户所能定义的常量类型限制为数值类型
  constantTypesClassesWhiteList = [        2⃣️
      Integer,
      Float,
      Long,
      Double,
      BigDecimal,
      Integer.TYPE,
      Long.TYPE,
      Float.TYPE,
      Double.TYPE
  ].asImmutable()
  // 如果接收者是其中一种类型,只允许方法调用
  // 注意,并不是一个运行时类型! 
  receiversClassesWhiteList = [           2⃣️ 
      Math,
      Integer,
      Float,
      Double,
      Long,
      BigDecimal
  ].asImmutable()
}

1⃣️ 用于 org.codehaus.groovy.syntax.Types 中的令牌类型
2⃣️ 可以使用类字面量。

如果安全 AST 自定义器满足不了你的需求,那么在创建自己的编译自定义器之前,要考虑一下 AST 自定义器所支持的表达式和语句检查器。一般而言,允许在 AST 树上,表达式上(表达式检查器)或语句(语句检查器)添加自定义检查。为此,必须实现 org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementCheckerorg.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker

这些接口定义了一个 isAuthorized 方法,它能返回一个布尔值,能够接收 StatementExpression 作为参数。该方法可以在表达式或语句上实现复杂的逻辑,是否允许用户去实现

比如说,自定义器上没有能够防止用户使用某个属性表达式的预定义配置标识,那么使用自定义检查器,一切就很简单:

def scz = new SecureASTCustomizer()
def checker = { expr ->
  !(expr instanceof AttributeExpression)
} as SecureASTCustomizer.ExpressionChecker
scz.addExpressionCheckers(checker)

然后通过计算一个简单的脚本就可以确保它的有效性:


new GroovyShell(config).evaluate '''
  class A {
    int val
  }

  def a = new A(val: 123)
  a.@val            1⃣️
'''

1⃣️ 会导致编译失败。

语句检查方面可参见:
org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker

表达式检查方面参见:
org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker

6.5 源识别自定义器

该自定义器可以当做其他自定义器上的过滤器。这种情况下的过滤器是 org.codehaus.groovy.control.SourceUnit 。源识别自定义器将其他自定义器作为一种委托,它只应用委托自定义,并且只有在源单位上的谓词相匹配才进行。

SourceUnit 可以让我们访问多项内容,但主要是针对被编译的文件(如果编译的是文件,理当如此)。可以根据文件名称来实施操作。范例如下:

import org.codehaus.groovy.control.customizers.SourceAwareCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer

def delegate = new ImportCustomizer()
def sac = new SourceAwareCustomizer(delegate)

然后就可以使用源识别自定义器上的谓词了:

// 自定义器只应用到名称以 'Bean' 结尾的文件内的类上 
sac.baseNameValidator = { baseName ->
  baseName.endsWith 'Bean'
}

// 自定义器只应用到扩展名为 '.spec' 的文件内的类上  
sac.extensionValidator = { ext -> ext == 'spec' }

// 源单位验证 
// 只有当文件包含至少一个类时才允许编译  
sac.sourceUnitValidator = { SourceUnit sourceUnit -> sourceUnit.AST.classes.size() == 1 }

// 类验证
// 自定义器只应用于结尾是 `Bean` 的类上  
sac.classValidator = { ClassNode cn -> cn.endsWith('Bean') } 

6.6 自定义器构建器

如果在 Groovy 代码中使用编译自定义器(如上面那些例子所示),则可以采用替代语法来自定义编译。可以使用一种构建器( org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder )来简化自定义器的创建,使用的是层级 DSL。

import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig     1⃣️

def conf = new CompilerConfiguration()
withConfig(conf) {
  // ...      2⃣️
}

1⃣️ 构建器方法的静态导入。
2⃣️ 这里放的是相关配置信息。

上例代码展示的是构建器的使用。静态方法 withConfig 获取一个跟构建器相关的闭包,自动将编译自定义器注册到配置信息中。分发的每一个编译自定义器都可以用这种方式来配置:

6.6.1 导入自定义器

withConfig(configuration) {
   imports { // imports customizer
    normal 'my.package.MyClass' // a normal import
    alias 'AI', 'java.util.concurrent.atomic.AtomicInteger' // an aliased import
    star 'java.util.concurrent' // star imports
    staticMember 'java.lang.Math', 'PI' // static import
    staticMember 'pi', 'java.lang.Math', 'PI' // aliased static import
   }
}

6.6.2 AST 转换自定义器

withConfig(conf) {
   ast(Log)           1⃣️
}

withConfig(conf) {
   ast(Log, value: 'LOGGER')    2⃣️
}

1⃣️ 显式使用 @Log 。 2⃣️ 应用 @Log ,并使用 logger 的另一个名字

6.6.3 安全 AST 自定义器

withConfig(conf) {
   secureAst {
     closuresAllowed = false
     methodDefinitionAllowed = false
   }
}

6.6.4 源识别自定义器


withConfig(configuration){
  source(extension: 'sgroovy') {
    ast(CompileStatic)         1⃣️
  }
}

withConfig(configuration){
  source(extensions: ['sgroovy','sg']) {
    ast(CompileStatic)         2⃣️
  }
}

withConfig(configuration) {
  source(extensionValidator: { it.name in ['sgroovy','sg']}) {
    ast(CompileStatic)          2⃣️
  }
}

withConfig(configuration) {
  source(basename: 'foo') {
    ast(CompileStatic)          3⃣️
  }
}

withConfig(configuration) {
  source(basenames: ['foo', 'bar']) {
    ast(CompileStatic)          4⃣️ 
  }
}

withConfig(configuration) {
  source(basenameValidator: { it in ['foo', 'bar'] }) {
    ast(CompileStatic)          4⃣️
  }
}

withConfig(configuration) {
  source(unitValidator: { unit -> !unit.AST.classes.any { it.name == 'Baz' } }) {
    ast(CompileStatic)          5⃣️ 
  }
}

1⃣️ 在 .sgroovy 文件上应用 AST 注释 CompileStatic
2⃣️ 在 .sgroovy 或 .sg 文件上应用 AST 注释 CompileStatic
3⃣️ 在名称为 foo 的文件上应用 AST 注释 CompileStatic
4⃣️ 在名称为 foo or bar 的文件上应用 AST 注释 CompileStatic
5⃣️ 在不包含名为 Baz 的类的文件上应用 AST 注释 CompileStatic

6.6.5 内联自定义器

内联自定义器可以让你直接编写一个编译自定义器,而不必为其创建任何类:

withConfig(configuration) {
  inline(phase:'CONVERSION') { source, context, classNode ->       1⃣️
    println "visiting $classNode"                  2⃣️
  }
}

1⃣️ 定义一个能在 CONVERSION 阶段执行的内联自定义器。
2⃣️ 打印正在编辑的类节点名称。

6.6.6 多个自定义器

当然,构建器还可以让你一次构建多个自定义器:

withConfig(configuration) {
   ast(ToString)
   ast(EqualsAndHashCode)
}

6.7 配置脚本标记

迄今为止,我们介绍了如何利用 CompilationConfiguration 类来自定义编译,但这是有一个前提条件的:内嵌 Groovy,并且创建了自己的 CompilerConfiguration 实例(然后用它来创建 GroovyShellGroovyScriptEngine ,等等)。

如果想把它用在那些利用普通 Groovy 编译器(也就是说利用 groovycantgradle )编译的类上,可以使用一个编译标记 configscript ,它以一个 Groovy 配置脚本作为参数。

该脚本可以让你在文件编译前(以名为 configuration 的变量暴露给配置脚本)访问 CompilerConfiguration 实例,因此还可以微调。

也可以显式地结合上面介绍的编译器配置构建器。下例展示了如何在所有的类上都默认激活静态编译。

6.7.1. 默认静态编译

通常,Groovy 中的类都是在动态运行时进行编译的。可以把 @CompileStatic 注释放在任何类上来激活静态编译。一些人可能喜欢默认激活这种模式,也就是不用手动地去注释类。使用 configscript 就可以。首先需要在 src/conf 上创建一个名为 config.groovy 的文件,内容如下:

withConfig(configuration) {          1⃣️
   ast(groovy.transform.CompileStatic)
}

1⃣️ configuration 引用了一个 CompilerConfiguration 实例。

所需的就这么多。不必导入构建器,因为它会自动暴露在脚本中。然后,使用下列命令编译文件即可:

groovyc -configscript src/conf/config.groovy src/main/groovy/MyClass.groovy

我们强烈建议你将类与配置文件分开,这就是我们为何之前建议使用 src/mainsrc/conf 目录的原因。

6.8 AST 转换

如果:

  • 运行时元编程不能满足你的要求。
  • 需要提升 DSL 的执行效率。
  • 想要利用 Groovy 的语法,但语义与之有所不同。
  • 改善 DSL 中对于类型检查的支持。

那么使用 AST 转换是一条路子。与迄今为止所使用的技术不同,AST 转换是在编译成字节码之前就进行改变或生成代码。AST 转换能够在运行时添加新的方法,或根据需求彻底改变方法体。它们是很强大的工具,但编写起来比较难。有关 AST 转换的详细信息,可参考本手册的 编译时元编程 部分。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文