在 Groovy-Eclipse 中更好地支持 DSL

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

Groovy 语言是创建领域特定语言 (DSL) 的优秀平台。好的 DSL 可以使程序更加简洁和富有表现力,同时提高程序员的生产力。然而,直到现在,这些 DSL 在编辑器中还没有得到 Groovy-Eclipse 的直接支持。当大量使用 DSL 时,内容辅助、搜索、悬停提示和导航等标准 IDE 功能就会失去价值。虽然现在已经可以编写 Eclipse 插件来扩展 Groovy-Eclipse,但这是一种重量级的方法,需要对 Eclipse API 有专门的了解。现在,Groovy-Eclipse 支持 DSL 描述符 (DSLDs),在 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)) 部分称为 *pointcut*(切入点),调用 property 的代码块称为 *contribution block*(贡献块)。稍后将详细介绍这些概念。

上面的脚本片段不是完整的 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 脚本包含一组切入点(pointcuts),每个切入点都与一个或多个贡献块(contribution blocks)相关联。切入点大致描述了需要增强类型推断的*位置*(即哪些上下文中的哪些类型),而贡献块描述了*如何*增强(即应该添加哪些属性和方法)。

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

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

*切入点(pointcut)*一词借用了面向切面编程 (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。为什么我应该关心 DSLDs?

    尽管大多数 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 社区的所有即将到来的活动。

    查看全部