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"]自訂 DSL 在 Groovy-Eclipse 中無法識別[/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 的子類型時,將 'm' 屬性新增至該類型,其類型為 DistancecurrentType(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 與典型的 AOP 語言(如 AspectJ)之間的主要區別在於,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 Meta 腳本複製到專案的來源資料夾中。這個腳本為 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 中新增的 meta-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 社群中所有即將舉辦的活動。

    查看全部