Groovy-Eclipse 中更好的 DSL 支持

工程 | Andrew Eisenberg | 2011 年 5 月 9 日 | ...

Groovy 语言是创建领域特定语言 (DSL) 的一个优秀平台。一个好的 DSL 可以使程序更简洁、更具表达力,并提高程序员的生产力。但是,到目前为止,Groovy-Eclipse 的编辑器中没有直接支持这些 DSL。当大量使用 DSL 时,标准 IDE 功能(如内容辅助、搜索、悬停和导航)会失去其价值。一段时间以来,一直可以通过编写 Eclipse 插件来扩展 Groovy-Eclipse,但这是一种重量级的方法,需要具备 Eclipse API 的特定知识。现在,Groovy-Eclipse 支持DSL 描述符 (DSLD),在 Groovy-Eclipse 中支持自定义 DSL 将变得更加容易。

一个简单的例子

考虑 Joachim Baumann 描述的这个 DSL。他创建了一个用于处理距离的简单 DSL。使用此 DSL,您可以编写如下代码来计算总行程距离

3.m + 2.yd + 2.mi - 1.km

这是一个简单且富有表现力的 DSL,但是当您将其输入 Groovy-Eclipse 中的 Groovy 编辑器时(为简洁起见,假设$url在其他地方定义)

[caption id="attachment_8774" align="aligncenter" width="179"]Groovy-Eclipse 中未识别的自定义 DSL[/caption]

您会看到下划线并且没有悬停提示,这意味着编辑器无法静态解析 DSL 的表达式。使用 DSLD,可以教会编辑器这些自定义 DSL 背后的一些语义,并提供悬停提示的文档

[caption id="attachment_8775" align="aligncenter" width="683"]带有文档且无下划线的编辑器中的 DSL[/caption]

要为距离 DSL 创建 DSL 描述符,您只需将一个文件添加到 Groovy 项目中,该文件具有.dsld 文件扩展名,并包含以下内容


currentType( subType( Number ) ).accept {   
   property name:"m", type:"Distance", 
    doc: """A <code>meter</code> from <a href="$url">$url</a>"""
}

此脚本表示每当编辑器中当前正在评估的类型是java.lang.Number的子类型时,都向其添加类型为Distance的'm'属性currentType(subType(Number))部分称为切点,包含对property的调用的代码块称为贡献块。稍后将详细介绍这些概念。

上面的脚本片段不是完整的 DSLD。它仅添加了'm'属性。要完成实现,您可以利用 Groovy 语法的全部功能


currentType( subType( Number ) ).accept {   
    [ m: "meter",  yd: "yard",  cm: "centimerter",  mi: "mile",  km: "kilometer"].each {
      property name:it.key, type:"Distance", 
        doc: """A <code>${it.value}</code> from <a href="$url">$url</a>"""
    }
}

这个简单的例子表明,一个相对较小的脚本可以创建一些强大的 DSL 支持。

DSLD 的结构

DSLD 增强了 Groovy-Eclipse 的类型推断引擎,该引擎在编辑过程中在后台运行。DSLD 由 IDE 评估,并根据需要由推断引擎查询。

DSLD 脚本包含一组切点,每个切点都与一个或多个贡献块相关联。切点大致描述了需要增强类型推断的位置(即哪些上下文中的哪些类型),而贡献块描述了增强方式(即应添加哪些属性和方法)。

提供了许多切点,并在DSLD 文档中用示例进行了详细描述。随着我们开始了解人们将如何创建脚本以及他们需要进行哪些操作,可用切点的集合可能会在未来版本的 DSLD 中扩展。

贡献块是 Groovy 代码块,通过accept方法与切点相关联。您可以在贡献块内部执行的两个主要操作是property(我们之前已介绍过)和method(将方法添加到贡献块中正在分析的类型)。

术语切点借鉴自面向方面编程 (AOP)。实际上,DSLD可以被认为是一种 AOP 语言。DSLD 与AspectJ等典型 AOP 语言的主要区别在于,DSLD 对正在编辑的程序的抽象语法树进行操作,而像 AspectJ 这样的语言则对已编译程序的 Java 字节码进行操作。

DSLD 入门

Codehaus 上的 wiki上提供了完整的 DSLD 文档。在这里,我将简要介绍如何开始使用 DSLD。开始使用

  1. 使用以下更新站点安装最新版本的 Groovy-Eclipse:http://dist.codehaus.org/groovy/distributions/greclipse/snapshot/e3.6/
  2. 在新创建的或现有的 Groovy-Eclipse 项目中,将 DSLD 元脚本复制到项目的源文件夹中。此脚本为 DSLD 文件本身提供编辑支持,并且可在此处获得
  3. 使用向导创建一个新的 DSLD 脚本:文件 -> 新建 -> Groovy DSL 描述符:DSLD 向导
  4. 在新创建的文件中,取消注释示例文本。

currentType(subType('groovy.lang.GroovyObject')).accept {
     property name : 'newProp', type : String, 
        provider : 'Sample DSL', 
        doc : 'This is a sample.  You should see this in content assist for all GroovyObjects:<pre>newProp</pre>'
}

