Groovy-Eclipse 中更好的 DSL 支援

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

Groovy 語言是建立領域特定語言 (DSL) 的絕佳平臺。一個好的 DSL 可以讓程式更簡潔、更具表現力,並提高程式設計師的生產力。然而,到目前為止,這些 DSL 在編輯器中並沒有得到 Groovy-Eclipse 的直接支援。當 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"]Custom DSL not recognized in Groovy-Eclipse[/caption]

你會看到下劃線,沒有懸停,這意味著編輯器無法靜態解析 DSL 的表示式。使用 DSLD,可以編輯器這些自定義 DSL 背後的一些語義,併為懸停提供文件

[caption id="attachment_8775" align="aligncenter" width="683"]DSL in editor with documentation and no underlines[/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 的未來版本中擴充套件。

貢獻塊是透過 accept 方法與切入點關聯的 Groovy 程式碼塊。您可以在貢獻塊中執行的兩個主要操作是 property(我們之前介紹過)和 method(它向貢獻塊中分析的型別新增一個方法)。

切入點”一詞借用自面向切面程式設計 (AOP)。實際上,DSLD 可以被認為是一種 AOP 語言。DSLD 和典型的 AOP 語言(如 AspectJ)之間的主要區別在於,DSLD 對正在編輯的程式的抽象語法樹進行操作,而像 AspectJ 這樣的語言對已編譯程式的 Java 位元組碼進行操作。

DSLD 入門

Codehaus 維基上提供了完整的 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 Wizard
  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 指令碼)。它看起來像這樣:Contents of DSLD file with hover

  • 現在,您可以建立一個新的 Groovy 指令碼並使用您剛剛建立的 DSLD 進行嘗試。您可以輸入

    
    this.newProp
    
    您應該會看到 newProp 被正確高亮顯示,並且懸停將顯示來自 DSLD 的文件,它看起來應該像這樣:Using the sample DSLD in a file
  • 您可以對 DSLD 進行更改。儲存後,更改將立即在所有 Groovy 指令碼和檔案中生效。
  • 恭喜!您現在已經實現了您的第一個 DSLD。
  • 您可以從 Groovy -> DSLD 首選項頁面檢視和管理工作空間中的所有 DSLD:DSLD Preference page

    在這裡,您可以啟用/停用單個指令碼,並選擇要編輯的指令碼。

    重要提示:由於在實現 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 將會學習約束語言。例如,在以下簡單域類中,您會在約束塊內獲得內容輔助:Using the constraints DSL

    上述指令碼可以調整以新增自定義文件。

    我使用 Groovy,但我沒有建立自己的 DSL。我為什麼要在意 DSLD?

    儘管大多數 Groovy 和 Grails 使用者不實現自己的 DSL,但他們使用 DSL(在 GrailsGaelyk 中,透過 構建器 等)。因此,儘管大多數 STS 使用者不會建立自己的 DSLD,但他們將受益於他人建立的 DSLD。我們將與庫和 DSL 開發人員密切合作,為 Groovy 生態系統的不同部分建立通用 DSLD。

    您可以期望在 Groovy-Eclipse 的即將釋出的版本中看到對流行的基於 Groovy 的框架的支援顯著增加。

    DSLD 的當前狀態

    DSLD 語言的核心實現現已可用,但我們將根據對使用者需求以及他們希望支援的 DSL 型別的更多瞭解進行調整。我們將實現更多的切入點,擴充套件文件,並努力將一些標準 DSLD 與 Groovy-Eclipse 本身一起釋出。

    請嘗試此處或維基上介紹的一些 DSLD,並透過此部落格文章、我們的問題跟蹤器Groovy-Eclipse 郵件列表向我們提供反饋。

    獲取 Spring 新聞通訊

    透過 Spring 新聞通訊保持聯絡

    訂閱

    領先一步

    VMware 提供培訓和認證,助您加速進步。

    瞭解更多

    獲得支援

    Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

    瞭解更多

    即將舉行的活動

    檢視 Spring 社群所有即將舉行的活動。

    檢視所有