創(chuàng)建 UML 模型并生成代碼
Adrian Powell
Advisory I/T Specialist, IBM
2004 年 4 月
Eclipse Modeling Framework(EMF)是一個(gè)開(kāi)放源代碼的模型驅(qū)動(dòng)應(yīng)用程序開(kāi)發(fā)框架。它可以基于 XML Schema、UML 或經(jīng)過(guò)注釋的 Java 中指定的模型,創(chuàng)建 Java 代碼,實(shí)現(xiàn)圖形化的數(shù)據(jù)編輯、操縱、讀取和序列化。EMF 是 IBM WebSphere Studio 和 Eclipse 項(xiàng)目中很多工具的基礎(chǔ)。本文將幫助您逐步了解創(chuàng)建模型、生成代碼、使用生成的應(yīng)用程序和定制編輯器的整個(gè)過(guò)程。
EMF 究竟是什么?
Eclipse Modeling Framework(EMF)是一個(gè)開(kāi)放源代碼的框架,它的目標(biāo)是實(shí)現(xiàn)模型驅(qū)動(dòng)架構(gòu)(Model-Driven Architecture)的開(kāi)發(fā)。如果我們當(dāng)中的少數(shù)人有幸得到了某個(gè) UML 模型,那么這個(gè)框架就可以幫助我們將文檔變成代碼。至于其他人,這個(gè)工具也使您又有一次機(jī)會(huì)向老板證實(shí),把時(shí)間花在為解決方案建模上是值得的。除了可以生成令人贊嘆的 Java 代碼之外,EMF 還可以生成 Eclipse 插件,以及圖形化的可定制編輯器。當(dāng)您改變模型時(shí)(這種情況真的會(huì)出現(xiàn)),EMF 可以通過(guò)單擊一個(gè)按鈕,就使代碼和模型保持同步。
EMF 生成的代碼也不是一種只配丟進(jìn)垃圾箱的解決方案。這種代碼支持標(biāo)準(zhǔn)的創(chuàng)建、獲取、更新和刪除操作,而且還支持元數(shù)約束、復(fù)雜關(guān)系和繼承結(jié)構(gòu)、屏蔽定義,以及一套屬性描述。生成的代碼還提供通知、參照完整性和可定制的 XMI 持久性。您所需要做的全部工作就是創(chuàng)建一個(gè)對(duì)象模型,就像您以前也想做的那樣。
EMF 是比較新的事物,但前景廣闊,對(duì)它持續(xù)支持的力度也很強(qiáng)。它實(shí)現(xiàn)的是一項(xiàng)公共標(biāo)準(zhǔn),即對(duì)象管理組織(Object Management Group)的元對(duì)象工具(Meta-Object Facility,MOF)。現(xiàn)在 EMF 已經(jīng)對(duì) MOF 的第二版進(jìn)行了增強(qiáng)。更進(jìn)一步看,EMF 還是 EMF:XSD 以及 Hyades 等 Eclipse 項(xiàng)目的基礎(chǔ),大多數(shù) IBM WebSphere Studio 產(chǎn)品也都使用它。EMF 第二版的開(kāi)發(fā)已經(jīng)開(kāi)始,開(kāi)發(fā)構(gòu)建應(yīng)該很快就會(huì)出爐。第二版開(kāi)發(fā)計(jì)劃中包括更好的 XML Schema 支持、更靈活的代碼生成方式以及模型之間的映射機(jī)制。
讓工具自己說(shuō)話
商業(yè)宣傳已經(jīng)說(shuō)得夠多了。現(xiàn)在讓我們直接進(jìn)入代碼中,看看 EMF 到底能做些什么。下面的例子都是用 Eclipse 3.0M7 和 EMF 2.0.0,再加上與之匹配的 XSD 工具箱實(shí)現(xiàn)的。現(xiàn)在有四種獨(dú)立的 EMF 開(kāi)發(fā)流程,每一種都適用于不同版本的 Eclipse,所以一定要保證根據(jù)您的 Eclipse 版本選擇了正確的 EMF 版本(請(qǐng)參閱 參考資料中的鏈接,獲取這些插件)。
我們將以一個(gè)簡(jiǎn)單的 Web 論壇為例,向您展示最重要的特性。模型的根為 Forum
,下面包括一組 Member
和 Topic
。每一個(gè) Topic
都具有一個(gè) TopicCategory
(枚舉類型), Member
和 Topic
通過(guò) Post
類間接相關(guān)聯(lián),這兩者之間也存在直接關(guān)聯(lián),因?yàn)?Member
可以創(chuàng)建 Topic
。
用 UML 和 Omondo 創(chuàng)建 EMF 模型
Omondo 的 UML 插件是在 Eclipse 中創(chuàng)建 UML 文檔的方便可靠的工具。它看起來(lái)就像是 Rational Rose 受冷落的小兄弟,但除非是您需要特別強(qiáng)大的功能,否則用它就可以工作得很好了。不過(guò),該工具尚不支持 Eclipse 3,所以我采用 Eclipse 2.1 來(lái)創(chuàng)建 UML 類圖。
一開(kāi)始,我們創(chuàng)建一個(gè)新的 Java 項(xiàng)目 UMLForum,以及一個(gè)新包 com.ibm.example.forum
。再創(chuàng)建一個(gè)新的 EMF 類圖, forum.ucd
,存放在 src/com/ibm/example/forum 下。目錄中創(chuàng)建了兩個(gè)文件,forum.ecd 和 forum.ecore。向類圖中增加一個(gè)新類,名為 Forum
,然后單擊 Finished。向 Forum
類中增加一條屬性描述,類型為 EString
(對(duì)于所有的簡(jiǎn)單 Java 類都有相應(yīng)的 Ecore 類),如圖 1 所示。對(duì)于屬性的特性,只選擇 changeable
,并將范圍設(shè)為從 0 到 1。
如果您過(guò)一會(huì)改主意了,想使用其他的特性,可以打開(kāi) Properties 視圖,選擇其中的類或?qū)傩浴?/p>
圖 1. 新建的 Forum 類及其屬性的性質(zhì)
?
對(duì)于下列接口重復(fù)上述步驟:
接口
|
屬性
|
類型
|
Member |
nickname |
EString
|
Topic |
title |
EString
|
Post |
comment |
EString
|
為定義關(guān)聯(lián),我們可以選中關(guān)聯(lián)按鈕,然后單擊關(guān)聯(lián)的源( Forum
)和目標(biāo)( Member
)。這樣將打開(kāi)關(guān)聯(lián)屬性設(shè)置對(duì)話框。在其中將名字設(shè)置為 members
,確保僅僅選擇了 changeable 和 containment,然后將上限設(shè)為 -1。在第二個(gè) Association End 選項(xiàng)卡中,取消選中的 Navigable,然后單擊 Ok。對(duì) Forum
和 Topic
也執(zhí)行相同的操作,屬性名稱從 members
改為 topics
。取消選中的 navigable,從而創(chuàng)建一個(gè)無(wú)方向的關(guān)聯(lián),但我們想讓其他屬性都保持為雙向。
按照下表所示完成關(guān)聯(lián)設(shè)置:
源
|
目標(biāo)
|
關(guān)聯(lián)
|
名稱
|
特性
|
范圍
|
Member |
Topic |
1st Association |
topicsCreated
|
changeable |
0 到 1 |
|
|
2nd Association |
creator
|
changeable |
0 到 1 |
Topic |
Post |
1st Association |
posts
|
Containment, changeable |
0 到 -1 |
|
|
2nd Association |
topic
|
changeable |
0 到 1 |
Member |
Post |
1st Association |
posts
|
changeable |
0 到 -1 |
|
2nd Association |
author
|
changeable |
0 到 1 |
?
最后,我們要定義一個(gè)枚舉類型,用于表示 topic 有多少不同的類型。創(chuàng)建一個(gè)新的枚舉類型,名字叫做 TopicCategory
。Literal 中加入以下的內(nèi)容:
-
ANNOUNCEMENT
, value = 0
-
GUEST_BOOK
, value = 1
-
DISCUSSION
, value = 2
然后,為 Topic 定義一個(gè)新屬性,叫做 category
,類型為 TopicCategory
,changeable,范圍 0-1。如果您愿意的話,可以在屬性標(biāo)簽上對(duì)默認(rèn)值進(jìn)行修改,但我們將接受 ANNOUNCEMENT
的默認(rèn)值。
圖 2. 完成后的 UML 類模型
一旦您完成了圖 2 所示的 UML 類圖,下一步就是創(chuàng)建一個(gè) EMF 模型。為此,需要先創(chuàng)建一個(gè)新的 EMF 項(xiàng)目( File > New > Project... > Eclipse Modeling Framework > EMF Project),并用 com.ibm.example.forum
作為該項(xiàng)目的名稱(這是插件名稱的基礎(chǔ),因此我們遵從 Eclipse 插件的命名規(guī)范)。在下一個(gè)頁(yè)面上,選擇 Load from an EMF core model,然后單擊 Next。從文件系統(tǒng)中加載 ecore 文件,它將自動(dòng)填充 Generator 的模型名。在最后一個(gè)頁(yè)面上,單擊包旁邊的復(fù)選框,然后單擊 Finish。這樣就創(chuàng)建好了 EMF 模型,它的名字叫做 forum.genmodel。您可以從 使用生成的 EMF 模型一節(jié)中了解到這個(gè)模型是什么,以及如何使用它。
用 XML Schema
創(chuàng)建 EMF 模型
XML Schema(XSD)的表現(xiàn)能力不如 UML 或帶注釋的 Java 代碼那么強(qiáng)大,例如,它不能表達(dá)出雙向引用的關(guān)聯(lián)。但是由于默認(rèn)的的序列化方法要使用到您的方案,因此 XSD 對(duì)定制序列化來(lái)說(shuō)是最快的方法。如果您希望為模型生成非常詳細(xì)的 XML/XMI,那么 XSD 就是必然的選擇。
清單 1. forum.xsd 的片段?
<xsd:simpleType name="TopicCategory">
<xsd:restriction base="xsd:NCName">
<xsd:enumeration value="Announcement"/>
<xsd:enumeration value="GuestBook"/>
<xsd:enumeration value="Discussion"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="Post">
<xsd:sequence>
<xsd:element name="comment" type="xsd:string"/>
<xsd:element name="author" type="xsd:anyURI" ecore:reference="forum:Member"/>
<xsd:element name="topic" type="xsd:anyURI" ecore:reference="forum:Topic"/>
</xsd:sequence>
</xsd:complexType>
|
在清單 1 中,您可以看到枚舉是如何表示的,也能從中了解到如何定義一個(gè)具有指向其他類型的元素和引用的類型。在 Forum 這個(gè)例子中,我們僅僅使用了字符串屬性 "xsd:string"
,但是其他簡(jiǎn)單 Java 類型也是支持的。有關(guān) XML Schema 和 forum.xsd 文件的更多信息,請(qǐng)參閱 參考資料。
一旦完成了 XSD,下一步就是創(chuàng)建 EMF 模型。方法與 UML 模型中類似,先創(chuàng)建一個(gè)新的 EMF 項(xiàng)目( File > New > Project... > Eclipse Modeling Framework > EMF Project),項(xiàng)目名稱為 com.ibm.example.forum(這是插件名稱的基礎(chǔ),因此我們遵從 Eclipse 插件的命名規(guī)范)。在下一個(gè)頁(yè)面上選擇 Load from an XML Schema,然后單擊 Next。在文件系統(tǒng)中找出 XSD 文件并加載,然后 Generator 中的模型名就會(huì)自動(dòng)填充。在最后一個(gè)頁(yè)面上,單擊包旁邊的復(fù)選框,然后單擊 Finish。這樣就創(chuàng)建了一個(gè) EMF 模型,名字叫做 forum.genmodel。 您可以從 使用生成的 EMF 模型一節(jié)中了解到這個(gè)模型是什么,以及如何使用它。
用帶注釋的 Java 代碼創(chuàng)建 EMF 模型
如果通過(guò) Java 代碼定義 EMF 模型,我們可以用 Interface 列出每一個(gè)類的屬性,以及類之間的關(guān)系。這樣得到的內(nèi)容并不充足,無(wú)法定義我們想要的全部信息,所以 EMF 使用了特殊的 JavaDoc 標(biāo)簽。每一個(gè)屬性或類,如果是 EMF 模型的一部分,就必須在其 JavaDoc 中包含一個(gè) @model
標(biāo)簽,也可以包含一個(gè)附加屬性列表。比如說(shuō),如果要構(gòu)造如上面圖 2 所示的一個(gè)對(duì)象模型,我們對(duì) Forum 的定義看起來(lái)應(yīng)該像清單 2 的樣子。
清單 2. 帶注釋的 Forum.java
package com.ibm.example.forum;
import java.util.List;
/**
*
@model
*/
public interface Forum {
/**
*
@model type="Topic" containment="true"
*/
List getTopics();
/**
*
@model type="Member" containment="true"
*/
List getMembers();
/**
*
@model
*/
String getDescription();
}
|
清單 2 聲明了一個(gè)叫做 Forum
的對(duì)象,它具有一條 String 類型的描述信息和兩個(gè)孩子,一個(gè)是 Topic
列表,還有一個(gè)是 Member
列表。這兩個(gè)孩子都包含在 Forum
之內(nèi)。
對(duì)于簡(jiǎn)單的屬性,如 描述信息
, @model
標(biāo)簽就足夠了,但對(duì)于 list 而言,您也需要為其指明類型。 containment
屬性是可選的,但是如果某個(gè)對(duì)象是被包含的,那么它就和其容器一起被序列化。為了簡(jiǎn)化序列化的過(guò)程,我們要保證所有的對(duì)象都是直接或者間接包含在 Forum
中的。其他一些有用的可選屬性如下:
-
opposite
(用于雙向?qū)傩裕?
-
default
(屬性的默認(rèn)值)。
-
transient
(該屬性不能被序列化)。
要獲得完整的屬性列表,請(qǐng)您參閱 參考資料中的 EMF user's guide。
惟一需要當(dāng)心的是枚舉類型。它被定義成一個(gè) Class,而不是其他模型類中的 Interface! 為了明確這一點(diǎn),清單 3 展示了 TopicCategory 枚舉類型是如何實(shí)現(xiàn)的。
清單 3. 枚舉類型 TopicCategory.java??
package com.ibm.example.forum;
/**
* @model
*/
public
class TopicCategory{
/**
* @model name="Announcement"
*/
public static final int ANNOUNCEMENT = 0;
/**
* @model name="GuestBook"
*/
public static final int GUEST_BOOK = 1;
/**
* @model name="Discussion"
*/
public static final int DISCUSSION = 2;
}
|
最后,生成如下所示的三個(gè)接口,模型就完成了:
接口
|
方法
|
模型標(biāo)簽
|
Member |
List getPosts()
|
type="Post" opposite="author" |
|
List getTopicsCreated()
|
type="Topic" opposite="creator" |
|
String getName()
|
|
Topic |
List getPosts()
|
type="Post" opposite="author" |
|
Member getCreator()
|
opposite="topicsCreated" |
|
String getTitle()
|
|
|
TopicCategory getCategory()
|
|
Post |
Member getAuthor
|
opposite="posts" |
|
Topic getTopic()
|
opposite="posts" |
|
String getComment()
|
|
模型定義完成之時(shí),可以生成 EMF 模型( File > New > Other > Eclipse Modeling Framework > EMF Models)。將父目錄設(shè)為 com.ibm.example.forum/src/model, File name設(shè)為 forum.genmodel。在下一個(gè)頁(yè)面上,選擇 Load from annotated Java,然后選中包“forum”旁邊的復(fù)選框。然后單擊 Finish。這樣就創(chuàng)建了一個(gè)名為 forum.genmodel 的 EMF 模型。
使用生成的 EMF 模型
現(xiàn)在您的工作空間中應(yīng)該有一個(gè)生成好的 EMF 模型 forum.genmodel。這個(gè)模型中包含您輸入其中的所有信息。用默認(rèn)的編輯器打開(kāi)這個(gè)模型(參見(jiàn)圖 3),再打開(kāi) Properties 視圖,然后檢查模型樹(shù)中每一個(gè)節(jié)點(diǎn)的屬性。前面輸入的所有屬性都可以定制,但是也有一些用于定制代碼生成的屬性。為了驗(yàn)證這一點(diǎn),讓我們?cè)囍薷摹癈opyright Text”或“Generate Schema”之類的屬性,看看會(huì)發(fā)生什么事情。
圖 3. 在默認(rèn)的編輯器中打開(kāi)生成的 EMF 模型
如果對(duì)模型描述(UML、XSD、帶注釋的 Java)進(jìn)行了修改,也可以在 Package Explorer 中用右鍵單擊該模型,然后選擇 Reload,這樣就能夠重新加載模型。這實(shí)現(xiàn)了用 EMF 生成的模型與模型描述之間的同步。重新加載后將會(huì)改變您在生成的模型中修改過(guò)的屬性。
生成 Java 代碼
如果您對(duì)模型描述感到滿意,或者如果您僅僅是想看看所有這一切到底是什么意思,那么現(xiàn)在就可以生成代碼了。在根節(jié)點(diǎn)上單擊鼠標(biāo)右鍵,選擇其中一個(gè)生成選項(xiàng):Model、Edit、或 Editor code。 Generate Model將在當(dāng)前項(xiàng)目中創(chuàng)建該 EMF 模型的 Java 實(shí)現(xiàn)代碼。其中會(huì)包含下列內(nèi)容:
-
com.ibm.example.forum
-- 創(chuàng)建該 Java 類的接口和工廠。
-
com.ibm.example.forum.impl
-- com.ibm.example.forum 中定義的接口的具體實(shí)現(xiàn)。
-
com.ibm.example.forum.util
-- AdapterFactory。
Generate Editor Code將創(chuàng)建 com.ibm.example.forum.edit 項(xiàng)目。其中僅僅包含一個(gè)包, com.ibm.example.forum.provider
,用于控制每一個(gè)模型對(duì)象出現(xiàn)在編輯器中的方式。 Generate Editor Code將在 com.ibm.example.forum.editor 項(xiàng)目中創(chuàng)建一個(gè)插件編輯器示例,其中包含了 com.ibm.example.forum.presentation。這些類提供了一系列簡(jiǎn)單的 JFace 編輯器,可以與您的模型進(jìn)行交互。
為了測(cè)試生成的插件,請(qǐng)依次進(jìn)入 Run > Run... > Run Time Workbench > New。輸入一個(gè)描述性的名稱,然后在 plug-ins 選項(xiàng)卡中,選擇 launch with all workspace and enabled external plug-ins。再在 Common 頁(yè)下,單擊 Display in favorites menu > Run和 Launch in background。最后保存設(shè)置并運(yùn)行。
這時(shí)將出現(xiàn)一個(gè)新的 Eclipse 工作臺(tái),您可以在 Help > About Eclipse Platform > Plug-in Details下面驗(yàn)證您的插件是否可用,如圖 4 所示。
圖 4. Forum 的插件詳細(xì)信息
為了測(cè)試生成的插件,您可以創(chuàng)建一個(gè)新的 Simple 項(xiàng)目,名為“Forum Demo”,然后依次進(jìn)入 New > Other... > Example EMF Model Creation Wizards > Forum Model。給文件取名叫做 sample.forum,然后選擇 Forum 作為 Model Object。這時(shí)會(huì)打開(kāi)一個(gè)窗口,您可以在這里向根中增加新的模型元素。其中包含幾種視圖:Selection、Parent、List、Tree、Table 和 TreeTable。所有這些視圖都顯示相同的數(shù)據(jù),也和 Outline 視圖保持同步。雖然所有視圖都會(huì)在右鍵菜單選項(xiàng)中顯示 New Sibling/New Child,但是我發(fā)現(xiàn),有些視圖在加入兄弟節(jié)點(diǎn)或子節(jié)點(diǎn)時(shí)不能正確響應(yīng)。如果您也遇到這種情況,可以使用 TableTree 視圖,或是在 Outline 視圖中創(chuàng)建新的節(jié)點(diǎn)。圖 5 展示了所生成的插件編輯器。
圖 5. 所生成的插件編輯器
定制生成的代碼
生成的代碼都很不錯(cuò),但是這只是真正應(yīng)用程序的起點(diǎn)。為了滿足我們的需要,我們必須對(duì)其進(jìn)行調(diào)整和定制。我們可以改變所生成的模型類的實(shí)現(xiàn),也可以對(duì)編輯器進(jìn)行擴(kuò)展和定制。好在 EMF 沒(méi)有讓我們失望,我們可以按照自己的想法做任何定制,當(dāng)重新生成代碼時(shí)也不會(huì)丟掉這些內(nèi)容。我們需要做的全部工作就是刪除 @generated
JavaDoc 標(biāo)簽,EMF 的 jmerge
將保證這些方法、屬性或類不被打擾。
為著重說(shuō)明您能對(duì)代碼進(jìn)行哪些修改,讓我們來(lái)看一個(gè)簡(jiǎn)單的例子。在所生成編輯器的 Table 視圖中,兩個(gè)字段都顯示出相同的的值。這一點(diǎn)并不是完全沒(méi)有用處。為了改善一下,我們可以修改第二個(gè)字段,讓它在選中一個(gè) Topic 的時(shí)候顯示 Author,然后增加第三個(gè)字段,給出該 Topic 中的帖子數(shù)。
第一步,向 Table 視圖中額外增加一個(gè)字段。這一步在 com.ibm.example.forum.editor
項(xiàng)目中實(shí)現(xiàn),即 createPages()
方法中的 com.ibm.example.forum.presentation.ForumEditor
。把 @generated 標(biāo)簽刪除,這樣就能持久保存我們的修改,然后定位到表瀏覽窗口所在的位置。按照清單 4 的內(nèi)容對(duì)這段代碼進(jìn)行修改。
清單 4. 修改后的 createPages()?
TableColumn selfColumn = new TableColumn(table, SWT.NONE);
layout.addColumnData(new ColumnWeightData(2, 100, true));
selfColumn.setText("Author");
selfColumn.setResizable(true);
TableColumn numberColumn = new TableColumn(table, SWT.NONE);
layout.addColumnData(new ColumnWeightData(4, 100, true));
numberColumn.setText("Number of Posts");
numberColumn.setResizable(true);
tableViewer.setColumnProperties(new String [] {"a", "b", "c"});
|
這樣就額外增加了一個(gè)字段,但是現(xiàn)在所有的三個(gè)字段都顯示相同的數(shù)據(jù)。為了定制每一個(gè)字段中的數(shù)據(jù),我們需要提供一些 ITableItemLabelProvider
的實(shí)現(xiàn)。打開(kāi) com.ibm.example.forum.provider.TopicItemProvider
,在實(shí)現(xiàn)列表中加入 ITableItemLabelProvider
。我們需要增加兩個(gè)方法, getColumnText(Object, int)
和 getColumnImage(Object, int)
,如清單 5 所示。
清單 5. 加入 TopicItemProvider
public String getColumnText(Object obj, int index) {
if( index == 0 ){
return getText(obj);
}
else if( index == 1 ) {
return ((Topic)obj).getCreator().getNickname();
} else if( index == 2 ) {
return " + ((Topic)obj).getPosts().size();
}
return "unknown";
}
public Object getColumnImage(Object obj, int index) {
return getImage( obj );
}
|
最后,我們需要注冊(cè)這個(gè)提供程序。實(shí)現(xiàn)方法是編輯 com.ibm.example.forum.provider.ForumItemProviderAdapterFactory
的構(gòu)造函數(shù),向支持的類型中增加 ITableItemLabelProvider
,如清單 6 所示。
清單 6. ForumItemProviderFactory 構(gòu)造函數(shù)
public ForumItemProviderAdapterFactory() {
supportedTypes.add(ITableItemLabelProvider.class); supportedTypes.add(IStructuredItemContentProvider.class);
supportedTypes.add(ITreeItemContentProvider.class);
supportedTypes.add(IItemPropertySource.class);
supportedTypes.add(IEditingDomainItemProvider.class);
supportedTypes.add(IItemLabelProvider.class);
}
|
現(xiàn)在我們?cè)龠\(yùn)行這個(gè)插件,打開(kāi)表視圖,就能看到圖 6。請(qǐng)注意,沒(méi)有實(shí)現(xiàn)的 ITableItemLabelProvider
元素將在所有的字段中顯示相同的文本。
圖 6. 修改后的 Table 編輯器
在 Java 中操縱模型
生成的模型代碼看起來(lái)就像是 Java 代碼中增加了一些有用的東西。系統(tǒng)還提供了一種靈活的定制反射 API,對(duì)工具很有用。您也許注意到了,這就是 eGet()
和 eSet()
兩個(gè)方法。在大多數(shù)情況下,我們并不需要關(guān)心它,所以我們還是看看我們感興趣的東西:如何創(chuàng)建、保存和加載模型。讓我們從頭開(kāi)始:加載 EMF 模型。
清單 7. 加載 Forum
// Register the XMI resource factory for the .forummodel extension
Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE;
Map m = reg.getExtensionToFactoryMap();
m.put("forummodel", new XMIResourceFactoryImpl());
ResourceSet resSet=new ResourceSetImpl();
Resource res = resSet.getResource(URI.createURI("model/forum.forummodel"),true);
Forum forum = (Forum)res.getContents().get(0);
|
清單 7 展示了如何給文件關(guān)聯(lián)一個(gè)符合 XMI 格式的擴(kuò)展名“forummodel”,然后用 EMF 的 ResourceSet 解析并加載 forum 模型。我們知道,F(xiàn)orum 是惟一的根元素,所以可以想象, res.getContents().get(0)
將返回一個(gè)且僅有一個(gè) Forum
對(duì)象。如果情況不是這樣,我們還可以從 getContents().iterator()
中取出一個(gè) Iterator,然后分別檢查每一個(gè)元素。
我們還可以換一種方法,創(chuàng)建一個(gè)新的 Forum,然后用程序組裝起來(lái),如清單 8 所示。
清單 8. 初始化 Forum
// initialize model and dependencies
ForumPackageImpl.init();
// retrieve the default Forum factory singleton
ForumFactory factory = ForumFactory.eINSTANCE;
Forum forum = factory.createForum();
forum.setDescription("programmatic forum example");
Member adminMember = factory.createMember();
adminMember.setNickname("Administrator");
forum.getMembers().add( adminMember );
Topic noticeTopic = factory.createTopic();
noticeTopic.setTitle("Notices");
noticeTopic.setCategory(TopicCategory.ANNOUNCEMENT_LITERAL);
noticeTopic.setCreator(adminMember);
forum.getTopic().add( noticeTopic );
|
在這個(gè)例子中,我們首先初始化包,然后創(chuàng)建 ForumFactory,用它生成所有的子對(duì)象。創(chuàng)建完畢之后,就可以像標(biāo)準(zhǔn)的 JavaBean 那樣訪問(wèn)這些對(duì)象。然而,由于我們把 Topic
和 Memeber
之間的 creator/topicsCreated
關(guān)系聲明為雙向,當(dāng)我們調(diào)用 noticeTopic.setCreator(adminMember)
的時(shí)候, adminMember
的 topicsCreated
清單中就包括 noticeTopic
。
一旦我們創(chuàng)建并操縱了 EMF 模型,就很容易將其保存為我們選定的格式(參見(jiàn)清單 9)。
清單 9. 保存 Forum
URI fileURI = URI.createFileURI("model/forum.ecore");
Resource resource = new XMIResourceFactoryImpl().createResource(fileURI);
resource.getContents().add( forum );
try {
resource.save(Collections.EMPTY_MAP);
} catch (IOException e) {
e.printStackTrace();
}
|
在本例中,我們給 URI.createFileURI()
提供了希望保存成的文件名與目標(biāo)格式。這個(gè)例子因?yàn)槭潜4鏋?XMI,所以使用了 XMIResourceFactoryImpl
。一旦創(chuàng)建完畢,所有的模型對(duì)象就如我們所愿的持久保存起來(lái)了。在這個(gè)例子中,除 Forum
之外的每一個(gè)對(duì)象都被另一個(gè)類包含,所以我們只需要對(duì)包含所有孩子的 root 增加這條命令即可。如果某些對(duì)象沒(méi)有 包含
關(guān)系,那么也必須通過(guò) resource.getContents().add()
顯式地將它們加進(jìn)去。否則,當(dāng)您調(diào)用 resource.save()
時(shí)就會(huì)出現(xiàn)異常。
結(jié)束語(yǔ)
Eclipse Modeling Framework 提供了進(jìn)行模型驅(qū)動(dòng)開(kāi)發(fā)的工具。它包含了將您的開(kāi)發(fā)精力集中在模型上而不是實(shí)現(xiàn)細(xì)節(jié)上所必需的元素。其主要關(guān)注的領(lǐng)域是:生成模型時(shí)支持定制、通知、參照完整性以及其他基本特性;生成可定制的模型編輯器;默認(rèn)的序列化。正像例子中展示的那樣,生成的過(guò)程既簡(jiǎn)單又直接,所有的定制代碼都支持定制。序列化或圖形化編輯器等獨(dú)立的工具也可以拉出來(lái)單獨(dú)使用,但所有的部件一起使用才能發(fā)揮完整的效力。EMF 已經(jīng)在很多成功的項(xiàng)目中得到應(yīng)用,它正在蓬勃成長(zhǎng)。
參考資料
使用?Eclipse?Modeling?Framework?進(jìn)行建模,第?2?部分
使用 Eclipse 的 Java Emitter Templates 生成代碼
Adrian Powell
資深軟件開(kāi)發(fā)人員, IBM
2004 年 6 月
Eclipse 的 Java Emitter Templates(JET) 是一個(gè)開(kāi)放源代碼工具,可以在 Eclipse Modeling Framework(EMF)中生成代碼。 JET 與 JSP 非常類似,不同之處在于 JET 功能更強(qiáng)大,也更靈活,可以生成 Java、 SQL 和任何其他語(yǔ)言的代碼,包括 JSP。本文將介紹如何創(chuàng)建和配置 JET,并將其部署到各種環(huán)境中。
Java Emitter Templates(JET) 概述
開(kāi)發(fā)人員通常都使用一些工具來(lái)生成常用的代碼。 Eclipse 用戶可能對(duì)一些標(biāo)準(zhǔn)的工具非常熟悉,這些工具可以為選定的屬性生成 for(;;) 循環(huán), main() 方法, 以及選定屬性的訪問(wèn)方法。將這些簡(jiǎn)單而機(jī)械的任務(wù)變得自動(dòng)化,可以加快編程的速度,并簡(jiǎn)化編程的過(guò)程。在某些情況中,例如為 J2EE 服務(wù)器生成部署代碼,自動(dòng)生成代碼就可以節(jié)省大量時(shí)間,并可以隱藏具體實(shí)現(xiàn)特有的一些復(fù)雜性,這樣就可以將程序部署到不同的 J2EE 服務(wù)器上。自動(dòng)生成代碼的功能并不只是為開(kāi)發(fā)大型工具的供應(yīng)商提供的,在很多項(xiàng)目中都可以使用這種功能來(lái)提高效率。 Eclipse 的 JET 被包裝為 EMF 的一部分,可以簡(jiǎn)單而有效地向項(xiàng)目中添加自動(dòng)生成的代碼。本文將介紹在各種環(huán)境中如何使用 JET 。
JET 是什么?
JET 與 JSP 非常類似:二者使用相同的語(yǔ)法,實(shí)際上在后臺(tái)都被編譯成 Java 程序;二者都用來(lái)將呈現(xiàn)頁(yè)面與模型和控制器分離開(kāi)來(lái);二者都可以接受輸入的對(duì)象作為參數(shù),都可以在代碼中插入字符串值(表達(dá)式),可以直接使用 Java 代碼執(zhí)行循環(huán)、聲明變量或執(zhí)行邏輯流程控制(腳本);二者都可以很好地表示所生成對(duì)象的結(jié)構(gòu),(Web 頁(yè)面、Java 類或文件),而且可以支持用戶的詳細(xì)定制。
JET 與 JSP 在幾個(gè)關(guān)鍵的地方存在區(qū)別。在 JET 中,可以變換標(biāo)記的結(jié)構(gòu)來(lái)支持在不同的語(yǔ)言中生成代碼。通常 JET 程序的輸入都是一個(gè)配置文件,而不是用戶的輸入(當(dāng)然也不禁止這樣使用)。而且對(duì)于一個(gè)給定的工作流來(lái)說(shuō),JET 通常只會(huì)執(zhí)行一次。這并不是技術(shù)上的限制;您可以看到 JET 有很多完全不同的用法。
開(kāi)始
創(chuàng)建模板
要使用 JET,創(chuàng)建一個(gè)新 Java 項(xiàng)目 JETExample,并將源文件夾設(shè)置為 src。為了讓 JET 啟用這個(gè)項(xiàng)目,請(qǐng)點(diǎn)擊鼠標(biāo)右鍵,然后選擇 Add JET Nature。這樣就會(huì)在新項(xiàng)目的根目錄下創(chuàng)建一個(gè) templates 目錄。JET 的缺省配置使用項(xiàng)目的根目錄來(lái)保存編譯出來(lái)的 Java 文件。要修改這種設(shè)置,打開(kāi)該項(xiàng)目的 properties 窗口,選擇 JET Settings,并將 source container 設(shè)置為 src。這樣在運(yùn)行 JET 編譯器時(shí),就會(huì)將編譯出來(lái)的 JET Java 文件保存到這個(gè)正確的源文件夾中。
現(xiàn)在我們已經(jīng)準(zhǔn)備好創(chuàng)建第一個(gè) JET 了。JET 編譯器會(huì)為每個(gè) JET 都創(chuàng)建一個(gè) Java 源文件,因此習(xí)慣上是將模板命名為 NewClass.javajet,其中 NewClass 是要生成的類名。雖然這種命名方式不是強(qiáng)制的,但是這樣可以避免產(chǎn)生混亂。
首先在模板目錄中創(chuàng)建一個(gè)新文件 GenDAO.javajet。這樣系統(tǒng)會(huì)出現(xiàn)一個(gè)對(duì)話框,警告您在這個(gè)新文件的第 1 行第 1 列處有編譯錯(cuò)誤。如果您詳細(xì)地看以下警告信息,就會(huì)發(fā)現(xiàn)它說(shuō) "The jet directive is missing"(沒(méi)有 jet 指令)。雖然這在技術(shù)上沒(méi)有什么錯(cuò)誤,因?yàn)槲覀儎偛胖徊贿^(guò)是創(chuàng)建了一個(gè)空文件,但是這個(gè)警告信息卻很容易產(chǎn)生混亂并誤導(dǎo)我們的思路。單擊 'OK' 關(guān)閉警告對(duì)話框,然后單擊 'Cancel' 清除 New File 對(duì)話框(這個(gè)文件已經(jīng)創(chuàng)建了)。為了防止再次出現(xiàn)這種問(wèn)題,我們的首要問(wèn)題是創(chuàng)建 jet 指令。
每個(gè) JET 都必須以 jet 指令開(kāi)始。這樣可以告訴 JET 編譯器編譯出來(lái)的 Java 模板是什么樣子(并不是模板生成了什么內(nèi)容,而是編譯生成的模板類是什么樣子;請(qǐng)?jiān)彛@個(gè)術(shù)語(yǔ)有些容易讓人迷惑)。此處還要給出一些標(biāo)準(zhǔn)的 Java 類信息。例如,在下面這個(gè)例子中使用了以下信息:
清單 1. 樣例 jet 聲明
明
<%@ jet
package="com.ibm.pdc.example.jet.gen"
class="GenDAO"
imports="java.util.* com.ibm.pdc.example.jet.model.*"
%>
|
清單 1 的內(nèi)容是真正自解釋的。在編譯 JET 模板時(shí),會(huì)創(chuàng)建一個(gè) Java 文件 GenDAO
,并將其保存到 com.ibm.pdc.example.jet.gen
中,它將導(dǎo)入指定的包。重復(fù)一遍,這只是說(shuō)明模板像什么樣子,而不是模板將要生成的內(nèi)容 -- 后者稍后將會(huì)介紹。注意 JET 輸出結(jié)果的 Java 文件名是在 jet
的聲明中定義的,它并不局限于這個(gè)文件名。如果兩個(gè)模板聲明了相同的類名,那么它們就會(huì)相互影響到對(duì)方的變化,而不會(huì)產(chǎn)生任何警告信息。 如果您只是拷貝并粘貼模板文件,而沒(méi)有正確地修改所有的 jet
聲明,那就可能出現(xiàn)這種情況。因?yàn)樵谀0迥夸浿袆?chuàng)建新文件時(shí)會(huì)產(chǎn)生警告,而拷貝和粘貼是非常常見(jiàn)的,因此要自己小心這個(gè)問(wèn)題。
JSP 可以通過(guò)預(yù)先聲明的變量(例如會(huì)話、錯(cuò)誤、上下文和請(qǐng)求)獲取信息, JET 與此類似,也可以使用預(yù)先聲明的變量向模板傳遞信息。JET 只使用兩個(gè)隱式的變量: stringBuffer
,其類型為 StringBuffer
(奇怪吧?),它用來(lái)在調(diào)用 generate()
時(shí)構(gòu)建輸出字符串;以及一個(gè)參數(shù),出于方便起見(jiàn),我們稱之為 argument
,它是 Object
類型。典型的 JET 模板的第一行會(huì)將其轉(zhuǎn)換為一個(gè)更適合的類,如清單 2 所示。
清單 2. JET 參數(shù)的初始化
<% GenDBModel genDBModel = (GenDBModel)argument; %>
package <%= genDBModel.getPackageName() %>;
|
正如您可以看到的一樣,JET 的缺省語(yǔ)法與 JSP 相同:使用 <%...%> 包括代碼,使用 <%= ... %> 打印表達(dá)式的值。與 JSP 類似,正確地使用 <% ... %> 標(biāo)簽就可以添加任何邏輯循環(huán)或結(jié)構(gòu),就像是在任何 Java 方法中一樣。例如:
清單 3. 腳本和表達(dá)式
Welcome <%= user.getName() %>!
<% if ( user.getDaysSinceLastVisit() > 5 ) { %>
Whew, thanks for coming back. We thought we'd lost you!
<% } else { %>
Back so soon? Don't you have anything better to do?
<% } %>
|
在定義完 JET 之后,保存文件并在包瀏覽器中在這個(gè)文件上點(diǎn)擊鼠標(biāo)右鍵,選擇 Compile Template。如果一切正常,就會(huì)在 com.ibm.pdc.example.jet.gen
包中創(chuàng)建一個(gè)類 GenDAO
。其中只有一個(gè)方法 public String generate(Object argument)
(參見(jiàn)清單 4),這樣做的結(jié)果就是在 javajet
模板中定義的內(nèi)容。
清單 4. 一個(gè)基本的 JET 編譯后的 Java 類,其功能是打印 "Hello <%=argument%>"
package com.ibm.pdc.example.jet.gen;
import java.util.*;
public class GenDAO
{
protected final String NL = System.getProperties().getProperty("line.separator");
protected final String TEXT_1 = NL + "Hello, ";
protected final String TEXT_2 = NL + "\t ";
public String generate(Object argument)
{
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(TEXT_1);
stringBuffer.append( argument );
stringBuffer.append(TEXT_2);
return stringBuffer.toString();
}
}
|
準(zhǔn)備公共代碼
編寫(xiě)好模板之后,您可能就會(huì)注意到一些公共的元素,這些元數(shù)會(huì)反復(fù)出現(xiàn),例如所有生成的代碼中都添加的版權(quán)信息。在 JSP 中,這是通過(guò) include
聲明處理的。將所有想要添加的內(nèi)容都放到一個(gè)文件中,并將該文件命名為 'copyright.inc',然后在 javajet
模板中添加 <%@ include file="copyright.inc" %>
語(yǔ)句。所指定的包含文件會(huì)被添加到編譯后的輸出結(jié)果中,因此它可以引用到現(xiàn)在為止已經(jīng)聲明的任何變量。擴(kuò)展名 .inc
可以任意,只是不要采用以 jet
或 JET 結(jié)尾的名字,否則將試圖編譯包含文件,這樣該文件的理解性自然很差。
定制 JET 編譯
如果只使用包含文件還不能滿足要求,您可能會(huì)想添加其他一些方法,或者對(duì)代碼生成過(guò)程進(jìn)行定制;最簡(jiǎn)單的方法是創(chuàng)建一個(gè)新的 JET 骨架。骨架文件就是描述編譯后的 JET 模板樣子的一個(gè)模板。缺省的骨架如清單 5 所示。
清單 5. 缺省的 JET 骨架
public class CLASS
{
public String generate(Object argument)
{
return "";
}
}
|
所有的 import 語(yǔ)句都位于最開(kāi)始, CLASS
會(huì)被替換為在 jet 聲明的 class
屬性中設(shè)置的類名, generate()
方法的代碼會(huì)被替換為執(zhí)行生成操作的代碼。因此,要修改編譯后的模板代碼的樣子,我們只需要?jiǎng)?chuàng)建一個(gè)新的骨架文件并進(jìn)行自己想要的定制即可,但是仍然要在原來(lái)的地方保留基本的元素。
要?jiǎng)?chuàng)建一個(gè)定制的骨架,在 custom.skeleton
模板目錄中創(chuàng)建一個(gè)新文件,如清單 6 所示。
清單 6. 定制 JET 骨架
public class CLASS
{
private java.util.Date getDate() {
return new java.util.Date();
}
public String generate(Object argument) {
return "";
}
}
|
然后在想要使用這個(gè)定制骨架的任何 JET 模板中,向 javajet
文件中的 jet
聲明添加 skeleton="custom.skeleton"
屬性。
或者,也可以使用它對(duì)基類進(jìn)行擴(kuò)充,例如 public class CLASS extends MyGenerator
,并在基類中添加所有必要的幫助器方法。這樣可能會(huì)更加整潔,因?yàn)樗A袅舜a的通用性,并可以簡(jiǎn)化開(kāi)發(fā)過(guò)程,因?yàn)?JET 編譯器并不能總是給出最正確的錯(cuò)誤消息。
定制骨架也可以用來(lái)修改方法名和 generate()
方法的參數(shù)列表,這樣非常挑剔的開(kāi)發(fā)人員就可以任意定制模板。說(shuō) JET 要將 generate()
的代碼替換為要生成的代碼,其實(shí)有些不太準(zhǔn)確。實(shí)際上,它只會(huì)替換在骨架中聲明的最后一個(gè)方法的代碼,因此如果粗心地修改骨架的代碼,就很容易出錯(cuò),而且會(huì)讓您的同事迷惑不解。
使用 CodeGen
正如您可以看到的一樣,模板一旦編譯好之后,就是一個(gè)標(biāo)準(zhǔn)的 Java 類。要在程序中使用這個(gè)類,只需要分發(fā)編譯后的模板類,而不需要分發(fā) javajet
模板。或者,您可能希望讓用戶可以修改模板,并在啟動(dòng)時(shí)自動(dòng)重新編譯模板。EMF 可以實(shí)現(xiàn)這種功能,任何需要這種功能或?qū)Υ烁信d趣的人都可以進(jìn)入 plugins/org.eclipse.emf.codegen.ecore/templates
中,并修改 EMF 生成模型或編輯器的方式。
如果您只是希望可以只分發(fā)編譯后的模板類,那么編譯過(guò)程可以實(shí)現(xiàn)自動(dòng)化。迄今為止,我們只看到了如何使用 JET Eclipse 插件來(lái)編譯 JET 模板,但實(shí)際上我們可以編寫(xiě)一些腳本來(lái)實(shí)現(xiàn)這種功能,或者將生成代碼的工作作為一項(xiàng) ANT 任務(wù)。
運(yùn)行時(shí)編譯模板
要讓最終用戶可以定制模板(以及對(duì)模板的調(diào)試),可以選擇在運(yùn)行時(shí)對(duì)模板進(jìn)行編譯。實(shí)現(xiàn)這種功能有幾種方法,首先我們使用一個(gè)非常有用的類 org.eclipse.emf.codegen.jet.JETEmitter
,它可以對(duì)細(xì)節(jié)進(jìn)行抽象。常見(jiàn)的(但通常是錯(cuò)誤的)代碼非常簡(jiǎn)單,如清單 7 所示。
清單 7. JETEmitter 的簡(jiǎn)單用法(通常是錯(cuò)誤的)
String uri = "platform:/templates/MyClass.javajet";
JETEmitter jetEmitter = new JETEmitter( uri );
String generated = jetEmitter.generate( new NullProgressMonitor(), new Object[]{argument} );
|
如果您試圖在一個(gè)標(biāo)準(zhǔn)的 main()
方法中運(yùn)行這段代碼,就會(huì)發(fā)現(xiàn)第一個(gè)問(wèn)題。 generate()
方法會(huì)觸發(fā)一個(gè) NullPointerException
異常,因?yàn)?JETEmitter
假設(shè)自己正被一個(gè)插件調(diào)用。在初始化過(guò)程中,它將調(diào)用 CodeGenPlugin.getPlugin().getString()
,這個(gè)函數(shù)會(huì)失敗,因?yàn)?CodeGenPlugin.getPlugin()
為空。
解決這個(gè)問(wèn)題有一個(gè)簡(jiǎn)單的方法:將這段代碼放到一個(gè)插件中,這樣的確可以管用,但卻不是完整的解決方法。現(xiàn)在 JETEmitter
的實(shí)現(xiàn)創(chuàng)建了一個(gè)隱藏項(xiàng)目 .JETEmitters
,其中包含了所生成的代碼。然而, JETEmitter
并不會(huì)將這個(gè)插件的 classpath 添加到這個(gè)新項(xiàng)目中,因此,如果所生成的代碼引用了任何標(biāo)準(zhǔn) Java 庫(kù)之外的對(duì)象,都將不能成功編譯。2.0.0 版本初期似乎解決了這個(gè)問(wèn)題,但是到 4 月初為止,這還沒(méi)有完全實(shí)現(xiàn)。要解決這個(gè)問(wèn)題,必須對(duì) JETEmitter
類進(jìn)行擴(kuò)充,使其覆蓋 initialize()
方法,并將其加入您自己的 classpath 項(xiàng)中。Remko Popma 已經(jīng)編寫(xiě)了很好的一個(gè)例子 jp.azzurri.jet.article2.codegen.MyJETEmitter
(參閱 參考資料),這個(gè)例子可以處理這個(gè)問(wèn)題,在 JET 增加這種正確的特性之前都可以使用這種方法。修改后的代碼如清單 8 所示。
清單 8. 正確的 JETEmitter 調(diào)用
String base = Platform.getPlugin(PLUGIN_ID).getDescriptor().getInstallURL().toString();
String uri = base + "templates/GenTestCase.javajet";
MyJETEmitter jetEmitter = new MyJETEmitter( uri );
jetEmitter.addClasspathVariable( "JET_EXAMPLE", PLUGIN_ID);
String generated = jetEmitter.generate( new NullProgressMonitor(),
new Object[]{genClass} );
|
命令行
在命令行中編譯 JET 非常簡(jiǎn)單,不會(huì)受到 classpath 問(wèn)題的影響,這個(gè)問(wèn)題會(huì)使編譯一個(gè) main()
方法都非常困難。在上面這種情況中,難點(diǎn)并不是將 javajet 編譯成 Java 代碼,而是將這個(gè) Java 代碼編譯成 .class
。在命令行中,我們可以更好地控制 classpath,這樣可以分解每個(gè)步驟,最終再組合起來(lái),就可以使整個(gè)工作順利而簡(jiǎn)單。唯一一個(gè)技巧是我們需要以一種 "無(wú)頭" 模式(沒(méi)有用戶界面)來(lái)運(yùn)行 Eclipse,但即便是這個(gè)問(wèn)題也已經(jīng)考慮到了。要編譯 JET,請(qǐng)查看一下 plugins/org.eclipse.emf.codegen_1.1.0/test
。這個(gè)目錄中包含了 Windows 和 Unix 使用的腳本,以及一個(gè)要驗(yàn)證的 JET 例子。
作為一個(gè) ANT 任務(wù)執(zhí)行
有一個(gè) ANT 任務(wù) jetc
,它要么可以采用一個(gè) template
屬性,要么對(duì)多個(gè)模板有一個(gè) fileset
屬性。一旦配置好 jetc
任務(wù)的 classpath 之后,模板的編譯就與標(biāo)準(zhǔn)的 Java 類一樣簡(jiǎn)單。有關(guān)如何獲取并使用這個(gè)任務(wù)的更多信息,請(qǐng)參閱 參考資料。
定制 JET 以生成 JSP
最終,JET 使用 "<%" 和 "%>" 來(lái)標(biāo)記模板,然而這與 JSP 使用的標(biāo)記相同。如果您希望生成 JSP 程序,那就只能修改定界符。這可以在模板開(kāi)頭的 jet
聲明中使用 startTag
和 endTag
屬性實(shí)現(xiàn),如清單 9 所示。在這種情況中,我使用 "[%" 和 "%]" 作為開(kāi)始定界符和結(jié)束定界符。正如您可以看到的一樣, "[%= expression %]" 可以正確處理,就像前面的 "<%= expression %>" 一樣。
清單 9. 修改標(biāo)簽后的 JET 模板
<%@ jet
package="com.ibm.pdc.example.jet.gen"
class="JspGen"
imports="java.util.* "
startTag = "[%"
endTag = "%]"
%>
[% String argValue = (String)argument; %]
package [%= argValue %];
|
結(jié)束語(yǔ)
有一個(gè)不幸的事實(shí):很多代碼都是通過(guò)拷貝/粘貼而實(shí)現(xiàn)重用的,不管是大型軟件還是小型軟件都是如此。很多時(shí)候這個(gè)問(wèn)題并沒(méi)有明顯的解決方案,即使面向?qū)ο笳Z(yǔ)言也不能解決問(wèn)題。在重復(fù)出現(xiàn)相同的基本代碼模式而只對(duì)實(shí)現(xiàn)稍微進(jìn)行了一些修改的情況中,將通用的代碼放到一個(gè)模板中,然后使用 JET 來(lái)生成各種變化,這是一種很好的節(jié)省時(shí)間和精力的辦法。JSP 早已采用了這種方法,因此 JET 可以從 JSP 的成功中借鑒很多東西。JET 使用與 JSP 相同的基本布局和語(yǔ)義,但是允許更靈活的定制。為了實(shí)現(xiàn)更好的控制,模板可以進(jìn)行預(yù)編譯;為了實(shí)現(xiàn)更高的靈活性,也可以在運(yùn)行時(shí)編譯和分發(fā)。
在本系列的下一篇文章中,我們將介紹如何為 Prime Time 生成代碼,這包括允許用戶定制代碼,以及集成以域或方法甚至更細(xì)粒度級(jí)別的修改,從而允許重新生成代碼。我們還會(huì)將它們都綁定到一個(gè)插件中,從而展示一種將生成的代碼集成到開(kāi)發(fā)過(guò)程的方法。
參考資料
關(guān)于作者
Adrian Powell 從剛加入 VisualAge for Java Enterprise Tooling 小組開(kāi)始使用 IBM 的 Java 開(kāi)發(fā)工具,在這兒他花費(fèi)了兩年的時(shí)間來(lái)手工編寫(xiě)一個(gè)代碼生成器。從那以后,他一直從事于 Eclipse 和 VisualAge for Java 中的工具和插件的開(kāi)發(fā),他現(xiàn)在幾乎為 Eclipse 和 VisualAge for Java 的每一個(gè)版本都開(kāi)發(fā)了這種工具和插件。Adrian 目前在 Vancouver Centre for IBM e-Business Innovation 工作,在這兒他正在開(kāi)發(fā)替代軟件。
|
使用?Eclipse?Modeling?Framework?進(jìn)行建模,第?3?部分
使用 Eclipse 的 JMerge 定制生成的代碼和編輯器
Adrian Powell
資深軟件開(kāi)發(fā)人員, IBM
2004 年 6 月
Eclipse Modeling Framework(EMF)中包含了一個(gè)開(kāi)放源代碼的工具 JMerge,這個(gè)工具可以使代碼生成更加靈活,可定制性更好。本文使用一個(gè)例子來(lái)展示如何將 JMerge 添加到一個(gè)應(yīng)用程序中,并為不同的環(huán)境定制 JMerge 的行為。
概述
本系列文章的 前一篇 介紹了有關(guān) Eclipse 的 Java Emitter Templates (JET)和代碼生成的知識(shí),在那篇文章中,您已經(jīng)看到如何通過(guò)使用模板和代碼生成器來(lái)節(jié)省時(shí)間,并實(shí)現(xiàn)模式級(jí)的代碼重用。然而在大部分情況中,這都還不夠。您需要能夠?qū)⑺傻拇a插入現(xiàn)有的代碼中,或者允許以后的開(kāi)發(fā)人員來(lái)定制所生成的代碼,而不需要在重新生成代碼時(shí)重新編寫(xiě)任何內(nèi)容。理想情況下,代碼生成器的創(chuàng)建者希望可以支持今后開(kāi)發(fā)人員所有的需求:從修改方法的實(shí)現(xiàn)、修改各種方法簽名,到修改所生成類的繼承結(jié)構(gòu)。這是一個(gè)非常有趣的問(wèn)題,目前還沒(méi)有很好的通用解決方案;但是有一個(gè)很好的純 Java 的解決方案,稱為 JMerge。
JMerge 是 EMF 中包含的一個(gè)開(kāi)放源代碼的工具,可以讓您定制所生成的模型和編輯器,而重新生成的代碼不會(huì)損壞已經(jīng)修改過(guò)的內(nèi)容。如果描述了如何將新生成的代碼合并到現(xiàn)有定制過(guò)的代碼中,那么 JETEmitter 就可以支持 JMerge。本文通過(guò)一個(gè)例子來(lái)展示其中的一些可用選項(xiàng)。
第一步
假設(shè)您已經(jīng)添加了一個(gè)新項(xiàng)目,在這個(gè)項(xiàng)目中需要為編寫(xiě)的每個(gè)類都創(chuàng)建一個(gè) JUnit 測(cè)試類,這樣必須要對(duì)編寫(xiě)的每個(gè)方法都進(jìn)行測(cè)試。作為一個(gè)認(rèn)真且高效的(或者比較懶的)程序員來(lái)說(shuō),您決定要編寫(xiě)一個(gè)插件,它接受一個(gè) Java 類作為輸入,并生成 JUnit 測(cè)試?yán)拥拇娓╯tub)。您熱情高漲地創(chuàng)建了 JET 和插件, 現(xiàn)在想允許用戶定制所生成的測(cè)試類;然而在原有類的接口發(fā)生變化時(shí),仍然需要重新生成代碼。要實(shí)現(xiàn)這種功能,可以使用 JMerge。
從插件中調(diào)用 JMerge 的代碼非常簡(jiǎn)單(參見(jiàn)清單 1)。這會(huì)創(chuàng)建一個(gè)新的 JMerger 實(shí)例,以及一個(gè) URI merge.xml,設(shè)置要合并的來(lái)源和目標(biāo),并調(diào)用 merger.merge()。然后合并的內(nèi)容就可以展開(kāi)為 merger.getTargetCompilationUnit()。
清單 1. 調(diào)用 JMerge
// ...
JMerger merger = getJMerger();
// set source
merger.setSourceCompilationUnit(
merger.createCompilationUnitForContents(generated));
// set target
merger.setTargetCompilationUnit(
merger.createCompilationUnitForInputStream(
new FileInputStream(target.getLocation().toFile())));
// merge source and target
merger.merge();
// extract merged contents
InputStream mergedContents = new ByteArrayInputStream(
merger.getTargetCompilationUnit().getContents().getBytes());
// overwrite the target with the merged contents
target.setContents(mergedContents, true, false, monitor);
// ...
// ...
private JMerger getJMerger() {
// build URI for merge document
String uri =
Platform.getPlugin(PLUGIN_ID).getDescriptor().getInstallURL().toString();
uri += "templates/merge.xml";
JMerger jmerger = new JMerger();
JControlModel controlModel = new JControlModel( uri );
jmerger.setControlModel( controlModel );
return jmerger;
}
|
要啟動(dòng)這個(gè)過(guò)程,可以使用清單 2 這個(gè)簡(jiǎn)單的 merge.xml。其中聲明了 <merge>
標(biāo)簽,以及缺省的命名空間聲明。這段代碼最主要的部分在 merge:pull
元素中。此處,源類中每個(gè)方法的代碼都會(huì)被替換為目標(biāo)類的對(duì)應(yīng)方法的代碼。如果一個(gè)方法在目標(biāo)類不存在,就會(huì)被創(chuàng)建。如果一個(gè)方法只在源類中存在,而在目標(biāo)類不存在,就會(huì)被保留。
清單 2. 一個(gè)非常簡(jiǎn)單的 merge.xml
<?xml version="1.0" encoding="UTF-8"?>
<merge:options xmlns:merge=
"http://www.eclipse.org/org/eclipse/emf/codegen/jmerge/Options">
<merge:pull
sourceGet="Method/getBody"
targetPut="Method/setBody"/>
</merge:options>
|
區(qū)分生成的方法
這種簡(jiǎn)單的方法有一個(gè)非常明顯的問(wèn)題:每次修改源類并重新生成代碼時(shí),此前所做的修改就全部丟失了。我們需要增加某種機(jī)制來(lái)告訴 JMerge 有些方法已經(jīng)被修改過(guò)了,因此這些方法不應(yīng)該被重寫(xiě)。要實(shí)現(xiàn)這種功能,可以使用 <merge:dictionaryPattern>
元素。 merge:dictionaryPattern
允許您使用正則表達(dá)式來(lái)區(qū)分 Java 元素(參見(jiàn)清單 3)。
清單 3. 一個(gè)簡(jiǎn)單的 dictionaryPattern
<merge:dictionaryPattern
name="generatedMember"
select="Member/getComment"
match=
"\s*@\s*(gen)erated\s*\n"/>
<merge:pull
targetMarkup=
"^gen$"
sourceGet="Method/getBody"
targetPut="Method/setBody"/>
|
dictionaryPattern
定義了一個(gè)正則表達(dá)式,它可以匹配注釋中包含 " @generated
" 的成員。 select
屬性列出了要對(duì)這個(gè)成員的哪些部分與在 match
屬性中給出的正則表達(dá)式進(jìn)行比較。 dictionaryPattern
是由字符串 gen
定義的,它就是 match
屬性值中圓括號(hào)中的內(nèi)容。
merge:pull
元素多了一個(gè)附加屬性 targetMarkup
。這個(gè)屬性可以匹配 dictionaryPattern
,它必須在應(yīng)用合并規(guī)則之前對(duì)目標(biāo)代碼進(jìn)行匹配。此處,我們正在檢查的是目標(biāo)代碼,而不是源代碼,因此用戶可以定制這些代碼。當(dāng)用戶刪除注釋中的 " @generated
" 標(biāo)簽時(shí), dictionaryPattern
就不會(huì)與目標(biāo)代碼匹配,因此就不會(huì)合并這個(gè)方法體。請(qǐng)參見(jiàn)清單 4。
清單 4. 定制代碼
/**
* test case for getName
*
@generated
*/
public void testSimpleGetName() {
// because of the @generated tag,
// any code in this method will be overridden
}
/**
* test case for getName
*/
public void testSimpleSetName() {
// code in this method will not be regenerated
}
|
您或許會(huì)注意到有些元素是不能定制的,任何試圖定制這些代碼的企圖都應(yīng)該被制止。為了支持這種功能,要定義另外一個(gè) dictionaryPattern
,它負(fù)責(zé)在源代碼(而不是目標(biāo)代碼)中查找其他標(biāo)記,例如 @unmodifiable
。然后再定義一條 pull
規(guī)則,來(lái)檢查 sourceMarkup
,而不是 targetMarkup
,這樣就能防止用戶刪除標(biāo)簽或阻礙合并操作。請(qǐng)參見(jiàn)清單5。
清單 5. 不可修改代碼的 merge.xml
<merge:dictionaryPattern
name="generatedUnmodifiableMembers"
select="Member/getComment"
match=
"\s*@\s*(unmod)ifiable\s*\n"/>
<merge:pull
sourceMarkup="^unmod$"
sourceGet="Member/getBody"
targetPut="Member/setBody"/>
|
細(xì)粒度的定制
在使用這種解決方案一段時(shí)間之后,您將注意到有些方法在定制的代碼中具有一些通用的不可修改的代碼(例如跟蹤和日志記錄代碼)。此時(shí)我們既不希望禁止生成代碼,也不希望全部生成整個(gè)方法的代碼,而是希望能夠讓用戶定制一部分代碼。
要實(shí)現(xiàn)這種功能,可以將前面的 pull
目標(biāo)替用清單 6 來(lái)代替。
清單 6. 細(xì)粒度的定制代碼
<!-- if target is generated, transfer -->
<!-- change to sourceMarkup if the source is the standard -->
<merge:pull
targetMarkup="^gen$"
sourceGet="Method/getBody"
sourceTransfer=
"(\s*//\s*begin-user-code.*?//\s*end-user-code\s*)\n"
targetPut="Method/setBody"/>
|
這樣會(huì)只重寫(xiě)字符串 " // begin-user-code
" 之前和 " // end user-code
" 之后的內(nèi)容,因此就可以在定制代碼中保留二者之間的內(nèi)容。在上面的正則表達(dá)式中, "?" 表示在目標(biāo)代碼中,除了要替換的內(nèi)容之外,其他內(nèi)容全部保留。您可以實(shí)現(xiàn)與 JavaDoc 注釋類似的功能,這樣就可以拷貝注釋,同時(shí)為用戶定制預(yù)留了空間。請(qǐng)參見(jiàn)清單 7。
清單 7. 細(xì)粒度的 JavaDoc 定制
<!-- copy comments except between the begin-user-doc
and end-user-doc tags -->
<merge:pull
sourceMarkup="^gen$"
sourceGet="Member/getComment"
sourceTransfer="(\s*<!--\s*begin-user-doc.*?end-user-doc\s*-->\s*)\n"
targetMarkup="^gen$"
targetPut="Member/setComment"/>
|
要支持這種注釋,首先要修改開(kāi)始標(biāo)簽和結(jié)束標(biāo)簽,使其遵循 HTML 注釋語(yǔ)法,這樣它們就不會(huì)出現(xiàn)在所生成的 JavaDoc 中;然后修改 sourceGet
和 targetPut
屬性,以便使用 "Member/ getComment"
和 "Member/ setComment"
。 JMerge 允許您在細(xì)粒度級(jí)別上存取 Java 代碼的不同部分。(更多內(nèi)容請(qǐng)參見(jiàn) 附錄 A)。
下一步
到現(xiàn)在為止,我們已經(jīng)介紹了如何轉(zhuǎn)換方法體,但是 JMerge 還可以處理域、初始化、異常、返回值、import 語(yǔ)句以及其他 Java 元素。它們也采用類似的基本思想,可能只需稍加修改即可。參考 plugins/org.eclipse.emf.codegen_1.1.0/test/merge.xml
就可以知道如何使用這些功能(我使用的是 Eclipse 2.1,因此如果您使用的是其他版本的 Eclipse,那么 ecore 插件的版本可能會(huì)不同)。這個(gè)例子非常簡(jiǎn)單,其中并沒(méi)有使用 sourceTransfer
標(biāo)記,但是該例顯示了處理異常、標(biāo)志和其他 Java 元素的方法。
更復(fù)雜的例子請(qǐng)參見(jiàn) EMF 使用 JMerge 的方法: plugins/org.eclipse.emf.codegen.ecore_1.1.0/templates/emf-merge.xml
。從這個(gè)例子中可以看出 EMF 只允許部分定制 JavaDoc,但是采用上面介紹的一些技巧,就可以為方法體添加支持(這樣可以增強(qiáng) JET 的功能)。
附錄 A:有效的目標(biāo)選項(xiàng)
在 dictionaryPattern
和 pull
規(guī)則中,我們已經(jīng)使用了 " Member/getComment
" 和 " Member/getBody
" 以及它們的 setter 方法,但是還有很多其他可用的選項(xiàng)。JMerge 支持 org.eclipse.jdt.core.jdom.IDOM*
中定義的任何類的匹配和取代。所有可用的選項(xiàng)如表 1 所示。
表 1. 有效的目標(biāo)選項(xiàng)
類型
|
方法
|
注釋
|
CompilationUnit |
getHeader/setHeader |
getName/setName |
Field |
getInitializer/setInitializer |
不包含 "=" |
getName/setName |
變量名 |
getName/setName |
類名 |
Import |
getName/setName |
要么是一個(gè)完全限定的類型名,要么是一個(gè)隨需應(yīng)變的包 |
Initializer |
getName/setName |
|
getBody/setBody |
|
Member |
getComment/setComment |
|
getFlags/setFlags |
例如: abstract, final, native 等。 |
Method |
addException |
|
addParameter |
|
getBody/setBody |
|
getName/setName |
|
getParameterNames/setParameterNames |
|
getParameterTypes/setParameterTypes |
|
getReturnType/setReturnType |
|
Package |
getName/setName |
|
Type |
addSuperInterface |
|
getName/setName |
|
getSuperclass/setSuperclass |
|
getSuperInterfaces/setSuperInterfaces |
|
附錄 B:merge:pull 屬性
表2 給出了 merge:pull
元素的屬性。
表 2. merge:pull 屬性
屬性
|
條件
|
sourceGet |
必需的。該值必須是 附錄 A中列出的一個(gè)選項(xiàng),例如 "Member/getBody"。 |
targetPut |
必需的。該值必須是 附錄 A中列出的一個(gè)選項(xiàng),例如 "Member/setBody"。 |
sourceMarkup |
可選的。用來(lái)在觸發(fā) merge:pull 規(guī)則之前過(guò)濾必須匹配源代碼的 dictionaryPatterns 。格式如 "^dictionaryName$",也可以使用 "|" 將多個(gè) dictionaryPatterns 合并在一行中。 |
targetMarkup |
可選的。用來(lái)在觸發(fā) merge:pull 規(guī)則之前過(guò)濾必須匹配目標(biāo)代碼的 dictionaryPatterns 。格式如 "^dictionaryName$",也可以使用 "|" 將多個(gè) dictionaryPatterns 合并在一行中。 |
sourceTransfer |
可選的。一個(gè)正則表達(dá)式,指定要傳遞給目標(biāo)代碼的源代碼的數(shù)量。 |
參考資料
下載
|
Name
|
|
|
|
Size
|
|
|
|
Download method
|
|
|
|
os-ecemf3/com.ibm.pdc.example.jet_1.0.0.zip |
|
|
|
|
|
|
|
FTP
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
關(guān)于作者
Adrian Powell 從剛開(kāi)始加入 VisualAge for Java Enterprise Tooling 小組開(kāi)始使用 IBM 的 Java 開(kāi)發(fā)工具,在這里他花費(fèi)了兩年的時(shí)間來(lái)手工編寫(xiě)一個(gè)代碼生成器。從那以后,他一直從事 Eclipse 和 VisualAge for Java 中的工具和插件的開(kāi)發(fā),他現(xiàn)在幾乎為 Eclipse 和 VisualAge for Java 的每一個(gè)版本都開(kāi)發(fā)了這種工具和插件。Adrian 目前在 IBM 的 Vancouver Centre 從事于電子商務(wù)創(chuàng)新方面的研究,在這里他閱讀源代碼,為同事提供一些支持。 |
????????????????
?????
?