在 DSLD 中,您应该会看到特定于 DSLD 的内容辅助和悬停提示(这来自步骤 2 中添加的元 DSLD 脚本)。它看起来像这样:带有悬停提示的 DSLD 文件内容

  • 现在,您可以创建一个新的 Groovy 脚本并使用您刚刚创建的 DSLD 进行尝试。您可以键入

    
    this.newProp
    
    您应该会看到newProp正确突出显示,并且悬停将显示来自 DSLD 的文档,它应该看起来像这样:在文件中使用示例 DSLD
  • 您可以更改 DSLD。保存更改后,所有 Groovy 脚本和文件将立即获取这些更改。
  • 恭喜!您现在已经实现了您的第一个 DSLD。
  • 您可以从 Groovy -> DSLD 首选项页面查看和管理工作区中的所有 DSLD:DSLD 首选项页面

    在这里,您可以启用/禁用各个脚本,以及选择要编辑的脚本。

    重要提示:由于在实现 DSLD 时查找和修复错误可能会有点隐晦,因此强烈建议您执行以下操作

    脚本的编译时和运行时问题将在这两个位置之一显示。

    Grails 约束语言的 DSLD

    对于更大的示例,让我们看看 Grails 框架。Grails 约束 DSL提供了一种声明性方式来验证 Grails 域类。它清晰简洁,但是如果没有对此 DSL 的直接编辑支持,Grails 程序员将依赖于外部文档,并且可能在运行时才会意识到语法错误。我们可以创建一个 DSLD 来解决此问题

    
    // only available in STS 2.7.0 and above
    supportsVersion(grailsTooling:"2.7.0")
    
    
    // a generic grails artifact is a class that is in a grails project, is not a script and is in one of the 'grails-app' folders
    def grailsArtifact = { String folder -> 
    	sourceFolderOfCurrentType("grails-app/" + folder) & 
    	nature("com.springsource.sts.grails.core.nature") & (~isScript())
    }
     
    // define the various kinds of grails artifacts
    def domainClass = grailsArtifact("domain")
    // we only require domainClass, but we can also reference other kinds of artifacts here
    def controllerClass = grailsArtifact("controllers")
    def serviceClass = grailsArtifact("services")
    def taglibClass = grailsArtifact("taglib")
    
     
    // constraints
    // The constraints DSL is only applicable inside of the static "constraints" field declaration
    inClosure() & (domainClass & enclosingField(name("constraints") & isStatic()) & 
    		(bind(props : properties()) & // 'bind' props to the collection of properties in the domain class
    		currentTypeIsEnclosingType())).accept {
    
    	provider = "Grails Constraints DSL"  // this value will appear in content assist
    
    	// for each non-static property, there are numerous constraints "methods" that are available
    	// define them all here
    	for (prop in props) {
    		if (prop.isStatic()) {
    			continue
    		}
    		if (prop.type == ClassHelper.STRING_TYPE) {
    			method isStatic: true, name: prop.name, params: [blank:Boolean], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [creditCard:Boolean], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [email:Boolean], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [url:Boolean], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [matches:String], useNamedArgs:true
    		} else if (prop.type.name == Date.name) {
    			method isStatic: true, name: prop.name, params: [max:Date], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [min:Date], useNamedArgs:true
    		} else if (ClassHelper.isNumberType(prop.type)) {
    			method isStatic: true, name: prop.name, params: [max:Number], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [min:Number], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [scale:Number], useNamedArgs:true
    		} else if (prop.type.implementsInterface(ClassHelper.LIST_TYPE)) {
    			method isStatic: true, name: prop.name, params: [maxSize:Number], useNamedArgs:true
    			method isStatic: true, name: prop.name, params: [minSize:Number], useNamedArgs:true
    		}
    		method isStatic: true, name: prop.name, params: [unique:Boolean], useNamedArgs:true
    		method isStatic: true, name: prop.name, params: [size:Integer], useNamedArgs:true
    		method isStatic: true, name: prop.name, params: [notEqual:Object], useNamedArgs:true
    		method isStatic: true, name: prop.name, params: [nullable:Boolean], useNamedArgs:true
    		method isStatic: true, name: prop.name, params: [range:Range], useNamedArgs:true
    		method isStatic: true, name: prop.name, params: [inList:List], useNamedArgs:true
    	}
    }
    

    如果复制上面的 DSLD 脚本并将其添加到 Grails 项目中的 DSLD 文件中,则将向 STS 教授约束语言。例如,在以下简单的域类中,您在约束块内获得以下内容辅助:使用约束 DSL

    可以调整上述脚本以添加自定义文档。

    我使用 Groovy,但我没有创建自己的 DSL。我为什么要关心 DSLD?

    即使大多数 Groovy 和 Grails 用户没有实现自己的 DSL,他们也会使用 DSL(在GrailsGaelyk中,通过构建器等)。因此,即使大多数 STS 用户不会创建自己的 DSLD,他们也将从其他人创建的 DSLD 中受益。我们将与库和 DSL 开发人员紧密合作,为 Groovy 生态系统的不同部分创建通用的 DSLD。

    您可以预期在即将发布的 Groovy-Eclipse 版本中,对流行的基于 Groovy 的框架的支持将显著增加。

    DSLD 的当前状态

    DSLD 语言的核心实现现已可用,但随着我们更多地了解用户需要什么以及他们想要支持哪种 DSL,我们将对其进行调整。我们将实现更多切点,扩展文档,并努力在 Groovy-Eclipse 本身中提供一些标准的 DSLD。

    请尝试此处或wiki上介绍的一些 DSLD,并向我们提供关于此博客文章的反馈,在我们的问题跟踪器上,或在Groovy-Eclipse 邮件列表上。

    获取 Spring 电子报

    与 Spring 电子报保持联系

    订阅

    领先一步

    VMware 提供培训和认证,以加速您的进步。

    了解更多

    获得支持

    Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

    了解更多

    即将举行的活动

    查看 Spring 社区中所有即将举行的活动。

    查看全部