在 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 社群的所有即將到來的活動。

    檢視全部